From 113616b03912439d0cd1b5513fbee0ddafeb0a4d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 03:12:02 +0300 Subject: [PATCH] docs/design/v2: add 12-variant React design archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stash the full claude.ai/design output (12 JSX variants — brutalist, classic, cli, compact, fluent-live, glass, hero-live, minimal, sketches, studio, wizard-live, workshop — plus shared hooks and a standalone HTML preview) for reference when we get to the Wails frontend in Phase 6/7. Source archive: C:\Users\root\Downloads\app(1).zip (~1MB). Not wired into any build target yet — current GUI is the temporary MessageBox stub. Pulling these in is the goal of the Wails phase. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/design/v2/Drover-Go Fluent.html | 51 + .../design/v2/Drover-Go GUI (standalone).html | 184 ++++ docs/design/v2/Drover-Go GUI.html | 249 +++++ docs/design/v2/Drover-Go shortlist.html | 103 ++ docs/design/v2/Drover-Go sketches.html | 27 + docs/design/v2/Drover-Go v2.html | 156 +++ docs/design/v2/design-canvas.jsx | 622 ++++++++++++ docs/design/v2/drover-brutalist.jsx | 335 +++++++ docs/design/v2/drover-classic.jsx | 440 +++++++++ docs/design/v2/drover-cli.jsx | 263 +++++ docs/design/v2/drover-compact.jsx | 293 ++++++ docs/design/v2/drover-fluent-live.jsx | 416 ++++++++ docs/design/v2/drover-glass.jsx | 348 +++++++ docs/design/v2/drover-hero-live.jsx | 424 ++++++++ docs/design/v2/drover-minimal.jsx | 358 +++++++ docs/design/v2/drover-shared.jsx | 308 ++++++ docs/design/v2/drover-sketches.jsx | 903 ++++++++++++++++++ docs/design/v2/drover-studio.jsx | 309 ++++++ docs/design/v2/drover-wizard-live.jsx | 455 +++++++++ docs/design/v2/drover-workshop.jsx | 322 +++++++ 20 files changed, 6566 insertions(+) create mode 100644 docs/design/v2/Drover-Go Fluent.html create mode 100644 docs/design/v2/Drover-Go GUI (standalone).html create mode 100644 docs/design/v2/Drover-Go GUI.html create mode 100644 docs/design/v2/Drover-Go shortlist.html create mode 100644 docs/design/v2/Drover-Go sketches.html create mode 100644 docs/design/v2/Drover-Go v2.html create mode 100644 docs/design/v2/design-canvas.jsx create mode 100644 docs/design/v2/drover-brutalist.jsx create mode 100644 docs/design/v2/drover-classic.jsx create mode 100644 docs/design/v2/drover-cli.jsx create mode 100644 docs/design/v2/drover-compact.jsx create mode 100644 docs/design/v2/drover-fluent-live.jsx create mode 100644 docs/design/v2/drover-glass.jsx create mode 100644 docs/design/v2/drover-hero-live.jsx create mode 100644 docs/design/v2/drover-minimal.jsx create mode 100644 docs/design/v2/drover-shared.jsx create mode 100644 docs/design/v2/drover-sketches.jsx create mode 100644 docs/design/v2/drover-studio.jsx create mode 100644 docs/design/v2/drover-wizard-live.jsx create mode 100644 docs/design/v2/drover-workshop.jsx diff --git a/docs/design/v2/Drover-Go Fluent.html b/docs/design/v2/Drover-Go Fluent.html new file mode 100644 index 0000000..a90bcd9 --- /dev/null +++ b/docs/design/v2/Drover-Go Fluent.html @@ -0,0 +1,51 @@ + + + + +Drover-Go · Fluent + + + + + + + + + + +
+ + + + + + + + diff --git a/docs/design/v2/Drover-Go GUI (standalone).html b/docs/design/v2/Drover-Go GUI (standalone).html new file mode 100644 index 0000000..e52e2fa --- /dev/null +++ b/docs/design/v2/Drover-Go GUI (standalone).html @@ -0,0 +1,184 @@ + + + + + Drover-Go — Desktop GUI explorations + + + + +
+ + + + + + + + DROVER-GO + +
+
Unpacking...
+ + + + + + + + + + \ No newline at end of file diff --git a/docs/design/v2/Drover-Go GUI.html b/docs/design/v2/Drover-Go GUI.html new file mode 100644 index 0000000..561eba6 --- /dev/null +++ b/docs/design/v2/Drover-Go GUI.html @@ -0,0 +1,249 @@ + + + + +Drover-Go — Desktop GUI explorations + + + + + + + + +
+ + + + + + + + + + + diff --git a/docs/design/v2/Drover-Go shortlist.html b/docs/design/v2/Drover-Go shortlist.html new file mode 100644 index 0000000..d8fe13b --- /dev/null +++ b/docs/design/v2/Drover-Go shortlist.html @@ -0,0 +1,103 @@ + + + + +Drover-Go — Shortlist + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/docs/design/v2/Drover-Go sketches.html b/docs/design/v2/Drover-Go sketches.html new file mode 100644 index 0000000..ea6373d --- /dev/null +++ b/docs/design/v2/Drover-Go sketches.html @@ -0,0 +1,27 @@ + + + + +Drover-Go · Style sketches + + + + + + + + + + + + +
+ + + diff --git a/docs/design/v2/Drover-Go v2.html b/docs/design/v2/Drover-Go v2.html new file mode 100644 index 0000000..db7c232 --- /dev/null +++ b/docs/design/v2/Drover-Go v2.html @@ -0,0 +1,156 @@ + + + + +Drover-Go — round 2 (dark only) + + + + + + +
+ + + + + + + + + + + diff --git a/docs/design/v2/design-canvas.jsx b/docs/design/v2/design-canvas.jsx new file mode 100644 index 0000000..9f3fc61 --- /dev/null +++ b/docs/design/v2/design-canvas.jsx @@ -0,0 +1,622 @@ + +// DesignCanvas.jsx — Figma-ish design canvas wrapper +// Warm gray grid bg + Sections + Artboards + PostIt notes. +// Artboards are reorderable (grip-drag), labels/titles are inline-editable, +// and any artboard can be opened in a fullscreen focus overlay (←/→/Esc). +// State persists to a .design-canvas.state.json sidecar via the host +// bridge. No assets, no deps. +// +// Usage: +// +// +// +// +// +// + +const DC = { + bg: '#f0eee9', + grid: 'rgba(0,0,0,0.06)', + label: 'rgba(60,50,40,0.7)', + title: 'rgba(40,30,20,0.85)', + subtitle: 'rgba(60,50,40,0.6)', + postitBg: '#fef4a8', + postitText: '#5a4a2a', + font: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif', +}; + +// One-time CSS injection (classes are dc-prefixed so they don't collide with +// the hosted design's own styles). +if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) { + const s = document.createElement('style'); + s.id = 'dc-styles'; + s.textContent = [ + '.dc-editable{cursor:text;outline:none;white-space:nowrap;border-radius:3px;padding:0 2px;margin:0 -2px}', + '.dc-editable:focus{background:#fff;box-shadow:0 0 0 1.5px #c96442}', + '[data-dc-slot]{transition:transform .18s cubic-bezier(.2,.7,.3,1)}', + '[data-dc-slot].dc-dragging{transition:none;z-index:10;pointer-events:none}', + '[data-dc-slot].dc-dragging .dc-card{box-shadow:0 12px 40px rgba(0,0,0,.25),0 0 0 2px #c96442;transform:scale(1.02)}', + '.dc-card{transition:box-shadow .15s,transform .15s}', + '.dc-card *{scrollbar-width:none}', + '.dc-card *::-webkit-scrollbar{display:none}', + '.dc-labelrow{display:flex;align-items:center;gap:4px;height:24px}', + '.dc-grip{cursor:grab;display:flex;align-items:center;padding:5px 4px;border-radius:4px;transition:background .12s}', + '.dc-grip:hover{background:rgba(0,0,0,.08)}', + '.dc-grip:active{cursor:grabbing}', + '.dc-labeltext{cursor:pointer;border-radius:4px;padding:3px 6px;display:flex;align-items:center;transition:background .12s}', + '.dc-labeltext:hover{background:rgba(0,0,0,.05)}', + '.dc-expand{position:absolute;bottom:100%;right:0;margin-bottom:5px;z-index:2;opacity:0;transition:opacity .12s,background .12s;', + ' width:22px;height:22px;border-radius:5px;border:none;cursor:pointer;padding:0;', + ' background:transparent;color:rgba(60,50,40,.7);display:flex;align-items:center;justify-content:center}', + '.dc-expand:hover{background:rgba(0,0,0,.06);color:#2a251f}', + '[data-dc-slot]:hover .dc-expand{opacity:1}', + ].join('\n'); + document.head.appendChild(s); +} + +const DCCtx = React.createContext(null); + +// ───────────────────────────────────────────────────────────── +// DesignCanvas — stateful wrapper around the pan/zoom viewport. +// Owns runtime state (per-section order, renamed titles/labels, focused +// artboard). Order/titles/labels persist to a .design-canvas.state.json +// sidecar next to the HTML. Reads go via plain fetch() so the saved +// arrangement is visible anywhere the HTML + sidecar are served together +// (omelette preview, direct link, downloaded zip). Writes go through the +// host's window.omelette bridge — editing requires the omelette runtime. +// Focus is ephemeral. +// ───────────────────────────────────────────────────────────── +const DC_STATE_FILE = '.design-canvas.state.json'; + +function DesignCanvas({ children, minScale, maxScale, style }) { + const [state, setState] = React.useState({ sections: {}, focus: null }); + // Hold rendering until the sidecar read settles so the saved order/titles + // appear on first paint (no source-order flash). didRead gates writes until + // the read settles so the empty initial state can't clobber a slow read; + // skipNextWrite suppresses the one echo-write that would otherwise follow + // hydration. + const [ready, setReady] = React.useState(false); + const didRead = React.useRef(false); + const skipNextWrite = React.useRef(false); + + React.useEffect(() => { + let off = false; + fetch('./' + DC_STATE_FILE) + .then((r) => (r.ok ? r.json() : null)) + .then((saved) => { + if (off || !saved || !saved.sections) return; + skipNextWrite.current = true; + setState((s) => ({ ...s, sections: saved.sections })); + }) + .catch(() => {}) + .finally(() => { didRead.current = true; if (!off) setReady(true); }); + const t = setTimeout(() => { if (!off) setReady(true); }, 150); + return () => { off = true; clearTimeout(t); }; + }, []); + + React.useEffect(() => { + if (!didRead.current) return; + if (skipNextWrite.current) { skipNextWrite.current = false; return; } + const t = setTimeout(() => { + window.omelette?.writeFile(DC_STATE_FILE, JSON.stringify({ sections: state.sections })).catch(() => {}); + }, 250); + return () => clearTimeout(t); + }, [state.sections]); + + // Build registries synchronously from children so FocusOverlay can read + // them in the same render. Only direct DCSection > DCArtboard children are + // walked — wrapping them in other elements opts out of focus/reorder. + const registry = {}; // slotId -> { sectionId, artboard } + const sectionMeta = {}; // sectionId -> { title, subtitle, slotIds[] } + const sectionOrder = []; + React.Children.forEach(children, (sec) => { + if (!sec || sec.type !== DCSection) return; + const sid = sec.props.id ?? sec.props.title; + if (!sid) return; + sectionOrder.push(sid); + const persisted = state.sections[sid] || {}; + const srcIds = []; + React.Children.forEach(sec.props.children, (ab) => { + if (!ab || ab.type !== DCArtboard) return; + const aid = ab.props.id ?? ab.props.label; + if (!aid) return; + registry[`${sid}/${aid}`] = { sectionId: sid, artboard: ab }; + srcIds.push(aid); + }); + const kept = (persisted.order || []).filter((k) => srcIds.includes(k)); + sectionMeta[sid] = { + title: persisted.title ?? sec.props.title, + subtitle: sec.props.subtitle, + slotIds: [...kept, ...srcIds.filter((k) => !kept.includes(k))], + }; + }); + + const api = React.useMemo(() => ({ + state, + section: (id) => state.sections[id] || {}, + patchSection: (id, p) => setState((s) => ({ + ...s, + sections: { ...s.sections, [id]: { ...s.sections[id], ...(typeof p === 'function' ? p(s.sections[id] || {}) : p) } }, + })), + setFocus: (slotId) => setState((s) => ({ ...s, focus: slotId })), + }), [state]); + + // Esc exits focus; any outside pointerdown commits an in-progress rename. + React.useEffect(() => { + const onKey = (e) => { if (e.key === 'Escape') api.setFocus(null); }; + const onPd = (e) => { + const ae = document.activeElement; + if (ae && ae.isContentEditable && !ae.contains(e.target)) ae.blur(); + }; + document.addEventListener('keydown', onKey); + document.addEventListener('pointerdown', onPd, true); + return () => { + document.removeEventListener('keydown', onKey); + document.removeEventListener('pointerdown', onPd, true); + }; + }, [api]); + + return ( + + {ready && children} + {state.focus && registry[state.focus] && ( + + )} + + ); +} + +// ───────────────────────────────────────────────────────────── +// DCViewport — transform-based pan/zoom (internal) +// +// Input mapping (Figma-style): +// • trackpad pinch → zoom (ctrlKey wheel; Safari gesture* events) +// • trackpad scroll → pan (two-finger) +// • mouse wheel → zoom (notched; distinguished from trackpad scroll) +// • middle-drag / primary-drag-on-bg → pan +// +// Transform state lives in a ref and is written straight to the DOM +// (translate3d + will-change) so wheel ticks don't go through React — +// keeps pans at 60fps on dense canvases. +// ───────────────────────────────────────────────────────────── +function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) { + const vpRef = React.useRef(null); + const worldRef = React.useRef(null); + const tf = React.useRef({ x: 0, y: 0, scale: 1 }); + + const apply = React.useCallback(() => { + const { x, y, scale } = tf.current; + const el = worldRef.current; + if (el) el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`; + }, []); + + React.useEffect(() => { + const vp = vpRef.current; + if (!vp) return; + + const zoomAt = (cx, cy, factor) => { + const r = vp.getBoundingClientRect(); + const px = cx - r.left, py = cy - r.top; + const t = tf.current; + const next = Math.min(maxScale, Math.max(minScale, t.scale * factor)); + const k = next / t.scale; + // keep the world point under the cursor fixed + t.x = px - (px - t.x) * k; + t.y = py - (py - t.y) * k; + t.scale = next; + apply(); + }; + + // Mouse-wheel vs trackpad-scroll heuristic. A physical wheel sends + // line-mode deltas (Firefox) or large integer pixel deltas with no X + // component (Chrome/Safari, typically multiples of 100/120). Trackpad + // two-finger scroll sends small/fractional pixel deltas, often with + // non-zero deltaX. ctrlKey is set by the browser for trackpad pinch. + const isMouseWheel = (e) => + e.deltaMode !== 0 || + (e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40); + + const onWheel = (e) => { + e.preventDefault(); + if (isGesturing) return; // Safari: gesture* owns the pinch — discard concurrent wheels + if (e.ctrlKey) { + // trackpad pinch (or explicit ctrl+wheel) + zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.01)); + } else if (isMouseWheel(e)) { + // notched mouse wheel — fixed-ratio step per click + zoomAt(e.clientX, e.clientY, Math.exp(-Math.sign(e.deltaY) * 0.18)); + } else { + // trackpad two-finger scroll — pan + tf.current.x -= e.deltaX; + tf.current.y -= e.deltaY; + apply(); + } + }; + + // Safari sends native gesture* events for trackpad pinch with a smooth + // e.scale; preferring these over the ctrl+wheel fallback gives a much + // better feel there. No-ops on other browsers. Safari also fires + // ctrlKey wheel events during the same pinch — isGesturing makes + // onWheel drop those entirely so they neither zoom nor pan. + let gsBase = 1; + let isGesturing = false; + const onGestureStart = (e) => { e.preventDefault(); isGesturing = true; gsBase = tf.current.scale; }; + const onGestureChange = (e) => { + e.preventDefault(); + zoomAt(e.clientX, e.clientY, (gsBase * e.scale) / tf.current.scale); + }; + const onGestureEnd = (e) => { e.preventDefault(); isGesturing = false; }; + + // Drag-pan: middle button anywhere, or primary button on canvas + // background (anything that isn't an artboard or an inline editor). + let drag = null; + const onPointerDown = (e) => { + const onBg = !e.target.closest('[data-dc-slot], .dc-editable'); + if (!(e.button === 1 || (e.button === 0 && onBg))) return; + e.preventDefault(); + vp.setPointerCapture(e.pointerId); + drag = { id: e.pointerId, lx: e.clientX, ly: e.clientY }; + vp.style.cursor = 'grabbing'; + }; + const onPointerMove = (e) => { + if (!drag || e.pointerId !== drag.id) return; + tf.current.x += e.clientX - drag.lx; + tf.current.y += e.clientY - drag.ly; + drag.lx = e.clientX; drag.ly = e.clientY; + apply(); + }; + const onPointerUp = (e) => { + if (!drag || e.pointerId !== drag.id) return; + vp.releasePointerCapture(e.pointerId); + drag = null; + vp.style.cursor = ''; + }; + + vp.addEventListener('wheel', onWheel, { passive: false }); + vp.addEventListener('gesturestart', onGestureStart, { passive: false }); + vp.addEventListener('gesturechange', onGestureChange, { passive: false }); + vp.addEventListener('gestureend', onGestureEnd, { passive: false }); + vp.addEventListener('pointerdown', onPointerDown); + vp.addEventListener('pointermove', onPointerMove); + vp.addEventListener('pointerup', onPointerUp); + vp.addEventListener('pointercancel', onPointerUp); + return () => { + vp.removeEventListener('wheel', onWheel); + vp.removeEventListener('gesturestart', onGestureStart); + vp.removeEventListener('gesturechange', onGestureChange); + vp.removeEventListener('gestureend', onGestureEnd); + vp.removeEventListener('pointerdown', onPointerDown); + vp.removeEventListener('pointermove', onPointerMove); + vp.removeEventListener('pointerup', onPointerUp); + vp.removeEventListener('pointercancel', onPointerUp); + }; + }, [apply, minScale, maxScale]); + + const gridSvg = `url("data:image/svg+xml,%3Csvg width='120' height='120' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M120 0H0v120' fill='none' stroke='${encodeURIComponent(DC.grid)}' stroke-width='1'/%3E%3C/svg%3E")`; + return ( +
+
+
+ {children} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// DCSection — editable title + h-row of artboards in persisted order +// ───────────────────────────────────────────────────────────── +function DCSection({ id, title, subtitle, children, gap = 48 }) { + const ctx = React.useContext(DCCtx); + const sid = id ?? title; + const all = React.Children.toArray(children); + const artboards = all.filter((c) => c && c.type === DCArtboard); + const rest = all.filter((c) => !(c && c.type === DCArtboard)); + const srcOrder = artboards.map((a) => a.props.id ?? a.props.label); + const sec = (ctx && sid && ctx.section(sid)) || {}; + + const order = React.useMemo(() => { + const kept = (sec.order || []).filter((k) => srcOrder.includes(k)); + return [...kept, ...srcOrder.filter((k) => !kept.includes(k))]; + }, [sec.order, srcOrder.join('|')]); + + const byId = Object.fromEntries(artboards.map((a) => [a.props.id ?? a.props.label, a])); + + return ( +
+
+ ctx && sid && ctx.patchSection(sid, { title: v })} + style={{ fontSize: 28, fontWeight: 600, color: DC.title, letterSpacing: -0.4, marginBottom: 6, display: 'inline-block' }} /> + {subtitle &&
{subtitle}
} +
+
+ {order.map((k) => ( + ctx && ctx.patchSection(sid, (x) => ({ labels: { ...x.labels, [k]: v } }))} + onReorder={(next) => ctx && ctx.patchSection(sid, { order: next })} + onFocus={() => ctx && ctx.setFocus(`${sid}/${k}`)} /> + ))} +
+ {rest} +
+ ); +} + +// DCArtboard — marker; rendered by DCArtboardFrame via DCSection. +function DCArtboard() { return null; } + +function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorder, onFocus }) { + const { id: rawId, label: rawLabel, width = 260, height = 480, children, style = {} } = artboard.props; + const id = rawId ?? rawLabel; + const ref = React.useRef(null); + + // Live drag-reorder: dragged card sticks to cursor; siblings slide into + // their would-be slots in real time via transforms. DOM order only + // changes on drop. + const onGripDown = (e) => { + e.preventDefault(); e.stopPropagation(); + const me = ref.current; + // translateX is applied in local (pre-scale) space but pointer deltas and + // getBoundingClientRect().left are screen-space — divide by the viewport's + // current scale so the dragged card tracks the cursor at any zoom level. + const scale = me.getBoundingClientRect().width / me.offsetWidth || 1; + const peers = Array.from(document.querySelectorAll(`[data-dc-section="${sectionId}"] [data-dc-slot]`)); + const homes = peers.map((el) => ({ el, id: el.dataset.dcSlot, x: el.getBoundingClientRect().left })); + const slotXs = homes.map((h) => h.x); + const startIdx = order.indexOf(id); + const startX = e.clientX; + let liveOrder = order.slice(); + me.classList.add('dc-dragging'); + + const layout = () => { + for (const h of homes) { + if (h.id === id) continue; + const slot = liveOrder.indexOf(h.id); + h.el.style.transform = `translateX(${(slotXs[slot] - h.x) / scale}px)`; + } + }; + + const move = (ev) => { + const dx = ev.clientX - startX; + me.style.transform = `translateX(${dx / scale}px)`; + const cur = homes[startIdx].x + dx; + let nearest = 0, best = Infinity; + for (let i = 0; i < slotXs.length; i++) { + const d = Math.abs(slotXs[i] - cur); + if (d < best) { best = d; nearest = i; } + } + if (liveOrder.indexOf(id) !== nearest) { + liveOrder = order.filter((k) => k !== id); + liveOrder.splice(nearest, 0, id); + layout(); + } + }; + + const up = () => { + document.removeEventListener('pointermove', move); + document.removeEventListener('pointerup', up); + const finalSlot = liveOrder.indexOf(id); + me.classList.remove('dc-dragging'); + me.style.transform = `translateX(${(slotXs[finalSlot] - homes[startIdx].x) / scale}px)`; + // After the settle transition, kill transitions + clear transforms + + // commit the reorder in the same frame so there's no visual snap-back. + setTimeout(() => { + for (const h of homes) { h.el.style.transition = 'none'; h.el.style.transform = ''; } + if (liveOrder.join('|') !== order.join('|')) onReorder(liveOrder); + requestAnimationFrame(() => requestAnimationFrame(() => { + for (const h of homes) h.el.style.transition = ''; + })); + }, 180); + }; + document.addEventListener('pointermove', move); + document.addEventListener('pointerup', up); + }; + + return ( +
+
+
+ +
+
+ e.stopPropagation()} + style={{ fontSize: 15, fontWeight: 500, color: DC.label, lineHeight: 1 }} /> +
+
+ +
+ {children ||
{id}
} +
+
+ ); +} + +// Inline rename — commits on blur or Enter. +function DCEditable({ value, onChange, style, tag = 'span', onClick }) { + const T = tag; + return ( + e.stopPropagation()} + onBlur={(e) => onChange && onChange(e.currentTarget.textContent)} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }} + style={style}>{value} + ); +} + +// ───────────────────────────────────────────────────────────── +// Focus mode — overlay one artboard; ←/→ within section, ↑/↓ across +// sections, Esc or backdrop click to exit. +// ───────────────────────────────────────────────────────────── +function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) { + const ctx = React.useContext(DCCtx); + const { sectionId, artboard } = entry; + const sec = ctx.section(sectionId); + const meta = sectionMeta[sectionId]; + const peers = meta.slotIds; + const aid = artboard.props.id ?? artboard.props.label; + const idx = peers.indexOf(aid); + const secIdx = sectionOrder.indexOf(sectionId); + + const go = (d) => { const n = peers[(idx + d + peers.length) % peers.length]; if (n) ctx.setFocus(`${sectionId}/${n}`); }; + const goSection = (d) => { + const ns = sectionOrder[(secIdx + d + sectionOrder.length) % sectionOrder.length]; + const first = sectionMeta[ns] && sectionMeta[ns].slotIds[0]; + if (first) ctx.setFocus(`${ns}/${first}`); + }; + + React.useEffect(() => { + const k = (e) => { + if (e.key === 'ArrowLeft') { e.preventDefault(); go(-1); } + if (e.key === 'ArrowRight') { e.preventDefault(); go(1); } + if (e.key === 'ArrowUp') { e.preventDefault(); goSection(-1); } + if (e.key === 'ArrowDown') { e.preventDefault(); goSection(1); } + }; + document.addEventListener('keydown', k); + return () => document.removeEventListener('keydown', k); + }); + + const { width = 260, height = 480, children } = artboard.props; + const [vp, setVp] = React.useState({ w: window.innerWidth, h: window.innerHeight }); + React.useEffect(() => { const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', r); return () => window.removeEventListener('resize', r); }, []); + const scale = Math.max(0.1, Math.min((vp.w - 200) / width, (vp.h - 260) / height, 2)); + + const [ddOpen, setDd] = React.useState(false); + const Arrow = ({ dir, onClick }) => ( + + ); + + // Portal to body so position:fixed is the real viewport regardless of any + // transform on DesignCanvas's ancestors (including the canvas zoom itself). + return ReactDOM.createPortal( +
ctx.setFocus(null)} + onWheel={(e) => e.preventDefault()} + style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(24,20,16,.6)', backdropFilter: 'blur(14px)', + fontFamily: DC.font, color: '#fff' }}> + + {/* top bar: section dropdown (left) · close (right) */} +
e.stopPropagation()} + style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, display: 'flex', alignItems: 'flex-start', padding: '16px 20px 0', gap: 16 }}> +
+ + {ddOpen && ( +
+ {sectionOrder.map((sid) => ( + + ))} +
+ )} +
+
+ +
+ + {/* card centered, label + index below — only the card itself stops + propagation so any backdrop click (including the margins around + the card) exits focus */} +
+
e.stopPropagation()} style={{ width: width * scale, height: height * scale, position: 'relative' }}> +
+ {children ||
{aid}
} +
+
+
e.stopPropagation()} style={{ fontSize: 14, fontWeight: 500, opacity: .85, textAlign: 'center' }}> + {(sec.labels || {})[aid] ?? artboard.props.label} + {idx + 1} / {peers.length} +
+
+ + go(-1)} /> + go(1)} /> + + {/* dots */} +
e.stopPropagation()} + style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8 }}> + {peers.map((p, i) => ( +
+
, + document.body, + ); +} + +// ───────────────────────────────────────────────────────────── +// Post-it — absolute-positioned sticky note +// ───────────────────────────────────────────────────────────── +function DCPostIt({ children, top, left, right, bottom, rotate = -2, width = 180 }) { + return ( +
{children}
+ ); +} + +Object.assign(window, { DesignCanvas, DCSection, DCArtboard, DCPostIt }); + diff --git a/docs/design/v2/drover-brutalist.jsx b/docs/design/v2/drover-brutalist.jsx new file mode 100644 index 0000000..7ed242c --- /dev/null +++ b/docs/design/v2/drover-brutalist.jsx @@ -0,0 +1,335 @@ +// drover-brutalist.jsx — Variant 4: Brutalist. +// Hard borders, no rounding. Heavy mono. Monochrome + one acid lime accent. +// Block-style "DOS-meets-zine" feeling. All-caps labels. + +const BrutTheme = { + d: { + bg: '#0c0c0c', panel: '#0c0c0c', alt: '#161616', + border: '#f4f4f4', borderDim: '#3a3a3a', + text: '#f4f4f4', dim: '#a8a8a8', dimmer: '#6a6a6a', + accent: '#c6ff00', danger: '#ff3a52', warn: '#ffaa00', pass: '#7eff84', skip: '#888', + inputBg: '#0c0c0c', primaryFg: '#0c0c0c', + }, + l: { + bg: '#f4f1ea', panel: '#f4f1ea', alt: '#e9e5d8', + border: '#0c0c0c', borderDim: '#bfb9a8', + text: '#0c0c0c', dim: '#3a3a3a', dimmer: '#7a7568', + accent: '#7d8f00', danger: '#b81e34', warn: '#a86c00', pass: '#1a7e2c', skip: '#7a7568', + inputBg: '#f4f1ea', primaryFg: '#f4f1ea', + }, +}; + +function BrutWindow({ mode = 'dark', initial }) { + const t = BrutTheme[mode === 'dark' ? 'd' : 'l']; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const isActive = D.phase === 'active'; + const fontMono = '"JetBrains Mono","IBM Plex Mono","Space Mono",ui-monospace,monospace'; + + return ( +
+ + +
+ {/* form */} +
+ // SOCKS5 PROXY +
+ + D.update({ host: e.target.value })} + placeholder="95.165.72.59 / example.com" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={brutInput(t, fontMono)}/> + + + D.update({ port: e.target.value.replace(/\D/g,'') })} + placeholder="12334" inputMode="numeric" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={brutInput(t, fontMono)}/> + +
+
+ { D.update({ auth: v }); if (v) setTimeout(()=>document.getElementById('br-login')?.focus(),30); }}> + AUTHENTICATION + +
+ + D.update({ login: e.target.value })} placeholder="user" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={brutInput(t, fontMono, !D.form.auth)}/> + + + D.update({ password: e.target.value })} placeholder="******" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={brutInput(t, fontMono, !D.form.auth)}/> + +
+
+ + {D.phase === 'checking' ? '>> CHECKING…' : '>> CHECK CONNECTION'} + +
+ + {/* status */} +
+ // STATUS + +
+ + {/* actions */} +
+
+ + +
+ {isActive && } +
+
+ + +
+ ); +} + +function BrutTitle({ t }) { + const cell = { width: 36, height: 28, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', + borderLeft: `2px solid ${t.border}`, color: t.text }; + return ( +
+
+ D + DROVER-GO + [v0.4.2] +
+
+
+
+
+
+
+ ); +} +function BrutLabel({ t, children }) { + return
{children}
; +} +function BField({ t, label, children, style }) { + return ; +} +function brutInput(t, fontMono, disabled) { + return { + background: t.inputBg, color: disabled ? t.dimmer : t.text, + border: `2px solid ${disabled ? t.borderDim : t.border}`, borderRadius: 0, + padding: '7px 9px', fontSize: 12, fontFamily: fontMono, outline:'none', + width:'100%', boxSizing:'border-box', + }; +} +function BrutCheckbox({ t, checked, onChange, children }) { + return ( + + ); +} +function BrutPrimary({ t, onClick, disabled, children, style }) { + return ( + + ); +} +function BrutStatus({ t, D, palette }) { + if (D.phase === 'idle') { + return ( +
+ + READY TO CHECK_ +
+ ); + } + return ( +
+ {D.phase === 'checking' + ?
+ + RUNNING DIAGNOSTICS… +
+ :
+ {D.lastSummary?.failed === 0 + ? '▮ ALL CHECKS PASSED. READY TO START.' + : `▮ ${D.lastSummary?.failed}/${D.tests.length} FAILED · SOME FEATURES BROKEN`} +
} +
+ {D.tests.map((test, i) => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + const last = i === D.tests.length - 1; + return ( +
+
+ + + {test.label} + + + {r?.metric || (state==='running'?'…':'')} + + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
! {r.error}
+
{r.hint}
+ +
+ )} +
+ ); + })} +
+
+ ); +} +function BrutStartBtn({ t, D }) { + const allFailed = D.lastSummary && D.lastSummary.failed === D.tests.length; + const ok = D.phase === 'checked' && !allFailed; + const active = D.phase === 'active'; + const warning = active && (D.lastSummary?.failed || 0) > 0; + if (active) { + return ( +
+ + ●{warning ? ' ACTIVE / UDP-WARN' : ' ACTIVE'} +
+ ); + } + return {'>> START PROXYING'}; +} +function BrutStopBtn({ t, D }) { + const enabled = D.phase === 'active'; + return ( + + ); +} +function BrutLiveStats({ t, stats }) { + const C = ({ icon, val, lbl }) => ( +
+ {icon}{val} + {lbl && {lbl}} +
+ ); + return ( +
+ } val={window.fmtBytes(stats.up)}/> + } val={window.fmtBytes(stats.down)}/> + + + +
+ ); +} +function BrutLogs({ t, D }) { + return ( +
+ + {D.logsOpen && ( + <> +
+ {[['COPY ALL', () => navigator.clipboard?.writeText(D.logs.map(x=>`[${x.level}] ${x.msg}`).join('\n'))], + ['CLEAR', D.clearLogs], ['OPEN FILE', null]].map(([l, fn]) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)} + style={{ maxHeight: 130, overflowY: 'auto', padding: '7px 14px', fontSize: 10.5, lineHeight: 1.6, color: t.text }}> + {D.logs.map((l,i) => ( +
+ {window.fmtTime(l.t)}{' '} + [{l.level}]{' '} + {l.msg} +
+ ))} +
+ + )} +
+ ); +} + +window.BrutWindow = BrutWindow; diff --git a/docs/design/v2/drover-classic.jsx b/docs/design/v2/drover-classic.jsx new file mode 100644 index 0000000..c94387f --- /dev/null +++ b/docs/design/v2/drover-classic.jsx @@ -0,0 +1,440 @@ +// drover-classic.jsx — Variant 1: Classic devtool. +// Information-dense. Mono metrics. Plain rectangles, hairline borders. +// Sober palette: neutral grays + one teal accent for primary action / success. + +const ClassicTheme = { + // dark + d: { + bg: '#1c1d20', + chrome: '#15161a', + panel: '#22242a', + panelAlt: '#1a1c20', + border: '#34373d', + borderSoft:'#2a2c32', + text: '#dde0e6', + dim: '#8a8f99', + dimmer: '#5b6068', + accent: '#3ea99f', // teal + accentDim: '#2a7d76', + danger: '#d96565', + warn: '#d9a155', + pass: '#5cba8b', + skip: '#7c8088', + inputBg: '#15161a', + btnBg: '#2c2f36', + btnBgH: '#373b43', + primaryBg: '#3ea99f', + primaryFg: '#0c1a18', + }, + l: { + bg: '#f3f4f6', + chrome: '#e8eaef', + panel: '#ffffff', + panelAlt: '#f8f9fb', + border: '#d8dbe1', + borderSoft:'#e6e8ec', + text: '#1c1f24', + dim: '#5c6168', + dimmer: '#8a8f97', + accent: '#2a7d76', + accentDim: '#bdded9', + danger: '#c0463f', + warn: '#a8731e', + pass: '#2f8c5a', + skip: '#7c8088', + inputBg: '#ffffff', + btnBg: '#ffffff', + btnBgH: '#f1f2f5', + primaryBg: '#2a7d76', + primaryFg: '#ffffff', + }, +}; + +function ClassicWindow({ mode = 'dark', initial }) { + const t = ClassicTheme[mode === 'dark' ? 'd' : 'l']; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontMono = '"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace'; + const fontUI = '"Inter","Segoe UI",system-ui,sans-serif'; + const isActive = D.phase === 'active'; + const allChecked = D.phase === 'checked' || D.phase === 'active'; + const failed = D.lastSummary?.failed ?? 0; + + return ( +
+ {/* ─── title bar ─── */} + + + {/* ─── content ─── */} +
+ + {/* Form */} + SOCKS5 Proxy +
+ + D.update({ host: e.target.value })} + placeholder="95.165.72.59 или example.com" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + style={inputStyle(t, fontMono)} /> + + + D.update({ port: e.target.value.replace(/\D/g,'') })} + placeholder="12334" inputMode="numeric" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + style={inputStyle(t, fontMono)} /> + +
+ + { D.update({ auth: v }); if (v) setTimeout(() => document.getElementById('cls-login')?.focus(), 30); }}> + Authentication + + +
+ + D.update({ login: e.target.value })} placeholder="user" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + style={inputStyle(t, fontMono, !D.form.auth)} /> + + + D.update({ password: e.target.value })} placeholder="••••••" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + style={inputStyle(t, fontMono, !D.form.auth)} /> + +
+ + + {D.phase === 'checking' ? 'Checking…' : 'Check connection'} + + + {/* Status */} +
+ Status + + + {/* Action buttons */} +
+
+ + +
+ + {isActive && } + +
+
+ + {/* Logs collapsible */} + +
+ ); +} + +// ─── pieces ───────────────────────────────────────────────────────────────── +function ClassicTitleBar({ t }) { + const cellStyle = { + width: 38, height: 28, display:'flex', alignItems:'center', justifyContent:'center', + color: t.dim, cursor:'pointer', + }; + return ( +
+
+ + Drover-Go + v0.4.2 +
+
+
+
+
e.currentTarget.style.background = '#c0463f'} + onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> + +
+
+
+ ); +} + +function SectionLabel({ children, t }) { + return
{children}
; +} + +function Field({ children, label, t, style }) { + return ( + + ); +} + +function inputStyle(t, fontMono, disabled) { + return { + background: t.inputBg, color: disabled ? t.dimmer : t.text, + border: `1px solid ${t.border}`, borderRadius: 3, padding: '7px 9px', + fontFamily: fontMono, fontSize: 12, outline: 'none', width: '100%', boxSizing: 'border-box', + transition: 'border-color .12s, box-shadow .12s', + }; +} + +function Checkbox({ checked, onChange, children, t }) { + return ( + + ); +} + +function PrimaryBtn({ t, onClick, disabled, children, style }) { + return ( + + ); +} + +// ─── status panel ────────────────────────────────────────────────────────── +function ClassicStatus({ t, D, palette, fontMono }) { + const idle = D.phase === 'idle'; + if (idle) { + return ( +
+ + Ready to check +
+ ); + } + return ( +
+ {/* header */} +
+ {D.phase === 'checking' + ? <> + + Running diagnostics… + + {Object.keys(D.results).length}/{D.tests.length} + + + : D.lastSummary?.failed === 0 + ? All checks passed. Ready to start. + : {D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.} +
+ {/* tests */} +
+ {D.tests.map((test, i) => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + const isLast = i === D.tests.length - 1; + return ( +
+
+ + + {test.label} + + + {r?.metric || (state === 'running' ? '...' : '')} + + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+
+ +
+
+ )} +
+ ); + })} +
+
+ ); +} + +function iconBtnStyle(t) { + return { + width: 20, height: 20, padding: 0, border:'none', background:'transparent', + cursor:'pointer', display:'inline-flex', alignItems:'center', justifyContent:'center', + borderRadius: 2, + }; +} +function smallBtn(t, fontMono) { + return { + display:'inline-flex', alignItems:'center', gap: 4, padding: '3px 7px', + background: t.btnBg, border: `1px solid ${t.border}`, color: t.dim, + borderRadius: 3, fontFamily: fontMono, fontSize: 10.5, cursor:'pointer', + }; +} + +// crude color mix for dark/light. expects hex (#rrggbb), bg can be hex too. amount=share-of-bg. +function mode_mix(fg, bg, amt) { + const a = hexToRgb(fg), b = hexToRgb(bg); + return `rgb(${Math.round(a.r*(1-amt)+b.r*amt)},${Math.round(a.g*(1-amt)+b.g*amt)},${Math.round(a.b*(1-amt)+b.b*amt)})`; +} +function hexToRgb(h) { + const v = h.replace('#',''); + return { r: parseInt(v.slice(0,2),16), g: parseInt(v.slice(2,4),16), b: parseInt(v.slice(4,6),16) }; +} + +// ─── start/stop ──────────────────────────────────────────────────────────── +function ClassicStartBtn({ t, D, fontMono }) { + const phase = D.phase; + const summary = D.lastSummary; + const allFailed = summary && summary.failed === D.tests.length; + const checkedOk = phase === 'checked' && !allFailed; + const active = phase === 'active'; + const warning = active && (summary?.failed || 0) > 0; + + if (active) { + return ( +
+ + Active{warning ? ' · UDP fallback' : ''} +
+ ); + } + return ( + + Start proxying + + ); +} + +function ClassicStopBtn({ t, D }) { + const enabled = D.phase === 'active'; + return ( + + ); +} + +function ClassicLiveStats({ t, stats, fontMono }) { + const cell = (icon, val) => ( +
+ {icon}{val} +
+ ); + return ( +
+ {cell(, window.fmtBytes(stats.up))} + {cell(, window.fmtBytes(stats.down))} + {cell(TCP, stats.tcp)} + {cell(UDP, stats.udp)} + {cell(↑t, window.fmtUptime(stats.uptimeS))} +
+ ); +} + +// ─── logs ────────────────────────────────────────────────────────────────── +function ClassicLogs({ t, D, fontMono }) { + return ( +
+ + {D.logsOpen && ( +
+
+ + + +
+
el && (el.scrollTop = el.scrollHeight)}> + {D.logs.map((l, i) => ( +
+ {window.fmtTime(l.t)} + {' '} + [{l.level}] + {' '} + {l.msg} +
+ ))} +
+
+ )} +
+ ); +} + +window.ClassicWindow = ClassicWindow; diff --git a/docs/design/v2/drover-cli.jsx b/docs/design/v2/drover-cli.jsx new file mode 100644 index 0000000..b73436d --- /dev/null +++ b/docs/design/v2/drover-cli.jsx @@ -0,0 +1,263 @@ +// drover-cli.jsx — Cyber CLI: terminal aesthetic, phosphor green, ASCII feel. +// All-mono. No round corners. Subtle CRT scanline overlay. + +const CLI = { + bg: '#0a0d0a', chrome: '#070a07', panel: '#0d110d', panel2: '#111511', + border: '#1f2a1f', borderSoft: '#162016', + text: '#bfe7c0', // phosphor green-white + dim: '#5a8a5e', dimmer: '#3a5a3d', + accent: '#5cf08a', accentDim: '#2a8a44', + danger: '#ff6b6b', warn: '#ffd166', pass: '#5cf08a', skip: '#6a8a6e', + inputBg: '#0a0e0a', +}; + +function CliWindow({ initial }) { + const t = CLI; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontMono = '"JetBrains Mono","IBM Plex Mono","Cascadia Mono",ui-monospace,monospace'; + const isActive = D.phase === 'active'; + + return ( +
+ {/* CRT scanlines */} +
+ {/* vignette */} +
+ + {/* title bar — like a terminal tab */} +
+
+ $ + drover-go + — ~ — 80×24 +
+
+ {['settings','min','close'].map(k => ( + {k==='close'?'×':k==='min'?'−':'⚙'} + ))} +
+
+ +
+ {/* prompt-style form */} +
┌── socks5 proxy ─────────────────────────────────────────
+
+ + host: + D.update({ host: v })} + placeholder="95.165.72.59 / example.com" onSubmit={D.runCheck} style={{ flex: 1 }}/> + port: + D.update({ port: v.replace(/\D/g,'') })} + placeholder="12334" onSubmit={D.runCheck} style={{ width: 70 }}/> + +
+ +
+ {D.form.auth && ( + + user: + D.update({ login: v })} + placeholder="login" onSubmit={D.runCheck} style={{ flex: 1 }}/> + pass: + D.update({ password: v })} type="password" + placeholder="••••••" onSubmit={D.runCheck} style={{ flex: 1 }}/> + + )} +
+ + + + {/* status */} +
+
┌── status ─────────────────────────────────────────────────
+ {D.phase === 'idle' + ?
+ {'> '} ready to check_ +
+ :
+ {D.phase === 'checking' + ?
{'> '} running diagnostics...
+ : (D.lastSummary?.failed === 0 + ?
[ ✓ ] all checks passed. ready to start.
+ :
[ ! ] {D.lastSummary?.failed}/{D.tests.length} failed. some features won't work.
+ )} +
+ {D.tests.map(test => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + const sym = state==='passed'?'✓':state==='failed'?'✗':state==='skipped'?'~':state==='running'?'›':'·'; + const c = state==='passed'?t.pass:state==='failed'?t.danger:state==='skipped'?t.skip:state==='running'?t.accent:t.dimmer; + return ( +
+
+ {sym} + {test.label} + {r?.metric || (state==='running'?'…':'')} + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
! {r.error}
+
{r.hint}
+
+ )} +
+ ); + })} +
+
+ } + + {/* actions */} +
+
+ {(() => { + const allFailed = D.lastSummary && D.lastSummary.failed === D.tests.length; + const ok = D.phase === 'checked' && !allFailed; + const warning = isActive && (D.lastSummary?.failed||0) > 0; + if (isActive) { + const c = warning ? t.warn : t.accent; + return ( +
+ + [ ACTIVE{warning ? ' / UDP-WARN' : ''} ] +
+ ); + } + return ( + + ); + })()} + +
+ {isActive && ( +
+ ↑ {window.fmtBytes(D.stats.up).padEnd(10)} + ↓ {window.fmtBytes(D.stats.down).padEnd(10)} + tcp={D.stats.tcp} + udp={D.stats.udp} + up={window.fmtUptime(D.stats.uptimeS)} +
+ )} +
+ + {/* logs */} +
+ + {D.logsOpen && ( + <> +
+ {[['copy', () => navigator.clipboard?.writeText(D.logs.map(x=>`[${x.level}] ${x.msg}`).join('\n'))], + ['clear', D.clearLogs], ['file', null]].map(([l, fn]) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)} + style={{ maxHeight: 110, overflowY:'auto', padding:'5px 14px', + fontFamily: fontMono, fontSize: 10, lineHeight: 1.55, color: t.dim }}> + {D.logs.map((l,i) => ( +
+ {window.fmtTime(l.t)}{' '} + {l.level}{' '} + {l.msg} +
+ ))} +
+ + )} +
+
+ ); +} + +function cliHead(t) { return { color: t.dim, fontSize: 10.5, marginBottom: 2, letterSpacing: 0.3 }; } +function CliRow({ children, style }) { + return
{children}
; +} +function CliInput({ value, onChange, placeholder, type, onSubmit, style, t, id }) { + return onChange(e.target.value)} placeholder={placeholder} + onKeyDown={e => e.key === 'Enter' && onSubmit?.()} + style={{ + background: t.inputBg, color: t.accent, border:'none', + borderBottom:`1px solid ${t.borderSoft}`, borderRadius: 0, + padding:'3px 4px', fontFamily: 'inherit', fontSize: 12, + outline:'none', boxSizing:'border-box', textShadow:`0 0 4px ${t.accent}60`, ...style, + }}/>; +} + +window.CliWindow = CliWindow; diff --git a/docs/design/v2/drover-compact.jsx b/docs/design/v2/drover-compact.jsx new file mode 100644 index 0000000..e8fbb62 --- /dev/null +++ b/docs/design/v2/drover-compact.jsx @@ -0,0 +1,293 @@ +// drover-v2.jsx — Round 2: four new dark-only variants. +// Reuses window.useDrover / StatusDot / icons / fmt helpers from drover-shared.jsx. + +// ============================================================================= +// V5 — COMPACT PRO. High-density, table-like. Wireshark/HTOP feel. Mono numbers. +// ============================================================================= +const CPT = { + bg: '#101216', chrome: '#0a0c0f', panel: '#15181d', panel2: '#1a1d23', + border: '#262a31', borderSoft: '#1d2026', + text: '#e1e3e8', dim: '#838892', dimmer: '#535862', + accent: '#7aa9ff', danger: '#e5685a', warn: '#d6a64a', pass: '#5fc888', skip: '#6a6f78', + inputBg: '#0c0e12', primaryFg: '#0b1426', +}; + +function CompactWindow({ initial }) { + const t = CPT; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontMono = '"JetBrains Mono","SF Mono",ui-monospace,Consolas,monospace'; + const fontUI = '"Inter","Segoe UI",system-ui,sans-serif'; + const isActive = D.phase === 'active'; + + return ( +
+ {/* title bar */} +
+
+ + drover-go + 0.4.2 + + · {D.form.host}:{D.form.port}{D.form.auth ? ' · auth' : ''} + +
+
+ + + +
+
+ +
+ {/* Row 1: form, single tight line */} +
+
SOCKS5
+
+ D.update({ host: v })} placeholder="host" style={{ flex: 1 }} + onSubmit={D.runCheck}/> + : + D.update({ port: v.replace(/\D/g,'') })} + placeholder="port" style={{ width: 64 }} onSubmit={D.runCheck}/> + { + D.update({ auth: v }); if (v) setTimeout(()=>document.getElementById('cpt-login')?.focus(),30); + }}>auth +
+ {D.form.auth && ( +
+ D.update({ login: v })} placeholder="login" + style={{ flex: 1 }} onSubmit={D.runCheck}/> + D.update({ password: v })} placeholder="password" + style={{ flex: 1 }} onSubmit={D.runCheck}/> +
+ )} + +
+ + {/* Row 2: status table, full width */} +
+
+ STATUS + + {D.phase === 'idle' && 'idle'} + {D.phase === 'checking' && `${Object.keys(D.results).length}/${D.tests.length}`} + {(D.phase === 'checked' || D.phase === 'active') && + (D.lastSummary?.failed === 0 ? 'all-pass' : `${D.lastSummary?.failed}/${D.tests.length} fail`)} + +
+ {D.phase === 'idle' + ?
+ ready to check_ +
+ : } +
+ + {/* Row 3: actions */} +
+
+ + +
+ {isActive && ( +
+ {window.fmtBytes(D.stats.up)} + {window.fmtBytes(D.stats.down)} + tcp:{D.stats.tcp} + udp:{D.stats.udp} + up:{window.fmtUptime(D.stats.uptimeS)} +
+ )} +
+
+ + {/* Logs */} + +
+ ); +} + +function CompactCell({ children, t, hover }) { + const [h, setH] = React.useState(false); + return
setH(true)} onMouseLeave={()=>setH(false)} style={{ + width: 32, height: 28, display:'flex', alignItems:'center', justifyContent:'center', + cursor:'pointer', background: h && hover ? hover : 'transparent', transition:'background .1s', + }}>{children}
; +} +function cptHead(t) { + return { + fontSize: 9.5, letterSpacing: 1.5, color: t.dim, fontWeight: 700, + marginBottom: 6, display:'flex', alignItems:'center', + }; +} +function CptInput({ value, onChange, placeholder, type, style, onSubmit, t, fontMono, id }) { + return onChange(e.target.value)} placeholder={placeholder} + onKeyDown={e => e.key === 'Enter' && onSubmit?.()} + style={{ + background: t.inputBg, color: t.text, border:`1px solid ${t.border}`, + borderRadius: 2, padding:'5px 7px', fontFamily: fontMono, fontSize: 11.5, + outline:'none', boxSizing:'border-box', ...style, + }}/>; +} +function CptCheck({ checked, onChange, t, children }) { + return ( + + ); +} +function CompactStatusTable({ t, D, palette, fontMono }) { + return ( +
+ {D.tests.map((test) => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ { + state==='passed'?'✓':state==='failed'?'✗':state==='skipped'?'–':state==='running'?'›':'·' + } + + {test.label} + {r?.metric || (state==='running'?'…':'')} + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+
+ )} +
+ ); + })} +
+ ); +} +function CompactStartBtn({ t, D, fontMono }) { + const allFailed = D.lastSummary && D.lastSummary.failed === D.tests.length; + const ok = D.phase === 'checked' && !allFailed; + const active = D.phase === 'active'; + const warning = active && (D.lastSummary?.failed || 0) > 0; + if (active) { + const c = warning ? t.warn : t.pass; + return ( +
+ + ACTIVE{warning ? ' · UDP-FALLBACK' : ''} +
+ ); + } + return ( + + ); +} +function CompactStopBtn({ t, D, fontMono }) { + const enabled = D.phase === 'active'; + return ( + + ); +} +function CompactLogs({ t, D, fontMono }) { + return ( +
+ + {D.logsOpen && ( + <> +
+ {[['copy', () => navigator.clipboard?.writeText(D.logs.map(x=>`[${x.level}] ${x.msg}`).join('\n'))], + ['clear', D.clearLogs], ['file', null]].map(([l, fn]) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)} + style={{ maxHeight: 110, overflowY:'auto', padding:'5px 12px', + fontFamily: fontMono, fontSize: 10, lineHeight: 1.55, color: t.dim, background: t.panel }}> + {D.logs.map((l,i) => ( +
+ {window.fmtTime(l.t)}{' '} + {l.level.padEnd(5)}{' '} + {l.msg} +
+ ))} +
+ + )} +
+ ); +} + +window.CompactWindow = CompactWindow; diff --git a/docs/design/v2/drover-fluent-live.jsx b/docs/design/v2/drover-fluent-live.jsx new file mode 100644 index 0000000..d26a8bb --- /dev/null +++ b/docs/design/v2/drover-fluent-live.jsx @@ -0,0 +1,416 @@ +// drover-fluent-live.jsx — Fluent / Win11 live variant. +// System-native: Segoe UI Variable, acrylic-blue (#0067c0), light cards on a tinted bg. + +const FLUENT_THEME = { + l: { + bg: '#f3f3f3', + chrome: '#fafafa', + panel: '#ffffff', + panelAlt: '#f7f7f7', + border: 'rgba(0,0,0,.06)', + borderHard:'#d6d6d6', + text: '#1a1a1a', + dim: '#666666', + dimmer: '#999999', + accent: '#0067c0', + accentHover:'#005a9e', + danger: '#c42b1c', + warn: '#9d5d00', + pass: '#107c10', + skip: '#888888', + inputBg: '#fafafa', + inputBorder:'#888', + activeBg: '#dff6dd', + activeBorder:'#9bc99b', + warnBg: '#fff4ce', + warnBorder:'#dba81a', + }, + d: { + bg: '#202020', + chrome: '#181818', + panel: '#2b2b2b', + panelAlt: '#252525', + border: 'rgba(255,255,255,.06)', + borderHard:'#3a3a3a', + text: '#f0f0f0', + dim: '#9b9b9b', + dimmer: '#6a6a6a', + accent: '#4cc2ff', + accentHover:'#6cd0ff', + danger: '#ff6b6b', + warn: '#fce100', + pass: '#6ccb5f', + skip: '#7a7a7a', + inputBg: '#2c2c2c', + inputBorder:'#666', + activeBg: '#1d2f1c', + activeBorder:'#3d6e3a', + warnBg: '#3a3017', + warnBorder:'#7a6320', + }, +}; + +function FluentWindow({ mode = 'light', initial }) { + const themeKey = mode === 'dark' ? 'd' : 'l'; + const t = FLUENT_THEME[themeKey]; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontUI = "'Segoe UI Variable','Segoe UI',system-ui,-apple-system,sans-serif"; + const fontMono = "'Cascadia Mono','Cascadia Code',Consolas,'JetBrains Mono',ui-monospace,monospace"; + const isActive = D.phase === 'active'; + + return ( +
+ + +
+ + {/* SOCKS5 form card */} + + SOCKS5 Proxy +
+ + D.update({ host: e.target.value })} + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + placeholder="95.165.72.59 или example.com" + style={fluentInputStyle(t, fontMono)} /> + + + D.update({ port: e.target.value.replace(/\D/g,'') })} + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + placeholder="12334" inputMode="numeric" + style={fluentInputStyle(t, fontMono)} /> + +
+ { D.update({ auth: v }); if (v) setTimeout(() => document.getElementById('flu-login')?.focus(), 30); }}> + Authentication + +
+ + D.update({ login: e.target.value })} + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + placeholder="user" style={fluentInputStyle(t, fontMono, !D.form.auth)} /> + + + D.update({ password: e.target.value })} + onKeyDown={e => e.key === 'Enter' && D.runCheck()} + placeholder="••••••" style={fluentInputStyle(t, fontMono, !D.form.auth)} /> + +
+ + {D.phase === 'checking' ? 'Checking…' : 'Check connection'} + +
+ + {/* Status card */} + + Status + + + + {/* Action buttons */} +
+ + +
+ + {isActive && } + +
+
+ + +
+ ); +} + +function FluentTitleBar({ t, mode }) { + return ( +
+
+ + Drover-Go + 0.4.2 +
+
+ + + +
+
+ ); +} +function FluentTitleBtn({ children, t, hoverBg, hoverFg }) { + const [hover, setHover] = React.useState(false); + return ( +
setHover(true)} onMouseLeave={() => setHover(false)} + style={{ width: 46, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', + cursor: 'pointer', + background: hover ? (hoverBg || 'rgba(127,127,127,.12)') : 'transparent', + color: hover && hoverFg ? hoverFg : 'inherit', + }}>{children}
+ ); +} + +function FluentCard({ t, children, style }) { + return ( +
{children}
+ ); +} +function FluentCardTitle({ t, children }) { + return
{children}
; +} +function FluentField({ t, label, children, style }) { + return ( + + ); +} +function fluentInputStyle(t, fontMono, disabled) { + return { + height: 30, background: t.inputBg, color: disabled ? t.dimmer : t.text, + border: `1px solid ${t.border}`, borderBottom: `1.5px solid ${t.inputBorder}`, + borderRadius: 4, padding: '0 10px', + fontFamily: fontMono, fontSize: 12.5, outline: 'none', width: '100%', boxSizing: 'border-box', + transition: 'border-color .12s', + }; +} +function FluentCheckbox({ t, checked, onChange, children }) { + return ( + + ); +} +function FluentPrimaryBtn({ t, onClick, disabled, children, style }) { + const [hover, setHover] = React.useState(false); + return ( + + ); +} + +function FluentStatus({ t, D, palette, fontMono, themeKey }) { + if (D.phase === 'idle') { + return ( +
+ + Ready to check +
+ ); + } + return ( + <> +
+ {D.phase === 'checking' + ? <> + + Running diagnostics… + + {Object.keys(D.results).length}/{D.tests.length} + + + : D.lastSummary?.failed === 0 + ? All checks passed. Ready to start. + : {D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.} +
+
+ {D.tests.map((test) => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ + + {test.label} + + + {r?.metric || (state === 'running' ? '…' : '')} + + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+ +
+ )} +
+ ); + })} +
+ + ); +} + +function FluentStartBtn({ t, D, fontMono }) { + const phase = D.phase; + const summary = D.lastSummary; + const allFailed = summary && summary.failed === D.tests.length; + const checkedOk = phase === 'checked' && !allFailed; + const active = phase === 'active'; + const warning = active && (summary?.failed || 0) > 0; + if (active) { + return ( +
+ + Active{warning ? ' · UDP fallback' : ''} +
+ ); + } + return ( + + Start proxying + + ); +} +function FluentStopBtn({ t, D }) { + const enabled = D.phase === 'active'; + const [hover, setHover] = React.useState(false); + return ( + + ); +} +function FluentLiveStats({ t, stats, fontMono }) { + const cell = (label, val) => ( +
+ {label}{val} +
+ ); + return ( +
+ {cell(, window.fmtBytes(stats.up))} + {cell(, window.fmtBytes(stats.down))} + {cell(TCP, stats.tcp)} + {cell(UDP, stats.udp)} + {cell(↑t, window.fmtUptime(stats.uptimeS))} +
+ ); +} + +function FluentLogs({ t, D, fontMono }) { + return ( +
+ + {D.logsOpen && ( +
+
+ {[ + ['Copy all', () => navigator.clipboard?.writeText(D.logs.map(l => `[${l.level}] ${l.msg}`).join('\n'))], + ['Clear', D.clearLogs], + ['Open log file', () => {}], + ].map(([l, fn], i) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)}> + {D.logs.map((l, i) => ( +
+ {window.fmtTime(l.t)} + {' '} + [{l.level}] + {' '} + {l.msg} +
+ ))} +
+
+ )} +
+ ); +} + +window.FluentWindow = FluentWindow; diff --git a/docs/design/v2/drover-glass.jsx b/docs/design/v2/drover-glass.jsx new file mode 100644 index 0000000..29e5a41 --- /dev/null +++ b/docs/design/v2/drover-glass.jsx @@ -0,0 +1,348 @@ +// drover-glass.jsx — Variant 3: Glassmorphism. +// Soft animated gradient backdrop. Frosted blur on every panel. Subtle glow on active. + +const GlassTheme = { + d: { + bgGrad: 'radial-gradient(120% 80% at 0% 0%, #2b1d4a 0%, #131326 50%, #0d1024 100%)', + glassBg: 'rgba(255,255,255,0.06)', + glassBg2: 'rgba(255,255,255,0.10)', + border: 'rgba(255,255,255,0.14)', + borderSoft: 'rgba(255,255,255,0.08)', + text: '#f3f1ff', dim: '#bdbcd6', dimmer: '#7e7d99', + accent: '#9b8bff', accentGlow: '#9b8bff', + danger: '#ff8a98', warn: '#ffc46b', pass: '#7ee0b3', skip: '#9aa0aa', + inputBg: 'rgba(0,0,0,0.18)', primaryFg: '#1a1538', + }, + l: { + bgGrad: 'radial-gradient(110% 70% at 0% 0%, #c8d8ff 0%, #f0e8ff 55%, #ffeaf2 100%)', + glassBg: 'rgba(255,255,255,0.55)', + glassBg2: 'rgba(255,255,255,0.75)', + border: 'rgba(255,255,255,0.7)', + borderSoft: 'rgba(255,255,255,0.5)', + text: '#1a1538', dim: '#5e5b7a', dimmer: '#8e8cab', + accent: '#5e4bcf', accentGlow: '#9b8bff', + danger: '#c0463f', warn: '#a06a14', pass: '#2c8a5a', skip: '#8a8aa0', + inputBg: 'rgba(255,255,255,0.6)', primaryFg: '#ffffff', + }, +}; + +function GlassWindow({ mode = 'dark', initial }) { + const t = GlassTheme[mode === 'dark' ? 'd' : 'l']; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontUI = '"Inter","Segoe UI",system-ui,sans-serif'; + const fontMono = '"JetBrains Mono",ui-monospace,monospace'; + const isActive = D.phase === 'active'; + + return ( +
+ {/* gradient orbs */} +
+
+ + {/* title */} + + +
+ + SOCKS5 Proxy +
+ + D.update({ host: e.target.value })} + placeholder="95.165.72.59 или example.com" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={glassInput(t, fontUI)}/> + + + D.update({ port: e.target.value.replace(/\D/g,'') })} + placeholder="12334" inputMode="numeric" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={glassInput(t, fontUI)}/> + +
+
+ { D.update({ auth: v }); if (v) setTimeout(()=>document.getElementById('gl-login')?.focus(),30); }}> + Authentication + +
+ + D.update({ login: e.target.value })} placeholder="user" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={glassInput(t, fontUI, !D.form.auth)}/> + + + D.update({ password: e.target.value })} placeholder="••••••" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={glassInput(t, fontUI, !D.form.auth)}/> + +
+
+ + {D.phase === 'checking' ? 'Checking…' : 'Check connection'} + + + +
+ + Status + + + +
+ +
+ + +
+ {isActive && } +
+
+
+ + +
+ ); +} + +function GlassTitle({ t }) { + const cell = { width: 44, height: 32, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color: t.dim }; + return ( +
+
+ + Drover-Go + 0.4.2 +
+
+
+
+
+
+
+ ); +} + +function GlassPanel({ t, children, style }) { + return
{children}
; +} +function GlassHeader({ t, children }) { + return
{children}
; +} +function GField({ t, label, children, style }) { + return ; +} +function glassInput(t, fontUI, disabled) { + return { + background: t.inputBg, color: disabled ? t.dimmer : t.text, + border: `1px solid ${t.borderSoft}`, borderRadius: 8, padding: '8px 11px', + fontSize: 13, fontFamily: fontUI, outline:'none', width:'100%', boxSizing:'border-box', + backdropFilter: 'blur(8px)', + }; +} +function GlassToggle({ t, checked, onChange, children }) { + return ( + + ); +} +function GlassPrimary({ t, onClick, disabled, children, style }) { + return ( + + ); +} +function GlassStatus({ t, D, palette, fontMono }) { + if (D.phase === 'idle') { + return ( +
+ + Ready to check +
+ ); + } + const allOk = D.lastSummary?.failed === 0; + return ( +
+ {D.phase === 'checking' + ?
+ + Running diagnostics… +
+ :
{allOk ? 'All checks passed. Ready to start.' : `${D.lastSummary?.failed} of ${D.tests.length} checks failed. Some features won't work.`}
} +
+ {D.tests.map(test => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ + {test.label} + + {r?.metric} + + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+
+ )} +
+ ); + })} +
+
+ ); +} +function GlassStartBtn({ t, D }) { + const allFailed = D.lastSummary && D.lastSummary.failed === D.tests.length; + const checkedOk = D.phase === 'checked' && !allFailed; + const active = D.phase === 'active'; + const warning = active && (D.lastSummary?.failed || 0) > 0; + if (active) { + const c = warning ? t.warn : t.pass; + return ( +
+ + Active{warning ? ' · UDP fallback' : ''} +
+ ); + } + return Start proxying; +} +function GlassStopBtn({ t, D }) { + const enabled = D.phase === 'active'; + return ( + + ); +} +function GlassLiveStats({ t, stats, fontMono }) { + const C = ({ icon, val, lbl }) => ( +
+ {icon}{val} + {lbl && {lbl}} +
+ ); + return ( +
+ } val={window.fmtBytes(stats.up)}/> + } val={window.fmtBytes(stats.down)}/> + + + +
+ ); +} +function GlassLogs({ t, D, fontMono }) { + return ( +
+ + {D.logsOpen && ( + <> +
+ {['Copy all','Clear','Open log file'].map((l,i) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)} + style={{ maxHeight: 130, overflowY: 'auto', padding: '8px 16px', + fontFamily: fontMono, fontSize: 11, lineHeight: 1.6, color: t.dim }}> + {D.logs.map((l,i) => ( +
+ {window.fmtTime(l.t)}{' '} + [{l.level}]{' '} + {l.msg} +
+ ))} +
+ + )} +
+ ); +} + +window.GlassWindow = GlassWindow; diff --git a/docs/design/v2/drover-hero-live.jsx b/docs/design/v2/drover-hero-live.jsx new file mode 100644 index 0000000..66f3e93 --- /dev/null +++ b/docs/design/v2/drover-hero-live.jsx @@ -0,0 +1,424 @@ +// drover-hero-live.jsx — Big toggle / VPN-style variant. +// Centerpiece: a giant connect button. Form lives in an expandable section below. +// Compact form (host:port) shown collapsed in active state; full form in idle. + +const HERO_THEME = { + d: { + bg: 'radial-gradient(ellipse at top, #142340 0%, #0e1426 60%, #080d1c 100%)', + chrome: 'transparent', + panel: '#15192a', + panelAlt: '#1a1f33', + border: 'rgba(255,255,255,.08)', + borderHard:'rgba(255,255,255,.18)', + text: '#ffffff', + textDim: '#cbd1d9', + dim: '#7a8499', + dimmer: '#5a6178', + accent: '#5dd4b3', + accentDeep:'#2da085', + accentDarker:'#1a6e5b', + danger: '#ff6b6b', + warn: '#f3c764', + pass: '#5dd4b3', + skip: '#7a8499', + }, + l: { + bg: 'linear-gradient(180deg, #f3f7fb 0%, #e8edf4 100%)', + chrome: 'transparent', + panel: '#ffffff', + panelAlt: '#f5f7fb', + border: 'rgba(0,0,0,.07)', + borderHard:'rgba(0,0,0,.15)', + text: '#0e1830', + textDim: '#3a4356', + dim: '#6a7383', + dimmer: '#9aa3b2', + accent: '#1aa787', + accentDeep:'#0f8c70', + accentDarker:'#0a6655', + danger: '#c0463f', + warn: '#a8731e', + pass: '#1aa787', + skip: '#7a8499', + }, +}; + +function HeroWindow({ mode = 'dark', initial }) { + const themeKey = mode === 'dark' ? 'd' : 'l'; + const t = HERO_THEME[themeKey]; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontUI = "'Inter','Segoe UI',system-ui,sans-serif"; + const fontMono = "'JetBrains Mono','SF Mono',ui-monospace,Consolas,monospace"; + + const [showForm, setShowForm] = React.useState(false); + const [showDetails, setShowDetails] = React.useState(false); + + const phase = D.phase; + const summary = D.lastSummary; + const allFailed = summary && summary.failed === D.tests.length; + const failed = summary?.failed ?? 0; + const isActive = phase === 'active'; + const warning = isActive && failed > 0; + + // big-button click handler. Two-step: if idle/checked, go. + const handleBigClick = () => { + if (phase === 'active') return D.stopProxy(); + if (phase === 'checking') return; + if (phase === 'idle') { + // run check, then auto-start if it passed + (async () => { + await D.runCheck(); + })(); + return; + } + if (phase === 'checked' && !allFailed) D.startProxy(); + if (phase === 'checked' && allFailed) D.runCheck(); + }; + + // After runCheck completes from idle, auto-start if all passed. + const prevPhase = React.useRef(phase); + React.useEffect(() => { + if (prevPhase.current === 'checking' && phase === 'checked') { + const sum = D.lastSummary; + if (sum && sum.failed === 0) { + // small delay so user perceives the "checked" state briefly + setTimeout(() => D.startProxy(), 600); + } + } + prevPhase.current = phase; + }, [phase]); + + // Big-button label / colors + let buttonState; + if (phase === 'active') buttonState = warning ? 'active-warn' : 'active'; + else if (phase === 'checking') buttonState = 'checking'; + else if (phase === 'checked' && allFailed) buttonState = 'failed'; + else buttonState = 'idle'; + + const ringColor = ({ + idle: t.accent, + checking: t.accent, + active: t.accent, + 'active-warn': t.warn, + failed: t.danger, + })[buttonState]; + + const headline = ({ + idle: 'Tap to connect', + checking: 'Verifying…', + active: 'Discord is protected', + 'active-warn': 'Connected · UDP fallback', + failed: 'Connection failed', + })[buttonState]; + + const subline = ({ + idle: `via ${D.form.host}`, + checking: `running 7 checks…`, + active: `via ${D.form.host} · ${window.fmtUptime(D.stats.uptimeS)}`, + 'active-warn': `voice/screen disabled · ${window.fmtUptime(D.stats.uptimeS)}`, + failed: `${failed} of ${D.tests.length} checks failed`, + })[buttonState]; + + return ( +
+ {/* Title bar */} +
+ + Drover-Go + 0.4.2 + + {phase === 'active' && } + {phase === 'idle' && 'idle'} + {phase === 'checking' && 'checking…'} + {phase === 'checked' && (allFailed ? 'failed' : 'ready')} + {phase === 'active' && (warning ? 'connected · warn' : 'connected')} + +
+ + + +
+
+ + {/* Big button + headlines */} +
+ + +
+
{headline}
+
{subline}
+
+ + {/* Stats — only when active */} + {isActive && ( +
+ + + + +
+ )} + + {/* Diagnostic mini-list during check or after */} + {(phase === 'checking' || (phase === 'checked' && !isActive)) && ( +
+
+ {D.tests.map((test) => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ + {test.label} + + {r?.metric || (state === 'running' ? '…' : '')} + + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+
+ )} +
+ ); + })} +
+
+ )} +
+ + {/* Bottom: form (idle) or footer chips (active) */} +
+
+ + {showForm && ( +
+
+ D.update({ host: v })} + onEnter={D.runCheck} placeholder="95.165.72.59 или example.com" label="Host" style={{ flex: 1 }} /> + D.update({ port: v.replace(/\D/g,'') })} + onEnter={D.runCheck} placeholder="12334" label="Port" style={{ width: 90 }} /> +
+ +
+ D.update({ login: v })} onEnter={D.runCheck} + placeholder="user" label="Login" style={{ flex: 1 }} disabled={!D.form.auth} /> + D.update({ password: v })} onEnter={D.runCheck} + placeholder="••••••" label="Password" style={{ flex: 1 }} disabled={!D.form.auth} /> +
+
+ )} +
+ + {/* Logs strip */} + + {showDetails && ( +
el && (el.scrollTop = el.scrollHeight)}> + {D.logs.map((l, i) => ( +
+ {window.fmtTime(l.t)}{' '} + [{l.level}]{' '} + {l.msg} +
+ ))} +
+ )} +
+
+ ); +} + +// ─── Big toggle button ────────────────────────────────────────────────── +function BigToggle({ t, state, onClick, progress = 0 }) { + // size = 168 px circle; outer pulsing rings + const isActive = state === 'active' || state === 'active-warn'; + const isChecking = state === 'checking'; + const isFailed = state === 'failed'; + const isWarn = state === 'active-warn'; + + const accent = isWarn ? t.warn : isFailed ? t.danger : t.accent; + const accentDeep = isWarn ? t.warn : isFailed ? t.danger : t.accentDeep; + const accentDarker = isWarn ? t.warn : isFailed ? t.danger : t.accentDarker; + + const ring = 168; + const stroke = 5; + const r = (ring - stroke) / 2; + const c = 2 * Math.PI * r; + + return ( + + ); +} + +function HeroStat({ icon, v, u, c, mono, t }) { + return ( +
+
{icon}
+
{v}
+
{u}
+
+ ); +} + +function HeroTitleBtn({ children, t, hoverBg }) { + const [hover, setHover] = React.useState(false); + return ( +
setHover(true)} onMouseLeave={() => setHover(false)} + style={{ + width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', + cursor: 'pointer', borderRadius: 5, + background: hover ? (hoverBg || 'rgba(127,127,127,.16)') : 'transparent', + }}>{children}
+ ); +} + +function HeroInput({ t, fontMono, value, onChange, onEnter, placeholder, label, style, type, disabled, id }) { + const [focus, setFocus] = React.useState(false); + return ( + + ); +} + +function fmtCompact(n) { + if (n < 1024) return n.toFixed(0); + if (n < 1024 * 1024) return (n / 1024).toFixed(0); + return (n / 1024 / 1024).toFixed(1); +} +function fmtUnit(n) { + if (n < 1024) return 'B/s'; + if (n < 1024 * 1024) return 'KB/s'; + return 'MB/s'; +} + +window.HeroWindow = HeroWindow; diff --git a/docs/design/v2/drover-minimal.jsx b/docs/design/v2/drover-minimal.jsx new file mode 100644 index 0000000..41a772e --- /dev/null +++ b/docs/design/v2/drover-minimal.jsx @@ -0,0 +1,358 @@ +// drover-minimal.jsx — Variant 2: Minimal / Fluent-aligned. +// Windows 11 Mica feel. Generous spacing. Subtle accents. Segoe UI Variable. +// Soft cards on a tinted background, accent: a calm slate-blue. + +const MinTheme = { + d: { + bg: '#1e1f23', mica: 'rgba(255,255,255,0.02)', chrome: 'rgba(255,255,255,0.03)', + panel: 'rgba(255,255,255,0.04)', panel2: 'rgba(255,255,255,0.06)', + border: 'rgba(255,255,255,0.08)', borderStrong: 'rgba(255,255,255,0.14)', + text: '#f3f3f3', dim: '#b0b3ba', dimmer: '#7a7d85', + accent: '#76b3ff', accentBg: 'rgba(118,179,255,0.16)', + danger: '#ff7a7a', warn: '#ffb86b', pass: '#7ad29c', skip: '#9aa0aa', + inputBg: 'rgba(255,255,255,0.04)', inputBorder: 'rgba(255,255,255,0.16)', + primaryBg: '#76b3ff', primaryFg: '#0d1722', + }, + l: { + bg: '#f6f6f8', mica: 'rgba(255,255,255,0.5)', chrome: 'rgba(255,255,255,0.65)', + panel: '#ffffff', panel2: '#fafafc', + border: 'rgba(0,0,0,0.06)', borderStrong: 'rgba(0,0,0,0.14)', + text: '#1c1d20', dim: '#4f5460', dimmer: '#8a8f97', + accent: '#3a72c7', accentBg: 'rgba(58,114,199,0.10)', + danger: '#c0463f', warn: '#9c6a14', pass: '#2c8a5a', skip: '#7c8088', + inputBg: '#ffffff', inputBorder: 'rgba(0,0,0,0.18)', + primaryBg: '#3a72c7', primaryFg: '#ffffff', + }, +}; + +function MinimalWindow({ mode = 'light', initial }) { + const t = MinTheme[mode === 'dark' ? 'd' : 'l']; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const isActive = D.phase === 'active'; + const fontUI = '"Segoe UI Variable","Segoe UI",system-ui,-apple-system,sans-serif'; + const fontMono = '"Cascadia Mono","Consolas",ui-monospace,monospace'; + + return ( +
+ {/* title */} + + +
+ {/* Form card */} + + SOCKS5 Proxy +
+ + D.update({ host: e.target.value })} + placeholder="95.165.72.59 или example.com" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={minInput(t, fontUI)}/> + + + D.update({ port: e.target.value.replace(/\D/g,'') })} + placeholder="12334" inputMode="numeric" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={minInput(t, fontUI)}/> + +
+ +
+ { D.update({ auth: v }); if (v) setTimeout(()=>document.getElementById('min-login')?.focus(),30); }}> + Authentication + +
+ + D.update({ login: e.target.value })} placeholder="user" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={minInput(t, fontUI, !D.form.auth)}/> + + + D.update({ password: e.target.value })} placeholder="••••••" + onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={minInput(t, fontUI, !D.form.auth)}/> + +
+ +
+ + + +
+ + {/* Status card */} + + Status + + + +
+ + {/* Action card */} + +
+ + +
+ {isActive && } +
+
+
+ + {/* Logs */} + +
+ ); +} + +function MinTitle({ t }) { + const cell = { width: 46, height: 32, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color: t.dim }; + return ( +
+
+ + Drover-Go + 0.4.2 +
+
+
+
+
{ e.currentTarget.style.background = '#c0463f'; e.currentTarget.style.color = 'white'; }} + onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = t.dim; }}> + +
+
+
+ ); +} +function Card({ t, children, style }) { + return
{children}
; +} +function CardHeader({ t, children }) { + return
{children}
; +} +function MinField({ t, label, children, style }) { + return ; +} +function minInput(t, fontUI, disabled) { + return { + background: t.inputBg, color: disabled ? t.dimmer : t.text, + border: `1px solid ${t.inputBorder}`, borderRadius: 5, padding: '8px 10px', + fontSize: 13, fontFamily: fontUI, outline:'none', width:'100%', boxSizing:'border-box', + }; +} +function MinToggle({ t, checked, onChange, children }) { + return ( + + ); +} +function MinStatus({ t, D, palette, fontMono }) { + if (D.phase === 'idle') { + return ( +
+ + Ready to check +
+ ); + } + return ( +
+
+ {D.phase === 'checking' + ? <> + + Running diagnostics… + + : D.lastSummary?.failed === 0 + ? All checks passed. Ready to start. + : {D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.} +
+ +
+ {D.tests.map(test => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ + {test.label} + + {r?.metric} + + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+ +
+ )} +
+ ); + })} +
+
+ ); +} +function SummaryPill({ t, kind, children }) { + const c = kind === 'ok' ? t.pass : t.warn; + return
{children}
; +} + +function MinStartBtn({ t, D, fontUI }) { + const phase = D.phase; + const allFailed = D.lastSummary && D.lastSummary.failed === D.tests.length; + const checkedOk = phase === 'checked' && !allFailed; + const active = phase === 'active'; + const warning = active && (D.lastSummary?.failed || 0) > 0; + if (active) { + const c = warning ? t.warn : t.pass; + return ( +
+ + Active{warning ? ' · UDP fallback' : ''} +
+ ); + } + return ( + + ); +} +function MinStopBtn({ t, D, fontUI }) { + const enabled = D.phase === 'active'; + return ( + + ); +} +function MinLiveStats({ t, stats, fontMono }) { + const Cell = ({ icon, val, lbl }) => ( +
+ {icon} + {val} + {lbl && {lbl}} +
+ ); + return ( +
+ } val={window.fmtBytes(stats.up)}/> + } val={window.fmtBytes(stats.down)}/> + + + +
+ ); +} +function MinLogs({ t, D, fontMono }) { + return ( +
+ + {D.logsOpen && ( + <> +
+ {['Copy all','Clear','Open log file'].map((l,i) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)} + style={{ maxHeight: 130, overflowY: 'auto', padding: '8px 16px', + fontFamily: fontMono, fontSize: 11, lineHeight: 1.6, color: t.dim, background: t.panel }}> + {D.logs.map((l,i) => ( +
+ {window.fmtTime(l.t)}{' '} + [{l.level}]{' '} + {l.msg} +
+ ))} +
+ + )} +
+ ); +} + +window.MinimalWindow = MinimalWindow; diff --git a/docs/design/v2/drover-shared.jsx b/docs/design/v2/drover-shared.jsx new file mode 100644 index 0000000..7632f64 --- /dev/null +++ b/docs/design/v2/drover-shared.jsx @@ -0,0 +1,308 @@ +// drover-shared.jsx — shared hooks, state machine, types for all Drover-Go variants. +// Each variant imports these via window globals and renders them in its own visual language. + +// ─── Test catalog ────────────────────────────────────────────────────────── +const ALL_TESTS = [ + { id: 'tcp', label: 'TCP reachability', desc: 'TCP-соединение до прокси установлено' }, + { id: 'greet', label: 'SOCKS5 greeting', desc: 'Прокси отвечает по протоколу SOCKS5' }, + { id: 'auth', label: 'SOCKS5 authentication', desc: 'Аутентификация по логину/паролю принята', authOnly: true }, + { id: 'connect', label: 'TCP CONNECT to Discord', desc: 'Прокси проксирует TCP к gateway' }, + { id: 'udp', label: 'UDP ASSOCIATE', desc: 'Прокси выдал UDP-релей' }, + { id: 'stun', label: 'UDP round-trip via STUN', desc: 'UDP-пакеты ходят туда-обратно' }, + { id: 'api', label: 'Discord API reachable', desc: 'HTTPS до discord.com через прокси' }, +]; + +// Pre-baked scenarios so the prototype feels alive. Each entry per test: +// { result: 'passed'|'failed'|'skipped', metric: '12 ms' | 'ok' | …, error?: 'short msg', hint?: 'what to try' } +const SCENARIOS = { + // Default happy path (no auth) + happy: { + tcp: { result: 'passed', metric: '14 ms' }, + greet: { result: 'passed', metric: 'SOCKS5/0x05' }, + connect: { result: 'passed', metric: 'gateway.discord.gg' }, + udp: { result: 'passed', metric: 'relay 95.165.72.59:54321' }, + stun: { result: 'passed', metric: '38 ms RTT' }, + api: { result: 'passed', metric: '204 OK · 89 ms' }, + }, + // With auth + happyAuth: { + tcp: { result: 'passed', metric: '14 ms' }, + greet: { result: 'passed', metric: 'SOCKS5/0x05' }, + auth: { result: 'passed', metric: 'user/pass · ok' }, + connect: { result: 'passed', metric: 'gateway.discord.gg' }, + udp: { result: 'passed', metric: 'relay 95.165.72.59:54321' }, + stun: { result: 'passed', metric: '38 ms RTT' }, + api: { result: 'passed', metric: '204 OK · 89 ms' }, + }, + // UDP fails — common Discord scenario + udpFail: { + tcp: { result: 'passed', metric: '17 ms' }, + greet: { result: 'passed', metric: 'SOCKS5/0x05' }, + connect: { result: 'passed', metric: 'gateway.discord.gg' }, + udp: { result: 'failed', metric: 'X\'07 cmd not supported', + error: 'Прокси не поддерживает UDP ASSOCIATE.', + hint: 'Голос и демонстрация экрана работать не будут. Текст и API — будут. Попробуйте другой SOCKS5-сервер с поддержкой UDP.' }, + stun: { result: 'skipped', metric: 'требует UDP ASSOCIATE' }, + api: { result: 'passed', metric: '204 OK · 92 ms' }, + }, +}; + +function getTests(authEnabled) { + return ALL_TESTS.filter(t => !t.authOnly || authEnabled); +} + +// ─── Drover state hook ───────────────────────────────────────────────────── +// Owns: form values, diagnostic phase, per-test results, drover-active state, +// live stats counter, log buffer. +// phase: 'idle' | 'checking' | 'checked' | 'active' +function useDrover(initial = {}) { + const [form, setForm] = React.useState({ + host: '95.165.72.59', + port: '12334', + auth: false, + login: '', + password: '', + ...initial, + }); + const [phase, setPhase] = React.useState('idle'); + const [results, setResults] = React.useState({}); // testId -> {result, metric, error, hint, expanded} + const [running, setRunning] = React.useState(null); // currently-running test id + const [scenario, setScenario] = React.useState('happy'); // for tweaks + const [stats, setStats] = React.useState({ up: 0, down: 0, tcp: 0, udp: 0, uptimeS: 0 }); + const [logs, setLogs] = React.useState(() => seedLogs()); + const [logsOpen, setLogsOpen] = React.useState(false); + const tests = getTests(form.auth); + const lastSummary = React.useMemo(() => { + if (phase !== 'checked' && phase !== 'active') return null; + const ids = tests.map(t => t.id); + const failed = ids.filter(id => results[id]?.result === 'failed').length; + return { total: ids.length, failed }; + }, [phase, results, tests]); + + // ── actions ──────────────────────────────────────────────────────────── + function update(patch) { setForm(f => ({ ...f, ...patch })); } + + function pushLog(level, msg) { + setLogs(l => [...l.slice(-499), { t: Date.now(), level, msg }]); + } + + async function runCheck() { + if (phase === 'checking') return; + setPhase('checking'); + setResults({}); + setRunning(null); + pushLog('INFO', `connect ${form.host}:${form.port}${form.auth ? ' (auth)' : ''}`); + const list = getTests(form.auth); + const sc = form.auth && scenario === 'happy' ? SCENARIOS.happyAuth : SCENARIOS[scenario]; + for (const t of list) { + setRunning(t.id); + // shorter when scenario is failing past skipped tests + const r = sc[t.id]; + if (r?.result === 'skipped') { + await sleep(120); + } else { + await sleep(380 + Math.random()*220); + } + setResults(prev => ({ ...prev, [t.id]: { ...r, expanded: r?.result === 'failed' } })); + pushLog(r?.result === 'failed' ? 'ERROR' : r?.result === 'skipped' ? 'WARN' : 'INFO', + `${t.label}: ${r?.result}${r?.metric ? ' · ' + r.metric : ''}`); + } + setRunning(null); + setPhase('checked'); + } + + function startProxy() { + if (phase !== 'checked') return; + if (lastSummary?.failed === tests.length) return; + setPhase('active'); + pushLog('INFO', 'drover: bound 127.0.0.1:1080 · routing discord traffic'); + } + + function stopProxy() { + if (phase !== 'active') return; + setPhase('checked'); + setStats({ up: 0, down: 0, tcp: 0, udp: 0, uptimeS: 0 }); + pushLog('INFO', 'drover: stopped'); + } + + // Live stats while active + React.useEffect(() => { + if (phase !== 'active') return; + const id = setInterval(() => { + setStats(s => ({ + up: Math.max(0, s.up + (Math.random()*40000 - 18000)), + down: Math.max(0, s.down + (Math.random()*120000 - 50000)), + tcp: Math.max(2, Math.min(28, s.tcp + (Math.random() < 0.3 ? (Math.random()<.5?-1:1) : 0))), + udp: Math.max(0, Math.min(8, s.udp + (Math.random() < 0.2 ? (Math.random()<.5?-1:1) : 0))), + uptimeS: s.uptimeS + 1, + })); + }, 1000); + // initial values + setStats({ up: 42000, down: 180000, tcp: 6, udp: 2, uptimeS: 0 }); + return () => clearInterval(id); + }, [phase]); + + // Toggle a test's expanded explanation + function toggleExpand(id) { + setResults(r => ({ ...r, [id]: { ...r[id], expanded: !r[id]?.expanded } })); + } + + return { + form, update, + phase, setPhase, + tests, results, running, + scenario, setScenario, + stats, + logs, logsOpen, setLogsOpen, pushLog, clearLogs: () => setLogs([]), + lastSummary, + runCheck, startProxy, stopProxy, + toggleExpand, + }; +} + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +function seedLogs() { + const t = Date.now(); + return [ + { t: t-9200, level: 'INFO', msg: 'drover-go v0.4.2 starting' }, + { t: t-9100, level: 'INFO', msg: 'config: ~/.drover/config.toml' }, + { t: t-9000, level: 'INFO', msg: 'no active session' }, + ]; +} + +function fmtBytes(n) { + if (n < 1024) return n.toFixed(0) + ' B/s'; + if (n < 1024*1024) return (n/1024).toFixed(1) + ' KB/s'; + return (n/1024/1024).toFixed(2) + ' MB/s'; +} +function fmtUptime(s) { + const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), ss = s%60; + if (h) return `${h}h ${m}m`; + if (m) return `${m}m ${ss}s`; + return `${ss}s`; +} +function fmtTime(t) { + const d = new Date(t); + return d.toLocaleTimeString('en-GB', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3,'0'); +} + +// ─── Shared icons (small, original) ──────────────────────────────────────── +// Drover-Go mark: a downward chevron through a ring — "tunneled traffic". +function BrandMark({ size = 16, color = 'currentColor', strokeWidth = 1.6 }) { + const s = size; + return ( + + ); +} + +function IconGear({ size=14, color='currentColor' }) { + return ( + + + + + ); +} +function IconMin({ size=14, color='currentColor' }) { + return ; +} +function IconClose({ size=14, color='currentColor' }) { + return ; +} +function IconChevron({ size=12, color='currentColor', dir='down' }) { + const r = { down: 0, up: 180, left: 90, right: -90 }[dir]; + return + + ; +} +function IconCopy({ size=12, color='currentColor' }) { + return + + + ; +} +function IconArrowUp({ size=10, color='currentColor' }) { + return + + ; +} +function IconArrowDown({ size=10, color='currentColor' }) { + return + + ; +} + +// ─── Test row state icons (per visual variant supplies its own colors) ───── +function StatusDot({ state, palette, size = 12 }) { + // state: 'pending' | 'running' | 'passed' | 'failed' | 'skipped' + const c = palette[state] || palette.pending; + if (state === 'running') { + return ( + + + + + + + ); + } + if (state === 'passed') { + return + + + ; + } + if (state === 'failed') { + return + + + ; + } + if (state === 'skipped') { + return + + + ; + } + // pending + return + + ; +} + +// CSS for the spinner — injected once. +if (typeof document !== 'undefined' && !document.getElementById('drv-shared-css')) { + const s = document.createElement('style'); + s.id = 'drv-shared-css'; + s.textContent = ` + @keyframes drv-spin { to { transform: rotate(360deg); } } + @keyframes drv-pulse { 0%,100% { opacity:1; transform:scale(1);} 50% { opacity:.55; transform:scale(0.7);} } + @keyframes drv-blink { 0%,100% { opacity:1;} 50% { opacity:.35;} } + @keyframes drv-fadein { from { opacity:0; transform:translateY(-2px);} to { opacity:1; transform:none;} } + .drv-fadein { animation: drv-fadein .18s ease-out; } + .drv-pulsedot { animation: drv-pulse 1.4s ease-in-out infinite; } + .drv-shimmer::after { + content:''; position:absolute; inset:0; background: linear-gradient(90deg,transparent,rgba(255,255,255,.25),transparent); + transform:translateX(-100%); animation: drv-shim 1.6s linear infinite; + } + @keyframes drv-shim { to { transform: translateX(100%); } } + /* Hide scrollbars for log panes inside artboards */ + .drv-log::-webkit-scrollbar { width:6px; } + .drv-log::-webkit-scrollbar-thumb { background: rgba(127,127,127,.35); border-radius: 3px; } + `; + document.head.appendChild(s); +} + +// Expose globals +Object.assign(window, { + useDrover, getTests, ALL_TESTS, SCENARIOS, + fmtBytes, fmtUptime, fmtTime, + BrandMark, StatusDot, + IconGear, IconMin, IconClose, IconChevron, IconCopy, IconArrowUp, IconArrowDown, +}); diff --git a/docs/design/v2/drover-sketches.jsx b/docs/design/v2/drover-sketches.jsx new file mode 100644 index 0000000..e0721b1 --- /dev/null +++ b/docs/design/v2/drover-sketches.jsx @@ -0,0 +1,903 @@ +// Drover-Go · style sketches v2 — "feels like real software" set. +// Each artboard explores a DIFFERENT software UI paradigm + element vocabulary, +// not just a different palette. 480×640 fixed Win11 window unless noted. + +const W = 480; +const H = 640; + +const Win = ({ children, bg = '#fff', radius = 8, ring = 'rgba(0,0,0,.18)' }) => ( +
{children}
+); + +const Mark = ({ size = 14, color = '#0c8c7a' }) => ( + + + + + +); + +const TitleButtons = ({ color = '#666' }) => ( +
+ + + × +
+); + +// ═════════════════════════════════════════════════════════════════════════ +// 1 · Fluent / Win11 native — the safe baseline +// ═════════════════════════════════════════════════════════════════════════ +const SketchFluent = () => { + const f = "'Segoe UI Variable','Segoe UI',system-ui,sans-serif"; + return ( + +
+ + Drover-Go + 0.4.2 + +
+
+
+
SOCKS5 Proxy
+
+ + +
+ + +
+
+
Status
+
+ + Running diagnostics… +
+ {[ + ['✓', 'TCP reachability', '12 ms', '#107c10'], + ['✓', 'SOCKS5 greeting', 'ok', '#107c10'], + ['◐', 'TCP CONNECT to Discord', '…', '#0067c0'], + ['·', 'UDP ASSOCIATE', '', '#bbb'], + ['·', 'UDP round-trip via STUN', '', '#bbb'], + ['·', 'Discord API reachable', '', '#bbb'], + ].map(([ic, name, val, c], i) => ( +
+ {ic} + {name} + {val} +
+ ))} +
+
+ + +
+
+ +
+ ); +}; +const FluentField = ({ label, value, flex, width }) => ( + +); + +// ═════════════════════════════════════════════════════════════════════════ +// 2 · Sidebar nav (VS Code / Postman / Tower) — left rail with sections +// ═════════════════════════════════════════════════════════════════════════ +const SketchSidebar = () => { + const f = "'Inter', system-ui, sans-serif"; + const m = "'JetBrains Mono', monospace"; + return ( + +
+ + Drover-Go + — proxy: 95.165.72.59:12334 + +
+
+ {/* sidebar */} +
+ {[['◉', 'Proxy', true], ['◇', 'Diagnose'], ['≋', 'Traffic'], ['≣', 'Logs'], ['⚙', 'Settings']].map(([ic, l, on], i) => ( +
+ {ic} + {l} +
+ ))} +
+
+ {/* breadcrumb / tabs */} +
+
● Proxy
+
diagnostic.run
+
+
+
+
Endpoint
+
+
SOCKS5
+
95.165.72.59
+
:12334
+ +
+
+
+
Diagnostic results
+
+ {[ + ['✓', 'tcp_reachability', '12 ms', '#21d07a'], + ['✓', 'socks5_greeting', 'ok', '#21d07a'], + ['✓', 'socks5_authentication', 'ok', '#21d07a'], + ['✓', 'tcp_connect_discord', '38 ms', '#21d07a'], + ['✗', 'udp_associate', 'EPERM: blocked', '#ff5e5e'], + ['—', 'udp_stun_rtt', 'skipped', '#666'], + ['—', 'discord_api', 'skipped', '#666'], + ].map(([ic, n, v, c], i) => ( +
+ {ic} + {n} + {v} +
+ ))} +
+
+ ⚠ 1 of 7 checks failed. UDP voice/screenshare won't work. +
+
+
+ {/* status bar */} +
+ ● 6/7 + idle + UTF-8 · LF · drover.toml +
+
+
+
+ ); +}; + +// ═════════════════════════════════════════════════════════════════════════ +// 3 · Step wizard / Tab progression — DigiCert / Tunnelblick connect-flow +// ═════════════════════════════════════════════════════════════════════════ +const SketchWizard = () => { + const f = "'Inter', system-ui, sans-serif"; + return ( + +
+ + Drover-Go · Setup + +
+
+ {/* stepper */} +
+ {[['1', 'Configure', true], ['2', 'Verify', true], ['3', 'Connect', false]].map(([n, l, done], i) => ( + +
+ + {done && i !== 1 ? '✓' : n} + + {l} +
+ {i < 2 &&
} + + ))} +
+
+
+

Verifying your proxy

+
We're running 7 checks against 95.165.72.59:12334 to make sure Discord will work end-to-end.
+
+ {/* progress + ring */} +
+ + + + 4/7 + +
+
Testing UDP relay…
+
This usually takes 5–15 seconds.
+
+
+
+ {[ + ['✓', 'TCP reachability', '12 ms', '#21a655'], + ['✓', 'SOCKS5 greeting', 'ok', '#21a655'], + ['✓', 'SOCKS5 authentication', 'ok', '#21a655'], + ['✓', 'TCP CONNECT to Discord', '38 ms', '#21a655'], + ['◐', 'UDP ASSOCIATE', 'running…', '#1e6fd9'], + ['·', 'UDP round-trip via STUN', '', '#bbb'], + ['·', 'Discord API reachable', '', '#bbb'], + ].map(([ic, n, v, c], i) => ( +
+ {ic} + {n} + {v} +
+ ))} +
+
+
+ + +
+
+ + ); +}; + +// ═════════════════════════════════════════════════════════════════════════ +// 4 · Network monitor — gauges, sparklines, multi-pane (Wireshark / Activity) +// ═════════════════════════════════════════════════════════════════════════ +const SketchMonitor = () => { + const f = "'Inter', system-ui, sans-serif"; + const m = "'JetBrains Mono', monospace"; + return ( + +
+ + Drover-Go + ● ACTIVE 4m 12s + +
+
+ {/* compact endpoint */} +
+ + 95.165.72.59:12334 + SOCKS5 · auth + +
+ {/* meters */} +
+ + +
+ {/* sparkline panel */} +
+
+ Throughput · last 60s + peak 3.4 MB/s +
+ + + + +
+ {/* connections table */} +
+
+ Proto + Endpoint + Up + Down +
+ {[ + ['UDP', 'voice-eu-rtc-1.discord', '142 KB/s', '2.4 MB/s', '#1e6fd9'], + ['TCP', 'gateway.discord:443', '8 KB/s', '12 KB/s', '#0c8c7a'], + ['TCP', 'cdn.discordapp:443', '0', '380 KB/s', '#0c8c7a'], + ['TCP', 'media.discordapp:443', '0', '8 KB/s', '#0c8c7a'], + ].map((r, i) => ( +
+ {r[0]} + {r[1]} + {r[2]} + {r[3]} +
+ ))} +
+
+ + +
+
+
+ ); +}; +const Gauge = ({ label, v, unit, pct, c }) => ( +
+
+ {label} +
+
+ {v} + {unit} +
+
+
+
+
+); + +// ═════════════════════════════════════════════════════════════════════════ +// 5 · Inspector / Properties — devtools-style key:value list +// ═════════════════════════════════════════════════════════════════════════ +const SketchInspector = () => { + const f = "'Inter', system-ui, sans-serif"; + const m = "'JetBrains Mono', monospace"; + return ( + +
+ + Drover-Go + 0.4.2 + +
+ {/* tabs */} +
+ {['Inspector', 'Network', 'Console', 'Settings'].map((t, i) => ( +
{t}
+ ))} +
+
+ + + } editable /> + + + + + + + + + + + + + + + + + + + +
+
+ + + 14 props · saved +
+
+ ); +}; +const Group = ({ title, expanded, children }) => ( +
+
+ {expanded ? '▼' : '▶'}{title} +
+
{children}
+
+); +const KV = ({ k, v, t, color, editable }) => ( +
+ {k} + {v} + {t && {t}} +
+); + +// ═════════════════════════════════════════════════════════════════════════ +// 6 · Command palette + form — Raycast / Linear / cmd+k feel +// ═════════════════════════════════════════════════════════════════════════ +const SketchPalette = () => { + const f = "'Inter', system-ui, sans-serif"; + const m = "'JetBrains Mono', monospace"; + return ( + +
+ + Drover-Go + +
+
+ {/* command bar */} +
+ + + esc +
+ {/* suggestion */} +
+
+ + Check connection + +
+
+ + Start proxying + ⇧↵ +
+
+ {/* Form below — proxy quick-edit */} +
+
Proxy
+
+ + +
+
Last run · 7/7 passed
+ {[ + ['tcp_reachability', '12 ms'], + ['socks5_greeting', 'ok'], + ['udp_associate', 'relay 1.2.3.4'], + ['udp_stun_rtt', '24 ms'], + ['discord_api', '200'], + ].map(([n, v], i) => ( +
+ + {n} + {v} +
+ ))} +
+
+ run + ⇧↵ start + idle · ready +
+
+
+ ); +}; +const PalField = ({ label, value, flex, width }) => ( + +); + +// ═════════════════════════════════════════════════════════════════════════ +// 7 · Big toggle / Hero — TunnelBear / NordVPN big-button connect +// ═════════════════════════════════════════════════════════════════════════ +const SketchHero = () => { + const f = "'Inter', system-ui, sans-serif"; + const m = "'JetBrains Mono', monospace"; + return ( + +
+ + Drover-Go + ● connected + +
+
+ {/* Big circular button */} +
+
+ + ACTIVE +
+
+
Discord is protected
+
via 95.165.72.59 · 4m 12s
+
+ {/* Stats row */} +
+ {[ + ['↑', '142', 'KB/s'], + ['↓', '2.8', 'MB/s'], + ['◇', '14', 'tcp'], + ['◈', '3', 'udp'], + ].map(([k, v, u], i) => ( +
+
{k}
+
{v}
+
{u}
+
+ ))} +
+ +
+ Logs (3 new) +
+
+ + + ); +}; + +// ═════════════════════════════════════════════════════════════════════════ +// 8 · Toolbar + ribbon — old-school Office / Audacity feel +// ═════════════════════════════════════════════════════════════════════════ +const SketchToolbar = () => { + const f = "'Segoe UI', system-ui, sans-serif"; + const m = "'Consolas', 'JetBrains Mono', monospace"; + return ( + +
+ + Drover-Go + — [Connected] + +
+ {/* menu bar */} +
+ {['File', 'Edit', 'Run', 'View', 'Tools', 'Help'].map(m => ( + {m} + ))} +
+ {/* ribbon */} +
+ + + +
+ + +
+ +
+
+
+
+ Host: +
95.165.72.59
+ Port: +
12334
+
+
+ + Authentication + User: +
alice
+
+
+
+
+ {[ + ['✓', 'TCP reachability', 'PASS · 12 ms'], + ['✓', 'SOCKS5 greeting', 'PASS · ok'], + ['✓', 'SOCKS5 authentication', 'PASS · ok'], + ['✓', 'TCP CONNECT to Discord', 'PASS · 38 ms'], + ['✓', 'UDP ASSOCIATE', 'PASS · relay 1.2.3.4'], + ['✓', 'UDP round-trip via STUN', 'PASS · 24 ms'], + ['✓', 'Discord API reachable', 'PASS · 200'], + ].map((r, i) => ( +
+ {r[0]} + {r[1]} + {r[2]} +
+ ))} +
+
+
+ {/* status bar */} +
+ Ready + ● 7/7 + ↑ 142 KB/s ↓ 2.8 MB/s + uptime 4m 12s +
+ + ); +}; +const Tool = ({ icon, label, primary, disabled }) => ( +
+ {icon} + {label} +
+); +const Fieldset = ({ label, children, flex }) => ( +
+ {label} + {children} +
+); + +// ═════════════════════════════════════════════════════════════════════════ +// 9 · Modern dev tool — Studio (Linear-ish, restrained dark) +// ═════════════════════════════════════════════════════════════════════════ +const SketchStudio = () => { + const f = "'Inter', sans-serif"; + return ( + +
+ + Drover-Go + 0.4.2 + +
+
+
+
SOCKS5 Proxy
+
+ + +
+ + +
+
+
+ Status ● All systems +
+
+ {[ + ['TCP reachability', '12 ms'], + ['SOCKS5 greeting', 'ok'], + ['SOCKS5 authentication', 'ok'], + ['TCP CONNECT to Discord', '38 ms'], + ['UDP ASSOCIATE', 'relay 1.2.3.4'], + ['UDP round-trip via STUN', '24 ms'], + ['Discord API reachable', '200'], + ].map(([n, v], i) => ( +
+ + {n} + {v} +
+ ))} +
+
+
+ + +
+
+
+ ); +}; +const StField = ({ label, value, flex, width }) => ( + +); + +// ═════════════════════════════════════════════════════════════════════════ +// 10 · Pipeline / DAG — visualize the test chain as a flow +// ═════════════════════════════════════════════════════════════════════════ +const SketchPipeline = () => { + const f = "'Inter', system-ui, sans-serif"; + const m = "'JetBrains Mono', monospace"; + const Node = ({ x, y, label, status, sub }) => { + const c = status === 'pass' ? '#21a655' : status === 'fail' ? '#e85a4f' : status === 'run' ? '#1e6fd9' : '#aaa'; + const bg = status === 'pass' ? '#e8f7ee' : status === 'fail' ? '#fdebe9' : status === 'run' ? '#eaf1fb' : '#f4f4f4'; + return ( + + + + {label} + {sub} + + ); + }; + const Arrow = ({ from, to, dashed }) => ( + + ); + return ( + +
+ + Drover-Go + · pipeline view + +
+
+ {/* compact endpoint */} +
+ SOCKS5 + + + +
+ {/* DAG */} +
+ + + + + + + + + + + + + + + + + + + +
+
+ ● 4/7 running + + + + +
+
+
+ ); +}; + +// ═════════════════════════════════════════════════════════════════════════ +// 11 · Bauhaus — geometric blocks, primary colors, asymmetric grid +// ═════════════════════════════════════════════════════════════════════════ +const SketchBauhaus = () => { + const f = "'Space Grotesk', sans-serif"; + return ( + +
+ + + + DROVER-GO + v0.4.2 · ⚙ · — · × +
+
+
+
FORM 01
+
SOCKS5 PROXY
+
+ + +
+
+ + +
+
STATUS · 7/7
+
ALL CHECKS PASSED
+
+ {[ + ['TCP reach', '12ms'], + ['SOCKS5 greet', 'ok'], + ['SOCKS5 auth', 'ok'], + ['CONNECT', '38ms'], + ['UDP assoc', 'ok'], + ['UDP STUN', '24ms'], + ['API', '200'], + ].map(([n, v], i) => ( +
+ + {n} + {v} +
+ ))} +
+
+
+
+ ); +}; +const BaField = ({ label, value, flex, width, dark }) => ( + +); + +// ═════════════════════════════════════════════════════════════════════════ +// 12 · Brutalist — hard borders, mono, acid lime +// ═════════════════════════════════════════════════════════════════════════ +const SketchBrutalist = () => { + const m = "'Geist Mono', 'JetBrains Mono', monospace"; + return ( + +
+ ▶ DROVER-GO + v0.4.2 +
+ [⚙][—][X] +
+
+
+
+
// SOCKS5 PROXY
+
+ + +
+
[X] AUTHENTICATION
+
+ + +
+ +
+
+
// STATUS · 7/7 OK
+ {[ + ['[OK]', 'TCP_REACHABILITY', '12ms'], + ['[OK]', 'SOCKS5_GREETING', 'ok'], + ['[OK]', 'SOCKS5_AUTH', 'ok'], + ['[OK]', 'CONNECT_DISCORD', '38ms'], + ['[OK]', 'UDP_ASSOCIATE', 'relay 1.2.3.4'], + ['[OK]', 'UDP_STUN_RTT', '24ms'], + ['[OK]', 'DISCORD_API', '200'], + ].map(([s, n, v], i) => ( +
+ {s} + {n} + {v} +
+ ))} +
+
+ + +
+
+
+ ); +}; +const BrField = ({ label, value, flex, width }) => ( + +); + +// ═════════════════════════════════════════════════════════════════════════ +// SketchesApp +// ═════════════════════════════════════════════════════════════════════════ + +const SKETCHES = [ + { id: 'fluent', label: '01 · Fluent / Win11 native', el: }, + { id: 'sidebar', label: '02 · IDE sidebar (VS Code-ish)', el: }, + { id: 'wizard', label: '03 · Wizard / stepper', el: }, + { id: 'monitor', label: '04 · Network monitor (gauges)', el: }, + { id: 'inspector', label: '05 · Inspector / properties', el: }, + { id: 'palette', label: '06 · Command palette (cmd+k)', el: }, + { id: 'hero', label: '07 · Big toggle (VPN-style)', el: }, + { id: 'toolbar', label: '08 · Toolbar / ribbon (classic)', el: }, + { id: 'studio', label: '09 · Studio (Linear-ish)', el: }, + { id: 'pipeline', label: '10 · Pipeline / DAG view', el: }, + { id: 'bauhaus', label: '11 · Bauhaus blocks', el: }, + { id: 'brutalist', label: '12 · Brutalist (mono+lime)', el: }, +]; + +const SketchesApp = () => ( + + +
+
What changed
+

Removed the "marketing" stylings (ticket, synthwave, soft pastel, editorial serif). Replaced with software-native paradigms — each card explores a different way to organize the same UI, not just a different paint job:

+
    +
  • Layout patterns: sidebar IDE · wizard/stepper · pipeline/DAG · ribbon · palette+form · big-toggle hero
  • +
  • Element vocab: gauges + sparklines · key/value inspector · fieldsets · tabs+breadcrumbs · status bar · toolbar buttons
  • +
  • Aesthetic: Fluent (Win11) · classic Office · Linear/Studio · brutalist · Bauhaus blocks
  • +
+

Tell me which paradigm/aesthetic combo to take forward.

+
+
+ + {SKETCHES.map(s => ( + + {s.el} + + ))} + +
+); + +// Expose individual sketches so other files (e.g. shortlist) can render them. +Object.assign(window, { + SketchFluent, SketchSidebar, SketchWizard, SketchMonitor, + SketchInspector, SketchPalette, SketchHero, SketchToolbar, + SketchStudio, SketchPipeline, SketchBauhaus, SketchBrutalist, +}); diff --git a/docs/design/v2/drover-studio.jsx b/docs/design/v2/drover-studio.jsx new file mode 100644 index 0000000..d09db9a --- /dev/null +++ b/docs/design/v2/drover-studio.jsx @@ -0,0 +1,309 @@ +// drover-studio.jsx — Studio: modern, calm, Linear-ish. Subtle blue accent. + +const STD = { + bg: '#0e0f12', chrome: '#0a0b0d', panel: '#15171b', panel2: '#1a1c21', + border: '#22252b', borderSoft: '#1a1c21', + text: '#e7e9ee', dim: '#9095a0', dimmer: '#5e6470', + accent: '#6c8aff', accentSoft: 'rgba(108,138,255,0.14)', + danger: '#ff7d70', danSoft: 'rgba(255,125,112,0.12)', + warn: '#f5b13d', warnSoft: 'rgba(245,177,61,0.12)', + pass: '#6cd391', passSoft: 'rgba(108,211,145,0.12)', + skip: '#7e848e', + inputBg: '#0e0f12', + primaryFg: '#0c1226', +}; + +function StudioWindow({ initial }) { + const t = STD; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontUI = '"Inter","Segoe UI",system-ui,-apple-system,sans-serif'; + const fontMono = '"JetBrains Mono",ui-monospace,monospace'; + const isActive = D.phase === 'active'; + + return ( +
+ +
+ + +
+ + D.update({ host: v })} onSubmit={D.runCheck} + placeholder="95.165.72.59 или example.com"/> + + + D.update({ port: v.replace(/\D/g,'') })} onSubmit={D.runCheck} + placeholder="12334" inputMode="numeric"/> + +
+ +
+ { D.update({ auth: v }); if (v) setTimeout(()=>document.getElementById('std-login')?.focus(),30); }}> + Authentication + +
+ + D.update({ login: v })} onSubmit={D.runCheck} placeholder="user" disabled={!D.form.auth}/> + + + D.update({ password: v })} onSubmit={D.runCheck} placeholder="••••••" + disabled={!D.form.auth}/> + +
+ +
+ + + +
+ + {D.phase === 'idle' + ?
+ + Ready to check +
+ : } +
+ +
+ +
+ + +
+ {isActive && ( +
+ } val={window.fmtBytes(D.stats.up)} fontMono={fontMono} t={t}/> + } val={window.fmtBytes(D.stats.down)} fontMono={fontMono} t={t}/> + + + +
+ )} +
+
+
+ +
+ ); +} + +function StdTitle({ t }) { + const cell = { width: 44, height: 32, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color: t.dim }; + return ( +
+
+ + Drover-Go + 0.4.2 +
+
+
+
+
+
+
+ ); +} + +function StdSection({ t, title, right, children }) { + return ( +
+ {(title || right) && ( +
+ {title &&
{title}
} + {right &&
{right}
} +
+ )} +
+ {children} +
+
+ ); +} +function StdField({ t, label, children, style }) { + return ; +} +function StdInput({ t, fontUI, value, onChange, type, placeholder, onSubmit, disabled, id, inputMode }) { + return onChange(e.target.value)} placeholder={placeholder} + onKeyDown={e => e.key === 'Enter' && onSubmit?.()} + style={{ + background: t.inputBg, color: disabled ? t.dimmer : t.text, + border:`1px solid ${t.border}`, borderRadius: 5, padding:'8px 10px', + fontSize: 13, fontFamily: fontUI, outline:'none', width:'100%', boxSizing:'border-box', + }}/>; +} +function StdCheck({ t, checked, onChange, children }) { + return ( + + ); +} +function StdStatus({ t, D, palette, fontMono }) { + const allOk = D.lastSummary?.failed === 0; + return ( +
+ {D.phase === 'checking' + ?
+ + Running diagnostics… +
+ :
+ {allOk ? 'All checks passed. Ready to start.' + : `${D.lastSummary?.failed} of ${D.tests.length} checks failed. Some features won't work.`} +
} +
+ {D.tests.map(test => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ + {test.label} + {r?.metric} + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+
+ )} +
+ ); + })} +
+
+ ); +} +function StdStartBtn({ t, D, fontUI }) { + const allFailed = D.lastSummary && D.lastSummary.failed === D.tests.length; + const ok = D.phase === 'checked' && !allFailed; + const active = D.phase === 'active'; + const warning = active && (D.lastSummary?.failed || 0) > 0; + if (active) { + const c = warning ? t.warn : t.pass; + return ( +
+ + Active{warning ? ' · UDP fallback' : ''} +
+ ); + } + return ; +} +function StdStopBtn({ t, D, fontUI }) { + const enabled = D.phase === 'active'; + return ; +} +function StdStat({ icon, val, lbl, fontMono, t }) { + return
+ {icon}{val} + {lbl && {lbl}} +
; +} +function StdLogs({ t, D, fontMono }) { + return ( +
+ + {D.logsOpen && ( + <> +
+ {[['Copy all', () => navigator.clipboard?.writeText(D.logs.map(x=>`[${x.level}] ${x.msg}`).join('\n'))], + ['Clear', D.clearLogs], ['Open log file', null]].map(([l, fn]) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)} + style={{ maxHeight: 130, overflowY:'auto', padding:'8px 16px', + fontFamily: fontMono, fontSize: 11, lineHeight: 1.6, color: t.dim, background: t.panel }}> + {D.logs.map((l,i) => ( +
+ {window.fmtTime(l.t)}{' '} + [{l.level}]{' '} + {l.msg} +
+ ))} +
+ + )} +
+ ); +} + +window.StudioWindow = StudioWindow; diff --git a/docs/design/v2/drover-wizard-live.jsx b/docs/design/v2/drover-wizard-live.jsx new file mode 100644 index 0000000..5a210af --- /dev/null +++ b/docs/design/v2/drover-wizard-live.jsx @@ -0,0 +1,455 @@ +// drover-wizard-live.jsx — Wizard / stepper variant. +// 3 steps: Configure → Verify → Connect. State derives from D.phase. +// Step 1 = idle (form). Step 2 = checking/checked (diagnostics). Step 3 = active (running proxy). + +const WIZARD_THEME = { + l: { + bg: '#fafafa', + chrome: '#ffffff', + panel: '#ffffff', + panelAlt: '#f6f7f9', + border: '#ececec', + borderHard:'#dcdcdc', + text: '#1a1a1a', + dim: '#666666', + dimmer: '#9aa0a6', + accent: '#1e6fd9', + accentSoft:'#eef4fc', + danger: '#c0463f', + warn: '#a8731e', + pass: '#21a655', + skip: '#888888', + }, + d: { + bg: '#0f1115', + chrome: '#171a20', + panel: '#1a1d23', + panelAlt: '#13151a', + border: '#2a2d33', + borderHard:'#3a3d45', + text: '#e6e8ec', + dim: '#9aa0a6', + dimmer: '#5d6168', + accent: '#5b9bf0', + accentSoft:'#1a253a', + danger: '#e57373', + warn: '#d9a155', + pass: '#5cba8b', + skip: '#7c8088', + }, +}; + +function WizardWindow({ mode = 'light', initial }) { + const themeKey = mode === 'dark' ? 'd' : 'l'; + const t = WIZARD_THEME[themeKey]; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontUI = "'Inter','Segoe UI',system-ui,sans-serif"; + const fontMono = "'JetBrains Mono','SF Mono',ui-monospace,Consolas,monospace"; + + // Determine current step from phase + const phase = D.phase; + const step = phase === 'idle' ? 1 + : (phase === 'checking' || phase === 'checked') ? 2 + : 3; // active + + return ( +
+ + + {/* Stepper */} +
+ {[[1, 'Configure'], [2, 'Verify'], [3, 'Connect']].map(([n, label], i) => { + const done = step > n; + const current = step === n; + const dim = step < n; + return ( + +
+ {done ? '✓' : n} + {label} +
+ {i < 2 &&
n ? t.accent : t.borderHard, + margin: '0 10px', transition: 'background .2s', + }} />} + + ); + })} +
+ + {/* Step content */} +
+ {step === 1 && } + {step === 2 && } + {step === 3 && } +
+ + {/* Footer */} +
+ { + if (step === 2) D.stopProxy?.(), D.setPhase('idle'); + if (step === 3) D.stopProxy(); + }} disabled={step === 1}> + {step === 3 ? 'Disconnect' : 'Back'} + + { + if (step === 1) D.runCheck(); + else if (step === 2) D.startProxy(); + }} + disabled={ + (step === 1 && (D.phase === 'checking')) || + (step === 2 && (D.phase === 'checking' || D.lastSummary?.failed === D.tests.length)) || + (step === 3) + }> + {step === 1 && 'Verify →'} + {step === 2 && (D.phase === 'checking' ? 'Verifying…' : 'Connect →')} + {step === 3 && 'Connected'} + +
+
+ ); +} + +function WizardTitleBar({ t }) { + return ( +
+ + Drover-Go · Setup +
+ + + +
+
+ ); +} +function WizTitleBtn({ children, t, hoverBg, hoverFg }) { + const [hover, setHover] = React.useState(false); + return ( +
setHover(true)} onMouseLeave={() => setHover(false)} + style={{ width: 38, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center', + cursor: 'pointer', + background: hover ? (hoverBg || 'rgba(127,127,127,.1)') : 'transparent', + color: hover && hoverFg ? hoverFg : 'inherit', + borderRadius: 4, + }}>{children}
+ ); +} + +// ─── Step 1: Configure ─────────────────────────────────────────────────── +function WizConfigure({ t, D, fontMono }) { + const inputStyle = (disabled) => ({ + height: 36, background: t.panel, color: disabled ? t.dimmer : t.text, + border: `1px solid ${t.borderHard}`, borderRadius: 6, padding: '0 12px', + fontFamily: fontMono, fontSize: 13, outline: 'none', width: '100%', boxSizing: 'border-box', + transition: 'border-color .12s, box-shadow .12s', + }); + const onFocus = (e) => { e.target.style.borderColor = t.accent; e.target.style.boxShadow = `0 0 0 3px ${t.accentSoft}`; }; + const onBlur = (e) => { e.target.style.borderColor = t.borderHard; e.target.style.boxShadow = 'none'; }; + return ( + <> +
+

Configure your proxy

+
+ Введите адрес SOCKS5-сервера. На следующем шаге мы проверим, что Discord будет работать через него. +
+
+
+
+ + +
+ + + +
+ + +
+
+
+ i + Нажмите Verify →, чтобы продолжить. +
+ + ); +} + +// ─── Step 2: Verify ─────────────────────────────────────────────────── +function WizVerify({ t, D, fontMono, palette, themeKey }) { + const phase = D.phase; + const total = D.tests.length; + const completed = Object.keys(D.results).length; + const failed = D.lastSummary?.failed ?? 0; + const ringFrac = phase === 'checked' ? 1 : completed / total; + const ringDash = 163.4; + return ( + <> +
+

+ {phase === 'checking' ? 'Verifying your proxy' : (failed === 0 ? 'All checks passed' : 'Some checks failed')} +

+
+ {phase === 'checking' + ? <>Запускаем 7 проверок против {D.form.host}:{D.form.port} + : (failed === 0 + ? <>Прокси работает. Discord — голос, чат и демонстрация — будет работать через него. + : <>{failed} из {total} проверок не прошли. Часть функций работать не будет.)} +
+
+ + {/* progress + ring */} +
+ + + 0 ? t.danger : phase === 'checked' ? t.pass : t.accent} + strokeWidth="6" strokeLinecap="round" + strokeDasharray={ringDash} + strokeDashoffset={ringDash * (1 - ringFrac)} + transform="rotate(-90 32 32)" + style={{ transition: 'stroke-dashoffset .35s, stroke .2s' }} /> + + {phase === 'checked' ? `${total - failed}/${total}` : `${completed}/${total}`} + + +
+
+ {phase === 'checking' ? <>Testing {D.tests.find(x => x.id === D.running)?.label || '…'} : (failed === 0 ? 'Готово к подключению' : 'Завершено с ошибками')} +
+
+ {phase === 'checking' ? 'This usually takes 5–15 seconds.' : 'См. отчёт ниже.'} +
+
+
+ + {/* test list */} +
+ {D.tests.map((test) => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ + {test.label} + + {r?.metric || (state === 'running' ? 'running…' : '')} + + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+ +
+ )} +
+ ); + })} +
+ + ); +} + +// ─── Step 3: Connect (active) ─────────────────────────────────────────── +function WizConnect({ t, D, fontMono }) { + const stats = D.stats; + const failed = D.lastSummary?.failed ?? 0; + return ( + <> +
+

+ 0 ? t.warn : t.pass, + display: 'inline-block', + }} /> + {failed > 0 ? 'Connected · UDP fallback' : 'Connected'} +

+
+ Discord routes through {D.form.host}:{D.form.port} + {' · '}{window.fmtUptime(stats.uptimeS)} +
+
+ + {/* stats grid */} +
+ + + + 0 ? t.warn : t.pass} /> +
+ + {/* logs panel inline */} +
+
+ RECENT LOG + + +
+
el && (el.scrollTop = el.scrollHeight)}> + {D.logs.slice(-30).map((l, i) => ( +
+ {window.fmtTime(l.t)} + {' '} + [{l.level}] + {' '} + {l.msg} +
+ ))} +
+
+ + ); +} + +function WizStat({ t, fontMono, label, value, accent }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function WizardPrimaryBtn({ t, onClick, disabled, children }) { + const [hover, setHover] = React.useState(false); + return ( + + ); +} +function WizardSecondaryBtn({ t, onClick, disabled, children }) { + const [hover, setHover] = React.useState(false); + return ( + + ); +} + +window.WizardWindow = WizardWindow; diff --git a/docs/design/v2/drover-workshop.jsx b/docs/design/v2/drover-workshop.jsx new file mode 100644 index 0000000..8a0b5c4 --- /dev/null +++ b/docs/design/v2/drover-workshop.jsx @@ -0,0 +1,322 @@ +// drover-workshop.jsx — Workshop: industrial slate + amber accent. Subtle texture. + +const WSH = { + bg: '#181a1d', chrome: '#121417', panel: '#1f2226', panel2: '#23272c', + border: '#2c3036', borderSoft: '#222529', + text: '#e5e3df', dim: '#9a958c', dimmer: '#5e5b56', + accent: '#e89a3c', accentSoft: 'rgba(232,154,60,0.15)', + danger: '#d56654', warn: '#d4a248', pass: '#7fb37b', skip: '#7c7872', + inputBg: '#141619', + primaryFg: '#1a120a', +}; + +function WorkshopWindow({ initial }) { + const t = WSH; + const D = window.useDrover(initial); + const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; + const fontUI = '"IBM Plex Sans","Inter",system-ui,sans-serif'; + const fontMono = '"IBM Plex Mono","JetBrains Mono",ui-monospace,monospace'; + const isActive = D.phase === 'active'; + + return ( +
+ {/* subtle grain */} +
+ + {/* title — workshop label tag */} +
+
+
+ Drover-Go + v0.4.2 +
+
+ {[, , ].map((ic,i) => ( +
{ic}
+ ))} +
+
+ +
+ {/* form */} + SOCKS5 PROXY +
+
+ + D.update({ host: v })} + placeholder="95.165.72.59 / example.com" onSubmit={D.runCheck} fontUI={fontUI}/> + + + D.update({ port: v.replace(/\D/g,'') })} + placeholder="12334" inputMode="numeric" onSubmit={D.runCheck} fontUI={fontUI}/> + +
+
+ { D.update({ auth: v }); if (v) setTimeout(()=>document.getElementById('wsh-login')?.focus(),30); }}> + AUTHENTICATION + +
+ + D.update({ login: v })} placeholder="user" onSubmit={D.runCheck} fontUI={fontUI}/> + + + D.update({ password: v })} placeholder="••••••" onSubmit={D.runCheck} fontUI={fontUI}/> + +
+
+ +
+ +
+ STATUS +
+ {D.phase === 'idle' + ?
+ + Ready to check +
+ : } +
+ +
+
+
+ + +
+ {isActive && ( +
+ + + + + +
+ )} +
+
+
+ +
+ ); +} + +function WshHead({ t, children, right }) { + return ( +
+
+ + {children} +
+ {right &&
{right}
} +
+ ); +} +function WshField({ t, label, children, style }) { + return ; +} +function WshInput({ t, value, onChange, type, placeholder, onSubmit, disabled, id, fontUI, inputMode }) { + return onChange(e.target.value)} placeholder={placeholder} + onKeyDown={e => e.key === 'Enter' && onSubmit?.()} + style={{ + background: t.inputBg, color: disabled ? t.dimmer : t.text, + border:`1px solid ${t.border}`, borderRadius: 1, padding:'8px 10px', + fontSize: 13, fontFamily: fontUI, outline:'none', width:'100%', boxSizing:'border-box', + }}/>; +} +function WshSwitch({ t, checked, onChange, children }) { + return ( + + ); +} +function WshStatus({ t, D, palette, fontMono }) { + return ( +
+ {D.phase === 'checking' + ?
+ + Running diagnostics… +
+ :
+ {D.lastSummary?.failed === 0 + ? 'All checks passed. Ready to start.' + : `${D.lastSummary?.failed} of ${D.tests.length} checks failed. Some features won't work.`} +
} +
+ {D.tests.map(test => { + const r = D.results[test.id]; + const state = r?.result || (D.running === test.id ? 'running' : 'pending'); + return ( +
+
+ + {test.label} + {r?.metric} + {r?.result === 'failed' && ( + + )} +
+ {r?.result === 'failed' && r.expanded && ( +
+
{r.error}
+
{r.hint}
+
+ )} +
+ ); + })} +
+
+ ); +} +function WshStartBtn({ t, D, fontUI }) { + const allFailed = D.lastSummary && D.lastSummary.failed === D.tests.length; + const ok = D.phase === 'checked' && !allFailed; + const active = D.phase === 'active'; + const warning = active && (D.lastSummary?.failed||0) > 0; + if (active) { + const c = warning ? t.warn : t.pass; + return ( +
+ + Active{warning ? ' · UDP warn' : ''} +
+ ); + } + return ; +} +function WshStopBtn({ t, D, fontUI }) { + const enabled = D.phase === 'active'; + return ; +} +function Stat({ val, lbl, t, fontMono, hl }) { + return
+ {lbl} + {val} +
; +} +function WshLogs({ t, D, fontMono }) { + return ( +
+ + {D.logsOpen && ( + <> +
+ {[['copy', () => navigator.clipboard?.writeText(D.logs.map(x=>`[${x.level}] ${x.msg}`).join('\n'))], + ['clear', D.clearLogs], ['file', null]].map(([l, fn]) => ( + + ))} +
+
el && (el.scrollTop = el.scrollHeight)} + style={{ maxHeight: 130, overflowY:'auto', padding:'8px 16px', + fontFamily: fontMono, fontSize: 10.5, lineHeight: 1.6, color: t.dim, background: t.panel }}> + {D.logs.map((l,i) => ( +
+ {window.fmtTime(l.t)}{' '} + [{l.level}]{' '} + {l.msg} +
+ ))} +
+ + )} +
+ ); +} + +window.WorkshopWindow = WorkshopWindow;