// 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' ? (
) : (
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 (
{label}
{children}
);
}
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 (
{checked && }
onChange(e.target.checked)} style={{ display:'none' }}/>
{children}
);
}
function PrimaryBtn({ t, onClick, disabled, children, style }) {
return (
{children}
);
}
// ─── 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') && (
D.toggleExpand(test.id)} style={iconBtnStyle(t)} title="Подробнее">
)}
{(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}
)}
navigator.clipboard?.writeText(
`[${test.label}] ${r.error} — ${r.metric}` + (r.rawHex ? ` — raw=${r.rawHex}` : ''))}
style={smallBtn(t, fontMono)}>
copy
)}
);
})}
);
}
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 (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{
width: 92, padding: '9px 12px', borderRadius: 3, fontWeight: 600, fontSize: 12.5,
background: t.btnBg, color: hover ? t.danger : t.text,
border: `1px solid ${hover ? t.danger : t.border}`, cursor: 'pointer',
transition: 'color .12s, border-color .12s',
}}>
Cancel
);
}
function ClassicStopBtn({ t, D }) {
const enabled = D.phase === 'active';
return (
Stop
);
}
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.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, fontFamily: fontMono, letterSpacing: 0.3,
}}>
Logs
{D.logs.length} lines
{D.logsOpen && (
navigator.clipboard?.writeText(D.logs.map(l => `[${l.level}] ${l.msg}`).join('\n'))}>copy all
clear
open log file
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;