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>
310 lines
15 KiB
React
310 lines
15 KiB
React
// 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 (
|
|
<div style={{
|
|
width: 480, height: 640, background: t.bg, color: t.text, display:'flex', flexDirection:'column',
|
|
fontFamily: fontUI, fontSize: 13, overflow:'hidden', border:'1px solid #000',
|
|
}}>
|
|
<StdTitle t={t}/>
|
|
<div style={{ flex: 1, overflow:'auto', padding:'14px 16px 4px' }}>
|
|
|
|
<StdSection t={t} title="SOCKS5 Proxy">
|
|
<div style={{ display:'flex', gap: 10 }}>
|
|
<StdField t={t} label="Host" style={{ flex: 1 }}>
|
|
<StdInput t={t} fontUI={fontUI} value={D.form.host}
|
|
onChange={v => D.update({ host: v })} onSubmit={D.runCheck}
|
|
placeholder="95.165.72.59 или example.com"/>
|
|
</StdField>
|
|
<StdField t={t} label="Port" style={{ width: 96 }}>
|
|
<StdInput t={t} fontUI={fontUI} value={D.form.port}
|
|
onChange={v => D.update({ port: v.replace(/\D/g,'') })} onSubmit={D.runCheck}
|
|
placeholder="12334" inputMode="numeric"/>
|
|
</StdField>
|
|
</div>
|
|
|
|
<div style={{ height: 12 }}/>
|
|
<StdCheck t={t} checked={D.form.auth}
|
|
onChange={(v) => { D.update({ auth: v }); if (v) setTimeout(()=>document.getElementById('std-login')?.focus(),30); }}>
|
|
Authentication
|
|
</StdCheck>
|
|
<div style={{
|
|
display:'flex', gap: 10, marginTop: 10,
|
|
opacity: D.form.auth ? 1 : 0.4, pointerEvents: D.form.auth?'auto':'none',
|
|
}}>
|
|
<StdField t={t} label="Login" style={{ flex: 1 }}>
|
|
<StdInput id="std-login" t={t} fontUI={fontUI} value={D.form.login}
|
|
onChange={v => D.update({ login: v })} onSubmit={D.runCheck} placeholder="user" disabled={!D.form.auth}/>
|
|
</StdField>
|
|
<StdField t={t} label="Password" style={{ flex: 1 }}>
|
|
<StdInput t={t} fontUI={fontUI} value={D.form.password} type="password"
|
|
onChange={v => D.update({ password: v })} onSubmit={D.runCheck} placeholder="••••••"
|
|
disabled={!D.form.auth}/>
|
|
</StdField>
|
|
</div>
|
|
|
|
<div style={{ height: 14 }}/>
|
|
<button onClick={D.runCheck} disabled={D.phase==='checking'||isActive} style={{
|
|
width:'100%', padding:'9px 14px', borderRadius: 6, border:'none',
|
|
background: (D.phase==='checking'||isActive) ? t.panel2 : t.accent,
|
|
color: (D.phase==='checking'||isActive) ? t.dimmer : t.primaryFg,
|
|
fontWeight: 600, fontSize: 13, cursor: D.phase==='checking'?'not-allowed':'pointer',
|
|
fontFamily: fontUI,
|
|
}}>{D.phase==='checking' ? 'Checking…' : 'Check connection'}</button>
|
|
</StdSection>
|
|
|
|
<div style={{ height: 14 }}/>
|
|
<StdSection t={t} title="Status" right={
|
|
D.phase==='checking' ? `${Object.keys(D.results).length}/${D.tests.length}` :
|
|
D.lastSummary ? (D.lastSummary.failed === 0 ? 'all passed' : `${D.lastSummary.failed} failed`) : null
|
|
}>
|
|
{D.phase === 'idle'
|
|
? <div style={{ display:'flex', alignItems:'center', gap: 8, color: t.dim, padding:'2px 0' }}>
|
|
<span style={{ width: 6, height: 6, borderRadius: 3, background: t.dimmer }}/>
|
|
Ready to check
|
|
</div>
|
|
: <StdStatus t={t} D={D} palette={palette} fontMono={fontMono}/>}
|
|
</StdSection>
|
|
|
|
<div style={{ height: 14 }}/>
|
|
<StdSection t={t}>
|
|
<div style={{ display:'flex', gap: 10 }}>
|
|
<StdStartBtn t={t} D={D} fontUI={fontUI}/>
|
|
<StdStopBtn t={t} D={D} fontUI={fontUI}/>
|
|
</div>
|
|
{isActive && (
|
|
<div style={{
|
|
marginTop: 12, paddingTop: 12, borderTop:`1px solid ${t.border}`,
|
|
display:'flex', justifyContent:'space-between', color: t.dim,
|
|
}}>
|
|
<StdStat icon={<window.IconArrowUp color={t.pass}/>} val={window.fmtBytes(D.stats.up)} fontMono={fontMono} t={t}/>
|
|
<StdStat icon={<window.IconArrowDown color={t.accent}/>} val={window.fmtBytes(D.stats.down)} fontMono={fontMono} t={t}/>
|
|
<StdStat val={D.stats.tcp} lbl="TCP" fontMono={fontMono} t={t}/>
|
|
<StdStat val={D.stats.udp} lbl="UDP" fontMono={fontMono} t={t}/>
|
|
<StdStat val={window.fmtUptime(D.stats.uptimeS)} lbl="UP" fontMono={fontMono} t={t}/>
|
|
</div>
|
|
)}
|
|
</StdSection>
|
|
<div style={{ height: 12 }}/>
|
|
</div>
|
|
<StdLogs t={t} D={D} fontMono={fontMono}/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StdTitle({ t }) {
|
|
const cell = { width: 44, height: 32, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color: t.dim };
|
|
return (
|
|
<div style={{ height: 36, background: t.chrome, borderBottom:`1px solid ${t.border}`, display:'flex', alignItems:'center', userSelect:'none' }}>
|
|
<div style={{ display:'flex', alignItems:'center', gap: 9, padding:'0 14px', flex:1 }}>
|
|
<window.BrandMark size={15} color={t.accent}/>
|
|
<span style={{ fontSize: 13, fontWeight: 600 }}>Drover-Go</span>
|
|
<span style={{ fontSize: 11, color: t.dimmer }}>0.4.2</span>
|
|
</div>
|
|
<div style={{ display:'flex' }}>
|
|
<div style={cell}><window.IconGear color={t.dim}/></div>
|
|
<div style={cell}><window.IconMin color={t.dim}/></div>
|
|
<div style={cell}><window.IconClose color={t.dim}/></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StdSection({ t, title, right, children }) {
|
|
return (
|
|
<section>
|
|
{(title || right) && (
|
|
<div style={{ display:'flex', alignItems:'baseline', marginBottom: 8 }}>
|
|
{title && <div style={{ fontSize: 11.5, fontWeight: 600, color: t.dim, letterSpacing: 0.5 }}>{title}</div>}
|
|
{right && <div style={{ marginLeft:'auto', fontSize: 11, color: t.dimmer, fontFamily:'"JetBrains Mono",monospace' }}>{right}</div>}
|
|
</div>
|
|
)}
|
|
<div style={{ background: t.panel, border:`1px solid ${t.border}`, borderRadius: 8, padding: 14 }}>
|
|
{children}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
function StdField({ t, label, children, style }) {
|
|
return <label style={{ display:'flex', flexDirection:'column', gap: 5, ...style }}>
|
|
<span style={{ fontSize: 11.5, color: t.dim }}>{label}</span>{children}
|
|
</label>;
|
|
}
|
|
function StdInput({ t, fontUI, value, onChange, type, placeholder, onSubmit, disabled, id, inputMode }) {
|
|
return <input id={id} value={value} type={type||'text'} disabled={disabled}
|
|
inputMode={inputMode}
|
|
onChange={e => 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 (
|
|
<label style={{ display:'inline-flex', alignItems:'center', gap: 8, cursor:'pointer' }}>
|
|
<span style={{
|
|
width: 16, height: 16, borderRadius: 4, border:`1px solid ${checked?t.accent:t.border}`,
|
|
background: checked ? t.accent : 'transparent',
|
|
display:'flex', alignItems:'center', justifyContent:'center', transition:'all .12s',
|
|
}}>
|
|
{checked && <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5l2 2 4-4.4" stroke={t.primaryFg} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" fill="none"/></svg>}
|
|
</span>
|
|
<input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)} style={{display:'none'}}/>
|
|
<span style={{ fontSize: 13 }}>{children}</span>
|
|
</label>
|
|
);
|
|
}
|
|
function StdStatus({ t, D, palette, fontMono }) {
|
|
const allOk = D.lastSummary?.failed === 0;
|
|
return (
|
|
<div>
|
|
{D.phase === 'checking'
|
|
? <div style={{ display:'flex', alignItems:'center', gap: 8, marginBottom: 8 }}>
|
|
<window.StatusDot state="running" palette={palette} size={14}/>
|
|
<span style={{ fontWeight: 500 }}>Running diagnostics…</span>
|
|
</div>
|
|
: <div style={{
|
|
padding:'7px 11px', marginBottom: 10, borderRadius: 6, fontSize: 12.5, fontWeight: 500,
|
|
background: allOk ? t.passSoft : t.warnSoft,
|
|
border:`1px solid ${allOk ? t.pass : t.warn}40`,
|
|
color: allOk ? t.pass : t.warn,
|
|
}}>
|
|
{allOk ? 'All checks passed. Ready to start.'
|
|
: `${D.lastSummary?.failed} of ${D.tests.length} checks failed. Some features won't work.`}
|
|
</div>}
|
|
<div>
|
|
{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: 10, height: 26 }}>
|
|
<window.StatusDot state={state} palette={palette} size={13}/>
|
|
<span style={{ color: state==='pending'?t.dim:t.text, fontSize: 13 }} title={test.desc}>{test.label}</span>
|
|
<span style={{
|
|
marginLeft:'auto', fontFamily: fontMono, fontSize: 11.5,
|
|
color: state==='failed'?t.danger:state==='skipped'?t.skip:t.dim,
|
|
}}>{r?.metric}</span>
|
|
{r?.result === 'failed' && (
|
|
<button onClick={() => D.toggleExpand(test.id)} style={{
|
|
background:'transparent', border:'none', cursor:'pointer', padding: 4, color: t.dim,
|
|
}}><window.IconChevron color={t.dim} dir={r.expanded?'up':'down'}/></button>
|
|
)}
|
|
</div>
|
|
{r?.result === 'failed' && r.expanded && (
|
|
<div className="drv-fadein" style={{
|
|
margin: '4px 0 6px 23px', padding: 10, borderRadius: 6,
|
|
background: t.danSoft, border: `1px solid ${t.danger}30`, fontSize: 12,
|
|
}}>
|
|
<div style={{ color: t.danger, fontWeight: 600, marginBottom: 3 }}>{r.error}</div>
|
|
<div style={{ color: t.dim, lineHeight: 1.5 }}>{r.hint}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
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 (
|
|
<div style={{
|
|
flex: 1, padding:'9px 14px', borderRadius: 6, fontWeight: 600, fontSize: 13,
|
|
background: warning ? t.warnSoft : t.passSoft, color: c, border:`1px solid ${c}55`,
|
|
display:'flex', alignItems:'center', justifyContent:'center', gap: 8,
|
|
}}>
|
|
<span className="drv-pulsedot" style={{ width: 7, height: 7, borderRadius: 4, background: c }}/>
|
|
Active{warning ? ' · UDP fallback' : ''}
|
|
</div>
|
|
);
|
|
}
|
|
return <button onClick={D.startProxy} disabled={!ok} style={{
|
|
flex: 1, padding:'9px 14px', borderRadius: 6, border:'none',
|
|
background: ok ? t.accent : t.panel2, color: ok ? t.primaryFg : t.dimmer,
|
|
fontWeight: 600, fontSize: 13, cursor: ok?'pointer':'not-allowed', fontFamily: fontUI,
|
|
}}>Start proxying</button>;
|
|
}
|
|
function StdStopBtn({ t, D, fontUI }) {
|
|
const enabled = D.phase === 'active';
|
|
return <button onClick={D.stopProxy} disabled={!enabled} style={{
|
|
flex: 1, padding:'9px 14px', borderRadius: 6, background:'transparent',
|
|
color: enabled ? t.text : t.dimmer, border:`1px solid ${t.border}`,
|
|
fontWeight: 600, fontSize: 13, cursor: enabled?'pointer':'not-allowed', fontFamily: fontUI,
|
|
}}>Stop</button>;
|
|
}
|
|
function StdStat({ icon, val, lbl, fontMono, t }) {
|
|
return <div style={{ display:'flex', alignItems:'center', gap: 4 }}>
|
|
{icon}<span style={{ fontFamily: fontMono, fontSize: 11.5, color: t.text }}>{val}</span>
|
|
{lbl && <span style={{ fontSize: 10, color: t.dimmer, textTransform:'uppercase', letterSpacing: 0.5 }}>{lbl}</span>}
|
|
</div>;
|
|
}
|
|
function StdLogs({ 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:'9px 16px', display:'flex', alignItems:'center', gap: 9,
|
|
background:'transparent', border:'none', color: t.dim, cursor:'pointer', fontSize: 12,
|
|
}}>
|
|
<window.IconChevron color={t.dim} dir={D.logsOpen?'down':'right'}/>
|
|
<span style={{ fontWeight: 600 }}>Logs</span>
|
|
<span style={{ marginLeft:'auto', fontFamily: fontMono, fontSize: 11, color: t.dimmer }}>{D.logs.length}</span>
|
|
</button>
|
|
{D.logsOpen && (
|
|
<>
|
|
<div style={{ display:'flex', gap: 6, padding:'0 16px 8px' }}>
|
|
{[['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]) => (
|
|
<button key={l} onClick={fn||undefined} style={{
|
|
background:'transparent', border:`1px solid ${t.border}`, borderRadius: 4,
|
|
padding:'4px 9px', fontSize: 11, color: t.dim, cursor:'pointer', fontFamily: fontMono,
|
|
}}>{l}</button>
|
|
))}
|
|
</div>
|
|
<div className="drv-log" ref={el => 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) => (
|
|
<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.accent, fontWeight: 600 }}>[{l.level}]</span>{' '}
|
|
{l.msg}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
window.StudioWindow = StudioWindow;
|