Files
drover-go/docs/design/v2/drover-wizard-live.jsx
T
root 113616b039
Release / release (push) Successful in 3m19s
docs/design/v2: add 12-variant React design archive
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>
2026-05-01 03:12:02 +03:00

456 lines
21 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;