// drover-wizard-live.jsx — Wizard / stepper variant.
// 3 steps: Configure → Verify → Connect. State derives from D.phase.
// Step 1 = idle (form). Step 2 = checking/checked (diagnostics). Step 3 = active (running proxy).
const WIZARD_THEME = {
l: {
bg: '#fafafa',
chrome: '#ffffff',
panel: '#ffffff',
panelAlt: '#f6f7f9',
border: '#ececec',
borderHard:'#dcdcdc',
text: '#1a1a1a',
dim: '#666666',
dimmer: '#9aa0a6',
accent: '#1e6fd9',
accentSoft:'#eef4fc',
danger: '#c0463f',
warn: '#a8731e',
pass: '#21a655',
skip: '#888888',
},
d: {
bg: '#0f1115',
chrome: '#171a20',
panel: '#1a1d23',
panelAlt: '#13151a',
border: '#2a2d33',
borderHard:'#3a3d45',
text: '#e6e8ec',
dim: '#9aa0a6',
dimmer: '#5d6168',
accent: '#5b9bf0',
accentSoft:'#1a253a',
danger: '#e57373',
warn: '#d9a155',
pass: '#5cba8b',
skip: '#7c8088',
},
};
function WizardWindow({ mode = 'light', initial }) {
const themeKey = mode === 'dark' ? 'd' : 'l';
const t = WIZARD_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";
// Determine current step from phase
const phase = D.phase;
const step = phase === 'idle' ? 1
: (phase === 'checking' || phase === 'checked') ? 2
: 3; // active
return (
{/* Stepper */}
{[[1, 'Configure'], [2, 'Verify'], [3, 'Connect']].map(([n, label], i) => {
const done = step > n;
const current = step === n;
const dim = step < n;
return (
{done ? '✓' : n}
{label}
{i < 2 && n ? t.accent : t.borderHard,
margin: '0 10px', transition: 'background .2s',
}} />}
);
})}
{/* Step content */}
{step === 1 && }
{step === 2 && }
{step === 3 && }
{/* Footer */}
{
if (step === 2) D.stopProxy?.(), D.setPhase('idle');
if (step === 3) D.stopProxy();
}} disabled={step === 1}>
{step === 3 ? 'Disconnect' : 'Back'}
{
if (step === 1) D.runCheck();
else if (step === 2) D.startProxy();
}}
disabled={
(step === 1 && (D.phase === 'checking')) ||
(step === 2 && (D.phase === 'checking' || D.lastSummary?.failed === D.tests.length)) ||
(step === 3)
}>
{step === 1 && 'Verify →'}
{step === 2 && (D.phase === 'checking' ? 'Verifying…' : 'Connect →')}
{step === 3 && 'Connected'}
);
}
function WizardTitleBar({ t }) {
return (
);
}
function WizTitleBtn({ children, t, hoverBg, hoverFg }) {
const [hover, setHover] = React.useState(false);
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{ width: 38, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer',
background: hover ? (hoverBg || 'rgba(127,127,127,.1)') : 'transparent',
color: hover && hoverFg ? hoverFg : 'inherit',
borderRadius: 4,
}}>{children}
);
}
// ─── Step 1: Configure ───────────────────────────────────────────────────
function WizConfigure({ t, D, fontMono }) {
const inputStyle = (disabled) => ({
height: 36, background: t.panel, color: disabled ? t.dimmer : t.text,
border: `1px solid ${t.borderHard}`, borderRadius: 6, padding: '0 12px',
fontFamily: fontMono, fontSize: 13, outline: 'none', width: '100%', boxSizing: 'border-box',
transition: 'border-color .12s, box-shadow .12s',
});
const onFocus = (e) => { e.target.style.borderColor = t.accent; e.target.style.boxShadow = `0 0 0 3px ${t.accentSoft}`; };
const onBlur = (e) => { e.target.style.borderColor = t.borderHard; e.target.style.boxShadow = 'none'; };
return (
<>
Configure your proxy
Введите адрес SOCKS5-сервера. На следующем шаге мы проверим, что Discord будет работать через него.
i
Нажмите Verify → , чтобы продолжить.
>
);
}
// ─── Step 2: Verify ───────────────────────────────────────────────────
function WizVerify({ t, D, fontMono, palette, themeKey }) {
const phase = D.phase;
const total = D.tests.length;
const completed = Object.keys(D.results).length;
const failed = D.lastSummary?.failed ?? 0;
const ringFrac = phase === 'checked' ? 1 : completed / total;
const ringDash = 163.4;
return (
<>
{phase === 'checking' ? 'Verifying your proxy' : (failed === 0 ? 'All checks passed' : 'Some checks failed')}
{phase === 'checking'
? <>Запускаем 7 проверок против {D.form.host}:{D.form.port} >
: (failed === 0
? <>Прокси работает. Discord — голос, чат и демонстрация — будет работать через него.>
: <>{failed} из {total} проверок не прошли. Часть функций работать не будет.>)}
{/* progress + ring */}
0 ? t.danger : phase === 'checked' ? t.pass : t.accent}
strokeWidth="6" strokeLinecap="round"
strokeDasharray={ringDash}
strokeDashoffset={ringDash * (1 - ringFrac)}
transform="rotate(-90 32 32)"
style={{ transition: 'stroke-dashoffset .35s, stroke .2s' }} />
{phase === 'checked' ? `${total - failed}/${total}` : `${completed}/${total}`}
{phase === 'checking' ? <>Testing {D.tests.find(x => x.id === D.running)?.label || '…'} > : (failed === 0 ? 'Готово к подключению' : 'Завершено с ошибками')}
{phase === 'checking' ? 'This usually takes 5–15 seconds.' : 'См. отчёт ниже.'}
{/* test list */}
{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' ? 'running…' : '')}
{r?.result === 'failed' && (
D.toggleExpand(test.id)}
style={{ width: 20, height: 20, padding: 0, border: 'none', background: 'transparent', cursor: 'pointer' }}>
)}
{r?.result === 'failed' && r.expanded && (
{r.error}
{r.hint}
navigator.clipboard?.writeText(`[${test.label}] ${r.error} — ${r.metric}`)}
style={{ marginTop: 6, padding: '3px 8px', fontSize: 11, fontFamily: fontMono,
background: t.panel, color: t.dim, border: `1px solid ${t.borderHard}`,
borderRadius: 4, cursor: 'pointer' }}>copy error
)}
);
})}
>
);
}
// ─── Step 3: Connect (active) ───────────────────────────────────────────
function WizConnect({ t, D, fontMono }) {
const stats = D.stats;
const failed = D.lastSummary?.failed ?? 0;
return (
<>
0 ? t.warn : t.pass,
display: 'inline-block',
}} />
{failed > 0 ? 'Connected · UDP fallback' : 'Connected'}
Discord routes through {D.form.host}:{D.form.port}
{' · '}{window.fmtUptime(stats.uptimeS)}
{/* stats grid */}
0 ? t.warn : t.pass} />
{/* logs panel inline */}
RECENT LOG
navigator.clipboard?.writeText(D.logs.map(l => `[${l.level}] ${l.msg}`).join('\n'))}
style={{ marginLeft: 'auto', padding: '2px 8px', fontSize: 11, fontFamily: fontMono,
background: 'transparent', color: t.dim, border: `1px solid ${t.borderHard}`,
borderRadius: 4, cursor: 'pointer' }}>copy
clear
el && (el.scrollTop = el.scrollHeight)}>
{D.logs.slice(-30).map((l, i) => (
{window.fmtTime(l.t)}
{' '}
[{l.level}]
{' '}
{l.msg}
))}
>
);
}
function WizStat({ t, fontMono, label, value, accent }) {
return (
);
}
function WizardPrimaryBtn({ t, onClick, disabled, children }) {
const [hover, setHover] = React.useState(false);
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{
height: 32, padding: '0 18px', borderRadius: 6, border: 'none',
background: disabled ? t.panelAlt : (hover ? '#1a5fbf' : t.accent),
color: disabled ? t.dimmer : '#fff',
fontWeight: 600, fontSize: 13, cursor: disabled ? 'not-allowed' : 'pointer',
boxShadow: disabled ? 'none' : 'inset 0 -1px 0 rgba(0,0,0,.18)',
}}>{children}
);
}
function WizardSecondaryBtn({ t, onClick, disabled, children }) {
const [hover, setHover] = React.useState(false);
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{
height: 32, padding: '0 14px', borderRadius: 6,
background: hover && !disabled ? t.panelAlt : t.panel,
color: disabled ? t.dimmer : t.text,
border: `1px solid ${t.borderHard}`,
fontSize: 13, cursor: disabled ? 'not-allowed' : 'pointer',
}}>{children}
);
}
window.WizardWindow = WizardWindow;