// drover-hero-live.jsx — Big toggle / VPN-style variant.
// Centerpiece: a giant connect button. Form lives in an expandable section below.
// Compact form (host:port) shown collapsed in active state; full form in idle.
const HERO_THEME = {
d: {
bg: 'radial-gradient(ellipse at top, #142340 0%, #0e1426 60%, #080d1c 100%)',
chrome: 'transparent',
panel: '#15192a',
panelAlt: '#1a1f33',
border: 'rgba(255,255,255,.08)',
borderHard:'rgba(255,255,255,.18)',
text: '#ffffff',
textDim: '#cbd1d9',
dim: '#7a8499',
dimmer: '#5a6178',
accent: '#5dd4b3',
accentDeep:'#2da085',
accentDarker:'#1a6e5b',
danger: '#ff6b6b',
warn: '#f3c764',
pass: '#5dd4b3',
skip: '#7a8499',
},
l: {
bg: 'linear-gradient(180deg, #f3f7fb 0%, #e8edf4 100%)',
chrome: 'transparent',
panel: '#ffffff',
panelAlt: '#f5f7fb',
border: 'rgba(0,0,0,.07)',
borderHard:'rgba(0,0,0,.15)',
text: '#0e1830',
textDim: '#3a4356',
dim: '#6a7383',
dimmer: '#9aa3b2',
accent: '#1aa787',
accentDeep:'#0f8c70',
accentDarker:'#0a6655',
danger: '#c0463f',
warn: '#a8731e',
pass: '#1aa787',
skip: '#7a8499',
},
};
function HeroWindow({ mode = 'dark', initial }) {
const themeKey = mode === 'dark' ? 'd' : 'l';
const t = HERO_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 = "'Inter','Segoe UI',system-ui,sans-serif";
const fontMono = "'JetBrains Mono','SF Mono',ui-monospace,Consolas,monospace";
const [showForm, setShowForm] = React.useState(false);
const [showDetails, setShowDetails] = React.useState(false);
const phase = D.phase;
const summary = D.lastSummary;
const allFailed = summary && summary.failed === D.tests.length;
const failed = summary?.failed ?? 0;
const isActive = phase === 'active';
const warning = isActive && failed > 0;
// big-button click handler. Two-step: if idle/checked, go.
const handleBigClick = () => {
if (phase === 'active') return D.stopProxy();
if (phase === 'checking') return;
if (phase === 'idle') {
// run check, then auto-start if it passed
(async () => {
await D.runCheck();
})();
return;
}
if (phase === 'checked' && !allFailed) D.startProxy();
if (phase === 'checked' && allFailed) D.runCheck();
};
// After runCheck completes from idle, auto-start if all passed.
const prevPhase = React.useRef(phase);
React.useEffect(() => {
if (prevPhase.current === 'checking' && phase === 'checked') {
const sum = D.lastSummary;
if (sum && sum.failed === 0) {
// small delay so user perceives the "checked" state briefly
setTimeout(() => D.startProxy(), 600);
}
}
prevPhase.current = phase;
}, [phase]);
// Big-button label / colors
let buttonState;
if (phase === 'active') buttonState = warning ? 'active-warn' : 'active';
else if (phase === 'checking') buttonState = 'checking';
else if (phase === 'checked' && allFailed) buttonState = 'failed';
else buttonState = 'idle';
const ringColor = ({
idle: t.accent,
checking: t.accent,
active: t.accent,
'active-warn': t.warn,
failed: t.danger,
})[buttonState];
const headline = ({
idle: 'Tap to connect',
checking: 'Verifying…',
active: 'Discord is protected',
'active-warn': 'Connected · UDP fallback',
failed: 'Connection failed',
})[buttonState];
const subline = ({
idle: `via ${D.form.host}`,
checking: `running 7 checks…`,
active: `via ${D.form.host} · ${window.fmtUptime(D.stats.uptimeS)}`,
'active-warn': `voice/screen disabled · ${window.fmtUptime(D.stats.uptimeS)}`,
failed: `${failed} of ${D.tests.length} checks failed`,
})[buttonState];
return (
{/* Title bar */}
Drover-Go
0.4.2
{phase === 'active' && }
{phase === 'idle' && 'idle'}
{phase === 'checking' && 'checking…'}
{phase === 'checked' && (allFailed ? 'failed' : 'ready')}
{phase === 'active' && (warning ? 'connected · warn' : 'connected')}
{/* Big button + headlines */}
{/* Stats — only when active */}
{isActive && (
)}
{/* Diagnostic mini-list during check or after */}
{(phase === 'checking' || (phase === 'checked' && !isActive)) && (
{D.tests.map((test) => {
const r = D.results[test.id];
const state = r?.result || (D.running === test.id ? 'running' : 'pending');
return (
{test.label}
{r?.metric || (state === 'running' ? '…' : '')}
{r?.result === 'failed' && (
)}
{r?.result === 'failed' && r.expanded && (
)}
);
})}
)}
{/* Bottom: form (idle) or footer chips (active) */}
{showForm && (
)}
{/* Logs strip */}
{showDetails && (
el && (el.scrollTop = el.scrollHeight)}>
{D.logs.map((l, i) => (
{window.fmtTime(l.t)}{' '}
[{l.level}]{' '}
{l.msg}
))}
)}
);
}
// ─── Big toggle button ──────────────────────────────────────────────────
function BigToggle({ t, state, onClick, progress = 0 }) {
// size = 168 px circle; outer pulsing rings
const isActive = state === 'active' || state === 'active-warn';
const isChecking = state === 'checking';
const isFailed = state === 'failed';
const isWarn = state === 'active-warn';
const accent = isWarn ? t.warn : isFailed ? t.danger : t.accent;
const accentDeep = isWarn ? t.warn : isFailed ? t.danger : t.accentDeep;
const accentDarker = isWarn ? t.warn : isFailed ? t.danger : t.accentDarker;
const ring = 168;
const stroke = 5;
const r = (ring - stroke) / 2;
const c = 2 * Math.PI * r;
return (
);
}
function HeroStat({ icon, v, u, c, mono, t }) {
return (
);
}
function HeroTitleBtn({ children, t, hoverBg }) {
const [hover, setHover] = React.useState(false);
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{
width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', borderRadius: 5,
background: hover ? (hoverBg || 'rgba(127,127,127,.16)') : 'transparent',
}}>{children}
);
}
function HeroInput({ t, fontMono, value, onChange, onEnter, placeholder, label, style, type, disabled, id }) {
const [focus, setFocus] = React.useState(false);
return (
);
}
function fmtCompact(n) {
if (n < 1024) return n.toFixed(0);
if (n < 1024 * 1024) return (n / 1024).toFixed(0);
return (n / 1024 / 1024).toFixed(1);
}
function fmtUnit(n) {
if (n < 1024) return 'B/s';
if (n < 1024 * 1024) return 'KB/s';
return 'MB/s';
}
window.HeroWindow = HeroWindow;