docs/design/v2: add 12-variant React design archive
Release / release (push) Successful in 3m19s

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 03:12:02 +03:00
parent 5da30ad058
commit 113616b039
20 changed files with 6566 additions and 0 deletions
+455
View File
@@ -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 (
<div style={{
width: 480, height: 640, background: t.bg, color: t.text,
display: 'flex', flexDirection: 'column', overflow: 'hidden',
fontFamily: fontUI, fontSize: 13.5, lineHeight: 1.45,
borderRadius: 0,
}}>
<WizardTitleBar t={t} />
{/* Stepper */}
<div style={{
display: 'flex', padding: '14px 18px', alignItems: 'center',
borderBottom: `1px solid ${t.border}`, background: t.chrome, gap: 0,
}}>
{[[1, 'Configure'], [2, 'Verify'], [3, 'Connect']].map(([n, label], i) => {
const done = step > n;
const current = step === n;
const dim = step < n;
return (
<React.Fragment key={n}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{
width: 22, height: 22, borderRadius: '50%',
background: dim ? t.panelAlt : t.accent,
border: dim ? `1px solid ${t.borderHard}` : 'none',
color: dim ? t.dimmer : '#fff',
fontSize: 11, fontWeight: 600,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
transition: 'background .2s',
}}>{done ? '✓' : n}</span>
<span style={{ fontSize: 12.5, fontWeight: current ? 600 : 400, color: dim ? t.dimmer : t.text }}>{label}</span>
</div>
{i < 2 && <div style={{
flex: 1, height: 1, background: step > n ? t.accent : t.borderHard,
margin: '0 10px', transition: 'background .2s',
}} />}
</React.Fragment>
);
})}
</div>
{/* Step content */}
<div style={{ flex: 1, overflow: 'auto', padding: '18px 22px', display: 'flex', flexDirection: 'column', gap: 14 }}>
{step === 1 && <WizConfigure t={t} D={D} fontMono={fontMono} />}
{step === 2 && <WizVerify t={t} D={D} fontMono={fontMono} palette={palette} themeKey={themeKey} />}
{step === 3 && <WizConnect t={t} D={D} fontMono={fontMono} />}
</div>
{/* Footer */}
<div style={{
height: 54, display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
gap: 8, padding: '0 18px', borderTop: `1px solid ${t.border}`, background: t.chrome,
}}>
<WizardSecondaryBtn t={t} onClick={() => {
if (step === 2) D.stopProxy?.(), D.setPhase('idle');
if (step === 3) D.stopProxy();
}} disabled={step === 1}>
{step === 3 ? 'Disconnect' : 'Back'}
</WizardSecondaryBtn>
<WizardPrimaryBtn t={t}
onClick={() => {
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'}
</WizardPrimaryBtn>
</div>
</div>
);
}
function WizardTitleBar({ t }) {
return (
<div style={{
height: 32, display: 'flex', alignItems: 'center', padding: '0 12px',
borderBottom: `1px solid ${t.border}`, fontSize: 12, color: t.text, background: t.chrome,
}}>
<window.BrandMark size={14} color={t.accent} />
<span style={{ marginLeft: 8, fontWeight: 600 }}>Drover-Go · Setup</span>
<div style={{ marginLeft: 'auto', display: 'flex' }}>
<WizTitleBtn t={t}><window.IconGear color={t.dim} /></WizTitleBtn>
<WizTitleBtn t={t}><window.IconMin color={t.dim} /></WizTitleBtn>
<WizTitleBtn t={t} hoverBg="#c0463f" hoverFg="#fff"><window.IconClose color={t.dim} /></WizTitleBtn>
</div>
</div>
);
}
function WizTitleBtn({ children, t, hoverBg, hoverFg }) {
const [hover, setHover] = React.useState(false);
return (
<div onMouseEnter={() => 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}</div>
);
}
// ─── 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 (
<>
<div>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>Configure your proxy</h2>
<div style={{ fontSize: 12.5, color: t.dim, marginTop: 4 }}>
Введите адрес SOCKS5-сервера. На следующем шаге мы проверим, что Discord будет работать через него.
</div>
</div>
<div style={{
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
padding: 16, display: 'flex', flexDirection: 'column', gap: 12,
}}>
<div style={{ display: 'flex', gap: 10 }}>
<label style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Host</span>
<input value={D.form.host}
onChange={e => D.update({ host: e.target.value })}
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
onFocus={onFocus} onBlur={onBlur}
placeholder="95.165.72.59 или example.com"
style={inputStyle(false)} />
</label>
<label style={{ width: 100, display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Port</span>
<input value={D.form.port}
onChange={e => D.update({ port: e.target.value.replace(/\D/g,'') })}
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
onFocus={onFocus} onBlur={onBlur}
placeholder="12334" inputMode="numeric"
style={inputStyle(false)} />
</label>
</div>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 9, cursor: 'pointer', userSelect: 'none', fontSize: 13 }}>
<span style={{
width: 18, height: 18, borderRadius: 4,
border: `1.5px solid ${D.form.auth ? t.accent : t.borderHard}`,
background: D.form.auth ? t.accent : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{D.form.auth && <svg width="11" height="11" viewBox="0 0 11 11"><path d="M2 5.5l2.5 2.5 4.5-5" stroke="#fff" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>}
</span>
<input type="checkbox" checked={D.form.auth}
onChange={e => { D.update({ auth: e.target.checked }); if (e.target.checked) setTimeout(() => document.getElementById('wiz-login')?.focus(), 30); }}
style={{ display: 'none' }} />
<span>Authentication</span>
</label>
<div style={{ display: 'flex', gap: 10, opacity: D.form.auth ? 1 : 0.4, pointerEvents: D.form.auth ? 'auto' : 'none' }}>
<label style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Login</span>
<input id="wiz-login" disabled={!D.form.auth} value={D.form.login}
onChange={e => D.update({ login: e.target.value })}
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
onFocus={onFocus} onBlur={onBlur}
placeholder="user" style={inputStyle(!D.form.auth)} />
</label>
<label style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Password</span>
<input disabled={!D.form.auth} type="password" value={D.form.password}
onChange={e => D.update({ password: e.target.value })}
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
onFocus={onFocus} onBlur={onBlur}
placeholder="••••••" style={inputStyle(!D.form.auth)} />
</label>
</div>
</div>
<div style={{ fontSize: 12, color: t.dim, marginTop: 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{
width: 14, height: 14, borderRadius: '50%', border: `1px solid ${t.borderHard}`,
fontSize: 9, color: t.dim, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
}}>i</span>
Нажмите <span style={{ color: t.text, fontWeight: 600 }}>Verify </span>, чтобы продолжить.
</div>
</>
);
}
// ─── 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 (
<>
<div>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
{phase === 'checking' ? 'Verifying your proxy' : (failed === 0 ? 'All checks passed' : 'Some checks failed')}
</h2>
<div style={{ fontSize: 12.5, color: t.dim, marginTop: 4 }}>
{phase === 'checking'
? <>Запускаем 7 проверок против <span style={{ fontFamily: fontMono, color: t.text }}>{D.form.host}:{D.form.port}</span></>
: (failed === 0
? <>Прокси работает. Discord голос, чат и демонстрация будет работать через него.</>
: <>{failed} из {total} проверок не прошли. Часть функций работать не будет.</>)}
</div>
</div>
{/* progress + ring */}
<div style={{
display: 'flex', alignItems: 'center', gap: 16, padding: 14,
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
}}>
<svg width="64" height="64" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="26" fill="none" stroke={t.border} strokeWidth="6" />
<circle cx="32" cy="32" r="26" fill="none"
stroke={failed > 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' }} />
<text x="32" y="36" textAnchor="middle" fontSize="13" fontWeight="600" fill={t.text}>
{phase === 'checked' ? `${total - failed}/${total}` : `${completed}/${total}`}
</text>
</svg>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 13.5 }}>
{phase === 'checking' ? <>Testing <span style={{ fontFamily: fontMono }}>{D.tests.find(x => x.id === D.running)?.label || '…'}</span></> : (failed === 0 ? 'Готово к подключению' : 'Завершено с ошибками')}
</div>
<div style={{ fontSize: 12, color: t.dim, marginTop: 2 }}>
{phase === 'checking' ? 'This usually takes 515 seconds.' : 'См. отчёт ниже.'}
</div>
</div>
</div>
{/* test list */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, fontSize: 12.5 }}>
{D.tests.map((test) => {
const r = D.results[test.id];
const state = r?.result || (D.running === test.id ? 'running' : 'pending');
return (
<div key={test.id}>
<div style={{
display: 'flex', alignItems: 'center', gap: 9, padding: '5px 10px',
background: D.running === test.id ? t.accentSoft : 'transparent',
borderRadius: 4, minHeight: 26,
}}>
<window.StatusDot state={state} palette={palette} size={12} />
<span style={{ color: state === 'pending' ? t.dimmer : t.text }} title={test.desc}>{test.label}</span>
<span style={{ marginLeft: 'auto', fontFamily: fontMono, fontSize: 11,
color: state === 'failed' ? t.danger : state === 'skipped' ? t.skip : t.dim }}>
{r?.metric || (state === 'running' ? 'running…' : '')}
</span>
{r?.result === 'failed' && (
<button onClick={() => D.toggleExpand(test.id)}
style={{ width: 20, height: 20, padding: 0, border: 'none', background: 'transparent', cursor: 'pointer' }}>
<window.IconChevron color={t.dim} dir={r.expanded ? 'up' : 'down'} />
</button>
)}
</div>
{r?.result === 'failed' && r.expanded && (
<div className="drv-fadein" style={{
margin: '2px 0 4px 22px', padding: '8px 10px', borderRadius: 6,
background: themeKey === 'l' ? '#fdf2f2' : '#3a1f1f',
border: `1px solid ${t.danger}`, fontSize: 12,
}}>
<div style={{ color: t.danger, fontWeight: 600, marginBottom: 2 }}>{r.error}</div>
<div style={{ color: t.dim }}>{r.hint}</div>
<button onClick={() => navigator.clipboard?.writeText(`[${test.label}] ${r.error}${r.metric}`)}
style={{ marginTop: 6, padding: '3px 8px', fontSize: 11, fontFamily: fontMono,
background: t.panel, color: t.dim, border: `1px solid ${t.borderHard}`,
borderRadius: 4, cursor: 'pointer' }}>copy error</button>
</div>
)}
</div>
);
})}
</div>
</>
);
}
// ─── Step 3: Connect (active) ───────────────────────────────────────────
function WizConnect({ t, D, fontMono }) {
const stats = D.stats;
const failed = D.lastSummary?.failed ?? 0;
return (
<>
<div>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="drv-pulsedot" style={{
width: 10, height: 10, borderRadius: 5, background: failed > 0 ? t.warn : t.pass,
display: 'inline-block',
}} />
{failed > 0 ? 'Connected · UDP fallback' : 'Connected'}
</h2>
<div style={{ fontSize: 12.5, color: t.dim, marginTop: 4 }}>
Discord routes through <span style={{ fontFamily: fontMono, color: t.text }}>{D.form.host}:{D.form.port}</span>
{' · '}<span style={{ fontFamily: fontMono }}>{window.fmtUptime(stats.uptimeS)}</span>
</div>
</div>
{/* stats grid */}
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8,
}}>
<WizStat t={t} fontMono={fontMono} label="Upload" value={window.fmtBytes(stats.up)} accent={t.accent} />
<WizStat t={t} fontMono={fontMono} label="Download" value={window.fmtBytes(stats.down)} accent={t.pass} />
<WizStat t={t} fontMono={fontMono} label="TCP" value={stats.tcp} accent={t.accent} />
<WizStat t={t} fontMono={fontMono} label="UDP" value={stats.udp} accent={failed > 0 ? t.warn : t.pass} />
</div>
{/* logs panel inline */}
<div style={{
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
padding: 12, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column',
}}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 6 }}>
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 600, letterSpacing: 0.2 }}>RECENT LOG</span>
<button onClick={() => navigator.clipboard?.writeText(D.logs.map(l => `[${l.level}] ${l.msg}`).join('\n'))}
style={{ marginLeft: 'auto', padding: '2px 8px', fontSize: 11, fontFamily: fontMono,
background: 'transparent', color: t.dim, border: `1px solid ${t.borderHard}`,
borderRadius: 4, cursor: 'pointer' }}>copy</button>
<button onClick={D.clearLogs}
style={{ marginLeft: 6, padding: '2px 8px', fontSize: 11, fontFamily: fontMono,
background: 'transparent', color: t.dim, border: `1px solid ${t.borderHard}`,
borderRadius: 4, cursor: 'pointer' }}>clear</button>
</div>
<div className="drv-log" style={{
flex: 1, overflowY: 'auto', fontFamily: fontMono, fontSize: 11,
lineHeight: 1.55, color: t.dim, minHeight: 0,
}} ref={el => el && (el.scrollTop = el.scrollHeight)}>
{D.logs.slice(-30).map((l, i) => (
<div key={i}>
<span style={{ color: t.dimmer }}>{window.fmtTime(l.t)}</span>
{' '}
<span style={{ color: l.level === 'ERROR' ? t.danger : l.level === 'WARN' ? t.warn : t.pass, fontWeight: 600 }}>[{l.level}]</span>
{' '}
<span>{l.msg}</span>
</div>
))}
</div>
</div>
</>
);
}
function WizStat({ t, fontMono, label, value, accent }) {
return (
<div style={{
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
padding: '10px 12px',
}}>
<div style={{ fontSize: 10.5, color: t.dim, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: 600 }}>{label}</div>
<div style={{ fontSize: 18, fontWeight: 600, fontFamily: fontMono, marginTop: 3, color: accent }}>{value}</div>
</div>
);
}
function WizardPrimaryBtn({ t, onClick, disabled, children }) {
const [hover, setHover] = React.useState(false);
return (
<button onClick={onClick} disabled={disabled}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
height: 32, padding: '0 18px', borderRadius: 6, border: 'none',
background: disabled ? t.panelAlt : (hover ? '#1a5fbf' : t.accent),
color: disabled ? t.dimmer : '#fff',
fontWeight: 600, fontSize: 13, cursor: disabled ? 'not-allowed' : 'pointer',
boxShadow: disabled ? 'none' : 'inset 0 -1px 0 rgba(0,0,0,.18)',
}}>{children}</button>
);
}
function WizardSecondaryBtn({ t, onClick, disabled, children }) {
const [hover, setHover] = React.useState(false);
return (
<button onClick={onClick} disabled={disabled}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
height: 32, padding: '0 14px', borderRadius: 6,
background: hover && !disabled ? t.panelAlt : t.panel,
color: disabled ? t.dimmer : t.text,
border: `1px solid ${t.borderHard}`,
fontSize: 13, cursor: disabled ? 'not-allowed' : 'pointer',
}}>{children}</button>
);
}
window.WizardWindow = WizardWindow;