// drover-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.
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',
},
};
function ClassicWindow({ mode = 'dark', initial }) {
const t = ClassicTheme[mode === 'dark' ? 'd' : 'l'];
const D = window.useDrover(initial);
const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip };
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 ─── */}
{/* Logs collapsible */}
);
}
// ─── pieces ─────────────────────────────────────────────────────────────────
function ClassicTitleBar({ t }) {
const cellStyle = {
width: 38, height: 28, display:'flex', alignItems:'center', justifyContent:'center',
color: t.dim, cursor:'pointer',
};
return (
Drover-Go
v0.4.2
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
? 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 === 'failed' && r.expanded && (
{r.error}
{r.hint}
)}
);
})}
);
}
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 ClassicStopBtn({ t, D }) {
const enabled = D.phase === 'active';
return (
);
}
function ClassicLiveStats({ t, stats, fontMono }) {
const cell = (icon, val) => (
{icon}{val}
);
return (
{cell(, window.fmtBytes(stats.up))}
{cell(, window.fmtBytes(stats.down))}
{cell(TCP, stats.tcp)}
{cell(UDP, stats.udp)}
{cell(↑t, window.fmtUptime(stats.uptimeS))}
);
}
// ─── logs ──────────────────────────────────────────────────────────────────
function ClassicLogs({ t, D, fontMono }) {
return (
{D.logsOpen && (
el && (el.scrollTop = el.scrollHeight)}>
{D.logs.map((l, i) => (
{window.fmtTime(l.t)}
{' '}
[{l.level}]
{' '}
{l.msg}
))}
)}
);
}
window.ClassicWindow = ClassicWindow;