Files
drover-go/docs/design/v2/drover-hero-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

425 lines
20 KiB
React

// 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 (
<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.4,
}}>
{/* Title bar */}
<div style={{ height: 36, display: 'flex', alignItems: 'center', padding: '0 14px', fontSize: 12.5 }}>
<window.BrandMark size={14} color={t.accent} />
<span style={{ marginLeft: 10, fontWeight: 600, color: t.text }}>Drover-Go</span>
<span style={{ marginLeft: 8, fontSize: 11, color: t.dim }}>0.4.2</span>
<span style={{ marginLeft: 'auto', fontSize: 11, color: ringColor, display: 'flex', alignItems: 'center', gap: 6 }}>
{phase === 'active' && <span className="drv-pulsedot" style={{ width: 6, height: 6, borderRadius: 3, background: ringColor }} />}
{phase === 'idle' && 'idle'}
{phase === 'checking' && 'checking…'}
{phase === 'checked' && (allFailed ? 'failed' : 'ready')}
{phase === 'active' && (warning ? 'connected · warn' : 'connected')}
</span>
<div style={{ display: 'flex', marginLeft: 8 }}>
<HeroTitleBtn t={t}><window.IconGear color={t.dim} /></HeroTitleBtn>
<HeroTitleBtn t={t}><window.IconMin color={t.dim} /></HeroTitleBtn>
<HeroTitleBtn t={t} hoverBg="#c42b1c"><window.IconClose color={t.dim} /></HeroTitleBtn>
</div>
</div>
{/* Big button + headlines */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '8px 18px 0', gap: 14, minHeight: 0 }}>
<BigToggle t={t} state={buttonState} onClick={handleBigClick} progress={
phase === 'checking' ? Object.keys(D.results).length / D.tests.length : 0
} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 19, fontWeight: 600 }}>{headline}</div>
<div style={{ fontSize: 12.5, color: t.dim, marginTop: 4, fontFamily: fontMono }}>{subline}</div>
</div>
{/* Stats — only when active */}
{isActive && (
<div style={{
display: 'flex', width: '100%', background: t.panel,
border: `1px solid ${t.border}`, borderRadius: 12, padding: 12, justifyContent: 'space-around',
}}>
<HeroStat icon="↑" v={fmtCompact(D.stats.up)} u={fmtUnit(D.stats.up)} c={t.accent} mono={fontMono} t={t} />
<HeroStat icon="↓" v={fmtCompact(D.stats.down)} u={fmtUnit(D.stats.down)} c={t.accent} mono={fontMono} t={t} />
<HeroStat icon="◇" v={D.stats.tcp} u="tcp" c={t.text} mono={fontMono} t={t} />
<HeroStat icon="◈" v={D.stats.udp} u="udp" c={warning ? t.warn : t.text} mono={fontMono} t={t} />
</div>
)}
{/* Diagnostic mini-list during check or after */}
{(phase === 'checking' || (phase === 'checked' && !isActive)) && (
<div style={{ width: '100%', flex: 1, minHeight: 0, overflow: 'auto' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{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: '4px 8px', minHeight: 24, fontSize: 12.5 }}>
<window.StatusDot state={state} palette={palette} size={11} />
<span style={{ color: state === 'pending' ? t.dimmer : t.textDim }} 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' ? '…' : '')}
</span>
{r?.result === 'failed' && (
<button onClick={() => D.toggleExpand(test.id)}
style={{ width: 18, height: 18, 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 8px 4px 28px', padding: '8px 10px', borderRadius: 6,
background: themeKey === 'd' ? 'rgba(255,107,107,.08)' : '#fdf2f2',
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>
</div>
)}
</div>
);
})}
</div>
</div>
)}
</div>
{/* Bottom: form (idle) or footer chips (active) */}
<div style={{ padding: '0 18px 14px' }}>
<div style={{
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 12,
overflow: 'hidden',
}}>
<button onClick={() => setShowForm(s => !s)}
style={{
width: '100%', padding: '11px 14px', display: 'flex', alignItems: 'center', gap: 10,
background: 'transparent', border: 'none', cursor: 'pointer', color: t.text,
fontSize: 12.5, fontFamily: fontUI, textAlign: 'left',
}}>
<span style={{
width: 26, height: 26, borderRadius: 13, background: t.panelAlt,
border: `1px solid ${t.border}`, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
fontFamily: fontMono, fontSize: 10, color: t.accent,
}}>S5</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 12.5, fontWeight: 600 }}>Proxy</div>
<div style={{ fontSize: 11, color: t.dim, fontFamily: fontMono }}>{D.form.host}:{D.form.port}{D.form.auth ? ' · auth' : ''}</div>
</div>
<window.IconChevron color={t.dim} dir={showForm ? 'up' : 'down'} />
</button>
{showForm && (
<div className="drv-fadein" style={{ borderTop: `1px solid ${t.border}`, padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ display: 'flex', gap: 8 }}>
<HeroInput t={t} fontMono={fontMono} value={D.form.host}
onChange={v => D.update({ host: v })}
onEnter={D.runCheck} placeholder="95.165.72.59 или example.com" label="Host" style={{ flex: 1 }} />
<HeroInput t={t} fontMono={fontMono} value={D.form.port}
onChange={v => D.update({ port: v.replace(/\D/g,'') })}
onEnter={D.runCheck} placeholder="12334" label="Port" style={{ width: 90 }} />
</div>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, cursor: 'pointer', userSelect: 'none', fontSize: 12.5 }}>
<span style={{
width: 16, height: 16, borderRadius: 4,
border: `1.5px solid ${D.form.auth ? t.accent : t.borderHard}`,
background: D.form.auth ? t.accent : 'transparent',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
transition: 'background .12s',
}}>
{D.form.auth && <svg width="10" height="10" viewBox="0 0 11 11"><path d="M2 5.5l2.5 2.5 4.5-5" stroke={themeKey === 'd' ? '#0c1a18' : '#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('hero-login')?.focus(), 30); }}
style={{ display: 'none' }} />
<span style={{ color: t.textDim }}>Authentication</span>
</label>
<div style={{ display: 'flex', gap: 8, opacity: D.form.auth ? 1 : 0.4, pointerEvents: D.form.auth ? 'auto' : 'none' }}>
<HeroInput t={t} fontMono={fontMono} id="hero-login" value={D.form.login}
onChange={v => D.update({ login: v })} onEnter={D.runCheck}
placeholder="user" label="Login" style={{ flex: 1 }} disabled={!D.form.auth} />
<HeroInput t={t} fontMono={fontMono} value={D.form.password} type="password"
onChange={v => D.update({ password: v })} onEnter={D.runCheck}
placeholder="••••••" label="Password" style={{ flex: 1 }} disabled={!D.form.auth} />
</div>
</div>
)}
</div>
{/* Logs strip */}
<button onClick={() => setShowDetails(s => !s)}
style={{
width: '100%', marginTop: 8, padding: '6px 4px', display: 'flex', alignItems: 'center', gap: 6,
background: 'transparent', border: 'none', cursor: 'pointer', color: t.dim,
fontSize: 11, fontFamily: fontMono, textAlign: 'left',
}}>
<window.IconChevron color={t.dim} dir={showDetails ? 'down' : 'right'} />
<span>Logs</span>
<span style={{ marginLeft: 'auto', color: t.dimmer }}>{D.logs.length} lines</span>
</button>
{showDetails && (
<div className="drv-fadein" style={{
background: t.panelAlt, border: `1px solid ${t.border}`, borderRadius: 8,
maxHeight: 110, overflow: 'auto', padding: '8px 12px',
fontFamily: fontMono, fontSize: 10.5, color: t.textDim,
}} ref={el => el && (el.scrollTop = el.scrollHeight)}>
{D.logs.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>
</div>
);
}
// ─── 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 (
<button onClick={onClick} disabled={isChecking}
style={{
position: 'relative', width: ring, height: ring, marginTop: 8,
background: isActive
? `radial-gradient(circle at 35% 30%, ${accent} 0%, ${accentDeep} 60%, ${accentDarker} 100%)`
: isFailed
? `radial-gradient(circle at 35% 30%, ${t.danger} 0%, #8c2c2c 100%)`
: `radial-gradient(circle at 35% 30%, ${t.panel} 0%, ${t.panelAlt} 100%)`,
boxShadow: isActive
? `0 0 60px ${accent}66, 0 0 0 6px ${accent}22, 0 0 0 12px ${accent}10`
: `0 0 0 2px ${accent}22, 0 6px 24px rgba(0,0,0,.15)`,
border: 'none', borderRadius: '50%', cursor: isChecking ? 'wait' : 'pointer',
color: isActive || isFailed ? '#fff' : accent,
transition: 'background .35s, box-shadow .35s, color .2s',
display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column',
}}>
{/* pulsing ring on active */}
{isActive && (
<span style={{
position: 'absolute', inset: -16, borderRadius: '50%',
border: `1px solid ${accent}66`, animation: 'drv-pulse-ring 2s ease-out infinite',
}} />
)}
{/* progress ring while checking */}
{isChecking && (
<svg width={ring} height={ring} style={{ position: 'absolute', inset: 0, transform: 'rotate(-90deg)' }}>
<circle cx={ring/2} cy={ring/2} r={r} fill="none" stroke={`${accent}30`} strokeWidth={stroke} />
<circle cx={ring/2} cy={ring/2} r={r} fill="none" stroke={accent} strokeWidth={stroke} strokeLinecap="round"
strokeDasharray={c} strokeDashoffset={c * (1 - progress)}
style={{ transition: 'stroke-dashoffset .35s' }} />
{/* spinner segment */}
<circle cx={ring/2} cy={ring/2} r={r-12} fill="none" stroke={accent} strokeWidth="2" strokeLinecap="round"
strokeDasharray={`${(c-24)*0.18} ${(c-24)*0.82}`}
style={{ animation: 'drv-spin 1.2s linear infinite', transformOrigin: '50% 50%' }} />
</svg>
)}
<span style={{ fontSize: 42, lineHeight: 1, fontWeight: 100 }}></span>
<span style={{ fontSize: 11, letterSpacing: '.25em', marginTop: 6, opacity: 0.95, fontWeight: 600 }}>
{isActive ? 'ACTIVE' : isChecking ? 'TESTING' : isFailed ? 'RETRY' : 'OFF'}
</span>
<style>{`
@keyframes drv-pulse-ring { 0%{ transform: scale(1); opacity:.5; } 70%{ transform: scale(1.18); opacity:0;} 100%{transform:scale(1.18); opacity:0;}}
`}</style>
</button>
);
}
function HeroStat({ icon, v, u, c, mono, t }) {
return (
<div style={{ textAlign: 'center', minWidth: 56 }}>
<div style={{ fontSize: 11, color: t.accent }}>{icon}</div>
<div style={{ fontSize: 18, fontWeight: 600, fontFamily: mono, marginTop: 2, color: c }}>{v}</div>
<div style={{ fontSize: 9.5, color: t.dim, letterSpacing: '.06em', textTransform: 'uppercase' }}>{u}</div>
</div>
);
}
function HeroTitleBtn({ children, t, hoverBg }) {
const [hover, setHover] = React.useState(false);
return (
<div onMouseEnter={() => 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}</div>
);
}
function HeroInput({ t, fontMono, value, onChange, onEnter, placeholder, label, style, type, disabled, id }) {
const [focus, setFocus] = React.useState(false);
return (
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, ...style }}>
<span style={{ fontSize: 10.5, color: t.dim, fontWeight: 500, letterSpacing: 0.4, textTransform: 'uppercase' }}>{label}</span>
<input id={id} type={type} value={value} disabled={disabled}
onChange={e => onChange(e.target.value)}
onKeyDown={e => e.key === 'Enter' && onEnter?.()}
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
placeholder={placeholder}
style={{
height: 32, background: t.panelAlt, color: disabled ? t.dimmer : t.text,
border: `1px solid ${focus ? t.accent : t.border}`,
borderRadius: 7, padding: '0 10px',
fontFamily: fontMono, fontSize: 12, outline: 'none', boxSizing: 'border-box',
transition: 'border-color .12s, box-shadow .12s',
boxShadow: focus ? `0 0 0 3px ${t.accent}22` : 'none',
}} />
</label>
);
}
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;