// Classic.jsx — Variant 1: Classic devtool. // Information-dense. Mono metrics. Plain rectangles, hairline borders. // Sober palette: neutral grays + one teal accent for primary action / success. import * as React from 'react' import { useDrover, BrandMark, IconGear, IconMin, IconClose, IconChevron, IconCopy, IconArrowUp, IconArrowDown, IconSun, IconMoon, StatusDot, fmtBytes, fmtUptime, fmtTime, } from './shared.jsx' import { WindowMinimise, Quit } from '../../wailsjs/runtime/runtime' import { Version as GoVersion } from '../../wailsjs/go/gui/App' const ClassicTheme = { // dark d: { bg: '#1c1d20', chrome: '#15161a', panel: '#22242a', panelAlt: '#1a1c20', border: '#34373d', borderSoft:'#2a2c32', text: '#dde0e6', dim: '#8a8f99', dimmer: '#5b6068', accent: '#3ea99f', // teal accentDim: '#2a7d76', danger: '#d96565', warn: '#d9a155', pass: '#5cba8b', skip: '#7c8088', inputBg: '#15161a', btnBg: '#2c2f36', btnBgH: '#373b43', primaryBg: '#3ea99f', primaryFg: '#0c1a18', }, l: { bg: '#f3f4f6', chrome: '#e8eaef', panel: '#ffffff', panelAlt: '#f8f9fb', border: '#d8dbe1', borderSoft:'#e6e8ec', text: '#1c1f24', dim: '#5c6168', dimmer: '#8a8f97', accent: '#2a7d76', accentDim: '#bdded9', danger: '#c0463f', warn: '#a8731e', pass: '#2f8c5a', skip: '#7c8088', inputBg: '#ffffff', btnBg: '#ffffff', btnBgH: '#f1f2f5', primaryBg: '#2a7d76', primaryFg: '#ffffff', }, }; export function ClassicWindow({ mode = 'dark', initial, onToggleMode }) { const t = ClassicTheme[mode === 'dark' ? 'd' : 'l']; const D = useDrover(initial); const [version, setVersion] = React.useState(''); React.useEffect(() => { GoVersion().then(setVersion).catch(() => {}); }, []); const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip, warn: t.warn }; const fontMono = '"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace'; const fontUI = '"Inter","Segoe UI",system-ui,sans-serif'; const isActive = D.phase === 'active'; const allChecked = D.phase === 'checked' || D.phase === 'active'; const failed = D.lastSummary?.failed ?? 0; return (
{/* ─── title bar ─── */} {/* ─── content ─── */}
{/* Form */} SOCKS5 Proxy
D.update({ host: e.target.value })} placeholder="95.165.72.59 или example.com" onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={inputStyle(t, fontMono)} /> D.update({ port: e.target.value.replace(/\D/g,'') })} placeholder="12334" inputMode="numeric" onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={inputStyle(t, fontMono)} />
{ D.update({ auth: v }); if (v) setTimeout(() => document.getElementById('cls-login')?.focus(), 30); }}> Authentication
D.update({ login: e.target.value })} placeholder="user" onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={inputStyle(t, fontMono, !D.form.auth)} /> D.update({ password: e.target.value })} placeholder="••••••" onKeyDown={e => e.key === 'Enter' && D.runCheck()} style={inputStyle(t, fontMono, !D.form.auth)} />
{D.phase === 'checking' ? (
Checking…
) : ( Check connection )} {/* Status */}
Status {/* Action buttons */}
{isActive && }
{/* Logs collapsible */}
); } // ─── pieces ───────────────────────────────────────────────────────────────── function ClassicTitleBar({ t, version, mode, onToggleMode }) { // Cell height = full title-bar height so the hover background fills // edge-to-edge (no thin sliver of chrome above the red close highlight). // alignSelf:'stretch' on the wrapper keeps it pinned to the top/bottom // of the flex row even though parent uses alignItems:'center' for text. const cellStyle = { width: 38, height: '100%', display:'flex', alignItems:'center', justifyContent:'center', color: t.dim, cursor:'pointer', }; return (
Drover-Go {version && v{version}}
onToggleMode && onToggleMode(e)}> {mode === 'dark' ? : }
WindowMinimise()}>
Quit()} onMouseEnter={e => e.currentTarget.style.background = '#c0463f'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
); } function SectionLabel({ children, t }) { return
{children}
; } function Field({ children, label, t, style }) { return ( ); } function inputStyle(t, fontMono, disabled) { return { background: t.inputBg, color: disabled ? t.dimmer : t.text, border: `1px solid ${t.border}`, borderRadius: 3, padding: '7px 9px', fontFamily: fontMono, fontSize: 12, outline: 'none', width: '100%', boxSizing: 'border-box', transition: 'border-color .12s, box-shadow .12s', }; } function Checkbox({ checked, onChange, children, t }) { return ( ); } function PrimaryBtn({ t, onClick, disabled, children, style }) { return ( ); } // ─── status panel ────────────────────────────────────────────────────────── function ClassicStatus({ t, D, palette, fontMono }) { const idle = D.phase === 'idle'; if (idle) { return (
Ready to check
); } return (
{/* header */}
{D.phase === 'checking' ? <> Running diagnostics… {Object.keys(D.results).length}/{D.tests.length} : D.lastSummary?.failed === 0 ? (D.lastSummary?.warnings > 0 ? All checks passed (with warnings). : All checks passed. Ready to start.) : {D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.}
{/* tests */}
{D.tests.map((test, i) => { const r = D.results[test.id]; const state = r?.result || (D.running === test.id ? 'running' : 'pending'); const isLast = i === D.tests.length - 1; return (
{test.label} {r?.metric || (state === 'running' ? '...' : '')} {(r?.result === 'failed' || r?.result === 'warn') && ( )}
{(r?.result === 'failed' || r?.result === 'warn') && r.expanded && (
{r.error ?
{r.error}
: (r.hint &&
{r.hint}
)} {r.error &&
{r.hint}
} {r.rawHex && (
{r.rawHex.length > 64 ? r.rawHex.slice(0, 64) + '…' : r.rawHex}
)}
)}
); })}
); } function iconBtnStyle(t) { return { width: 20, height: 20, padding: 0, border:'none', background:'transparent', cursor:'pointer', display:'inline-flex', alignItems:'center', justifyContent:'center', borderRadius: 2, }; } function smallBtn(t, fontMono) { return { display:'inline-flex', alignItems:'center', gap: 4, padding: '3px 7px', background: t.btnBg, border: `1px solid ${t.border}`, color: t.dim, borderRadius: 3, fontFamily: fontMono, fontSize: 10.5, cursor:'pointer', }; } // crude color mix for dark/light. expects hex (#rrggbb), bg can be hex too. amount=share-of-bg. function mode_mix(fg, bg, amt) { const a = hexToRgb(fg), b = hexToRgb(bg); return `rgb(${Math.round(a.r*(1-amt)+b.r*amt)},${Math.round(a.g*(1-amt)+b.g*amt)},${Math.round(a.b*(1-amt)+b.b*amt)})`; } function hexToRgb(h) { const v = h.replace('#',''); return { r: parseInt(v.slice(0,2),16), g: parseInt(v.slice(2,4),16), b: parseInt(v.slice(4,6),16) }; } // ─── start/stop ──────────────────────────────────────────────────────────── function ClassicStartBtn({ 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 (
Active{warning ? ' · UDP fallback' : ''}
); } return ( Start proxying ); } function ClassicCancelBtn({ t, onClick }) { const [hover, setHover] = React.useState(false); return ( ); } function ClassicStopBtn({ t, D }) { const enabled = D.phase === 'active'; return ( ); } function ClassicLiveStats({ t, stats, fontMono }) { const cell = (icon, val) => (
{icon}{val}
); return (
{cell(, fmtBytes(stats.up))} {cell(, fmtBytes(stats.down))} {cell(TCP, stats.tcp)} {cell(UDP, stats.udp)} {cell(↑t, fmtUptime(stats.uptimeS))}
); } // ─── logs ────────────────────────────────────────────────────────────────── function ClassicLogs({ t, D, fontMono }) { return (
{D.logsOpen && (
el && (el.scrollTop = el.scrollHeight)}> {D.logs.map((l, i) => (
{fmtTime(l.t)} {' '} [{l.level}] {' '} {l.msg}
))}
)}
); } // Default export so App.jsx can `import ClassicWindow from './components/Classic'`. export default ClassicWindow;