113616b039
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>
417 lines
18 KiB
React
417 lines
18 KiB
React
// 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 (
|
|
<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,
|
|
borderRadius: 0,
|
|
}}>
|
|
<FluentTitleBar t={t} mode={mode} />
|
|
|
|
<div style={{ flex: 1, overflow: 'auto', padding: '12px 12px 0', display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
|
|
{/* SOCKS5 form card */}
|
|
<FluentCard t={t}>
|
|
<FluentCardTitle t={t}>SOCKS5 Proxy</FluentCardTitle>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<FluentField t={t} label="Host" style={{ flex: 1 }}>
|
|
<input value={D.form.host}
|
|
onChange={e => D.update({ host: e.target.value })}
|
|
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
|
placeholder="95.165.72.59 или example.com"
|
|
style={fluentInputStyle(t, fontMono)} />
|
|
</FluentField>
|
|
<FluentField t={t} label="Port" style={{ width: 92 }}>
|
|
<input value={D.form.port}
|
|
onChange={e => D.update({ port: e.target.value.replace(/\D/g,'') })}
|
|
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
|
placeholder="12334" inputMode="numeric"
|
|
style={fluentInputStyle(t, fontMono)} />
|
|
</FluentField>
|
|
</div>
|
|
<FluentCheckbox t={t} checked={D.form.auth}
|
|
onChange={(v) => { D.update({ auth: v }); if (v) setTimeout(() => document.getElementById('flu-login')?.focus(), 30); }}>
|
|
Authentication
|
|
</FluentCheckbox>
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 8, opacity: D.form.auth ? 1 : 0.45, pointerEvents: D.form.auth ? 'auto' : 'none' }}>
|
|
<FluentField t={t} label="Login" style={{ flex: 1 }}>
|
|
<input id="flu-login" disabled={!D.form.auth} value={D.form.login}
|
|
onChange={e => D.update({ login: e.target.value })}
|
|
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
|
placeholder="user" style={fluentInputStyle(t, fontMono, !D.form.auth)} />
|
|
</FluentField>
|
|
<FluentField t={t} label="Password" style={{ flex: 1 }}>
|
|
<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()}
|
|
placeholder="••••••" style={fluentInputStyle(t, fontMono, !D.form.auth)} />
|
|
</FluentField>
|
|
</div>
|
|
<FluentPrimaryBtn t={t} onClick={D.runCheck} disabled={D.phase === 'checking' || isActive}>
|
|
{D.phase === 'checking' ? 'Checking…' : 'Check connection'}
|
|
</FluentPrimaryBtn>
|
|
</FluentCard>
|
|
|
|
{/* Status card */}
|
|
<FluentCard t={t}>
|
|
<FluentCardTitle t={t}>Status</FluentCardTitle>
|
|
<FluentStatus t={t} D={D} palette={palette} fontMono={fontMono} themeKey={themeKey} />
|
|
</FluentCard>
|
|
|
|
{/* Action buttons */}
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<FluentStartBtn t={t} D={D} fontMono={fontMono} />
|
|
<FluentStopBtn t={t} D={D} />
|
|
</div>
|
|
|
|
{isActive && <FluentLiveStats t={t} stats={D.stats} fontMono={fontMono} />}
|
|
|
|
<div style={{ height: 8 }} />
|
|
</div>
|
|
|
|
<FluentLogs t={t} D={D} fontMono={fontMono} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FluentTitleBar({ t, mode }) {
|
|
return (
|
|
<div style={{
|
|
height: 32, background: t.chrome, borderBottom: `1px solid ${t.border}`,
|
|
display: 'flex', alignItems: 'center',
|
|
fontFamily: "'Segoe UI Variable','Segoe UI',system-ui,sans-serif",
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', flex: 1 }}>
|
|
<window.BrandMark size={14} color={t.accent} />
|
|
<span style={{ fontSize: 12.5, fontWeight: 600 }}>Drover-Go</span>
|
|
<span style={{ fontSize: 11, color: t.dimmer }}>0.4.2</span>
|
|
</div>
|
|
<div style={{ display: 'flex' }}>
|
|
<FluentTitleBtn t={t}><window.IconGear color={t.dim} /></FluentTitleBtn>
|
|
<FluentTitleBtn t={t}><window.IconMin color={t.dim} /></FluentTitleBtn>
|
|
<FluentTitleBtn t={t} hoverBg="#c42b1c" hoverFg="#fff"><window.IconClose color={t.dim} /></FluentTitleBtn>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
function FluentTitleBtn({ children, t, hoverBg, hoverFg }) {
|
|
const [hover, setHover] = React.useState(false);
|
|
return (
|
|
<div onMouseEnter={() => 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}</div>
|
|
);
|
|
}
|
|
|
|
function FluentCard({ t, children, style }) {
|
|
return (
|
|
<div style={{
|
|
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 6,
|
|
padding: 12, boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
|
|
display: 'flex', flexDirection: 'column', gap: 8, ...style,
|
|
}}>{children}</div>
|
|
);
|
|
}
|
|
function FluentCardTitle({ t, children }) {
|
|
return <div style={{ fontSize: 11.5, fontWeight: 600, color: t.dim, letterSpacing: 0.2 }}>{children}</div>;
|
|
}
|
|
function FluentField({ t, label, children, style }) {
|
|
return (
|
|
<label style={{ display: 'flex', flexDirection: 'column', gap: 2, ...style }}>
|
|
<span style={{ fontSize: 11, color: t.dimmer }}>{label}</span>
|
|
{children}
|
|
</label>
|
|
);
|
|
}
|
|
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 (
|
|
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, cursor: 'pointer', userSelect: 'none', fontSize: 12.5, marginTop: 2 }}>
|
|
<span style={{
|
|
width: 18, height: 18, borderRadius: 4,
|
|
border: `1.5px solid ${checked ? t.accent : t.borderHard}`,
|
|
background: checked ? t.accent : 'transparent',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
transition: 'background .12s, border-color .12s',
|
|
}}>
|
|
{checked && <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={checked} onChange={e => onChange(e.target.checked)} style={{ display: 'none' }} />
|
|
<span>{children}</span>
|
|
</label>
|
|
);
|
|
}
|
|
function FluentPrimaryBtn({ t, onClick, disabled, children, style }) {
|
|
const [hover, setHover] = React.useState(false);
|
|
return (
|
|
<button onClick={onClick} disabled={disabled}
|
|
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
style={{
|
|
height: 34, width: '100%', borderRadius: 4, border: 'none',
|
|
background: disabled ? t.panelAlt : (hover ? t.accentHover : t.accent),
|
|
color: disabled ? t.dimmer : '#fff', fontWeight: 600, fontSize: 13,
|
|
boxShadow: disabled ? 'none' : 'inset 0 -1px 0 rgba(0,0,0,.12)',
|
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
transition: 'background .12s', marginTop: 4, ...style,
|
|
}}>{children}</button>
|
|
);
|
|
}
|
|
|
|
function FluentStatus({ t, D, palette, fontMono, themeKey }) {
|
|
if (D.phase === 'idle') {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 0', color: t.dim, fontSize: 12.5 }}>
|
|
<span style={{ width: 8, height: 8, borderRadius: 4, background: t.dimmer }} />
|
|
<span>Ready to check</span>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5,
|
|
color: D.phase === 'checking' ? t.accent
|
|
: D.lastSummary?.failed === 0 ? t.pass : t.warn,
|
|
fontWeight: 600,
|
|
}}>
|
|
{D.phase === 'checking'
|
|
? <>
|
|
<window.StatusDot state="running" palette={palette} size={12} />
|
|
<span>Running diagnostics…</span>
|
|
<span style={{ marginLeft: 'auto', color: t.dim, fontFamily: fontMono, fontSize: 11, fontWeight: 400 }}>
|
|
{Object.keys(D.results).length}/{D.tests.length}
|
|
</span>
|
|
</>
|
|
: D.lastSummary?.failed === 0
|
|
? <span>All checks passed. Ready to start.</span>
|
|
: <span>{D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.</span>}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, marginTop: 4 }}>
|
|
{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 2px', minHeight: 22 }}>
|
|
<window.StatusDot state={state} palette={palette} size={12} />
|
|
<span style={{ fontSize: 12.5, 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' ? '…' : '')}
|
|
</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 21px', padding: '8px 10px', borderRadius: 4,
|
|
background: themeKey === 'l' ? '#fdf2f2' : '#3a1f1f',
|
|
border: `1px solid ${t.danger}`,
|
|
fontSize: 11.5,
|
|
}}>
|
|
<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.panelAlt, color: t.dim, border: `1px solid ${t.borderHard}`,
|
|
borderRadius: 3, cursor: 'pointer' }}>
|
|
copy error
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div style={{
|
|
flex: 1, height: 34, borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
|
background: warning ? t.warnBg : t.activeBg,
|
|
border: `1px solid ${warning ? t.warnBorder : t.activeBorder}`,
|
|
color: warning ? t.warn : t.pass, fontWeight: 600, fontSize: 12.5,
|
|
}}>
|
|
<span className="drv-pulsedot" style={{ width: 8, height: 8, borderRadius: 4, background: warning ? t.warn : t.pass }} />
|
|
Active{warning ? ' · UDP fallback' : ''}
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<FluentPrimaryBtn t={t} disabled={!checkedOk} onClick={D.startProxy} style={{ flex: 1, marginTop: 0 }}>
|
|
Start proxying
|
|
</FluentPrimaryBtn>
|
|
);
|
|
}
|
|
function FluentStopBtn({ t, D }) {
|
|
const enabled = D.phase === 'active';
|
|
const [hover, setHover] = React.useState(false);
|
|
return (
|
|
<button onClick={D.stopProxy} disabled={!enabled}
|
|
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
style={{
|
|
flex: 1, height: 34, borderRadius: 4, fontWeight: 600, fontSize: 12.5,
|
|
background: enabled && hover ? t.panelAlt : t.panel,
|
|
color: enabled ? t.text : t.dimmer,
|
|
border: `1px solid ${t.borderHard}`, cursor: enabled ? 'pointer' : 'not-allowed',
|
|
transition: 'background .12s',
|
|
}}>Stop</button>
|
|
);
|
|
}
|
|
function FluentLiveStats({ t, stats, fontMono }) {
|
|
const cell = (label, val) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 5, color: t.dim, fontSize: 11.5, fontFamily: fontMono }}>
|
|
{label}<span style={{ color: t.text }}>{val}</span>
|
|
</div>
|
|
);
|
|
return (
|
|
<div style={{
|
|
padding: '8px 12px', borderRadius: 4,
|
|
background: t.panel, border: `1px solid ${t.border}`,
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
}}>
|
|
{cell(<window.IconArrowUp color={t.dim} />, window.fmtBytes(stats.up))}
|
|
{cell(<window.IconArrowDown color={t.dim} />, window.fmtBytes(stats.down))}
|
|
{cell(<span style={{ fontSize: 9, color: t.dimmer }}>TCP</span>, stats.tcp)}
|
|
{cell(<span style={{ fontSize: 9, color: t.dimmer }}>UDP</span>, stats.udp)}
|
|
{cell(<span style={{ fontSize: 9, color: t.dimmer }}>↑t</span>, window.fmtUptime(stats.uptimeS))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FluentLogs({ t, D, fontMono }) {
|
|
return (
|
|
<div style={{ borderTop: `1px solid ${t.border}`, background: t.chrome, flexShrink: 0 }}>
|
|
<button onClick={() => D.setLogsOpen(!D.logsOpen)} style={{
|
|
width: '100%', padding: '8px 14px', display: 'flex', alignItems: 'center', gap: 8,
|
|
background: 'transparent', border: 'none', color: t.dim, cursor: 'pointer',
|
|
fontSize: 11.5, fontFamily: fontMono,
|
|
}}>
|
|
<window.IconChevron color={t.dim} dir={D.logsOpen ? 'down' : 'right'} />
|
|
<span>Logs</span>
|
|
<span style={{ marginLeft: 'auto', color: t.dimmer }}>{D.logs.length} lines</span>
|
|
</button>
|
|
{D.logsOpen && (
|
|
<div style={{ borderTop: `1px solid ${t.border}` }}>
|
|
<div style={{ display: 'flex', gap: 6, padding: '6px 12px', borderBottom: `1px solid ${t.border}` }}>
|
|
{[
|
|
['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) => (
|
|
<button key={i} onClick={fn} style={{
|
|
padding: '3px 8px', fontSize: 11, fontFamily: fontMono,
|
|
background: t.panel, color: t.dim, border: `1px solid ${t.borderHard}`,
|
|
borderRadius: 3, cursor: 'pointer',
|
|
}}>{l}</button>
|
|
))}
|
|
</div>
|
|
<div className="drv-log" style={{
|
|
maxHeight: 130, overflowY: 'auto', padding: '6px 12px',
|
|
fontFamily: fontMono, fontSize: 10.5, lineHeight: 1.55, color: t.dim,
|
|
background: t.panelAlt,
|
|
}} 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>
|
|
);
|
|
}
|
|
|
|
window.FluentWindow = FluentWindow;
|