113616b039
Release / release (push) Successful in 3m19s
Stash the full claude.ai/design output (12 JSX variants — brutalist, classic, cli, compact, fluent-live, glass, hero-live, minimal, sketches, studio, wizard-live, workshop — plus shared hooks and a standalone HTML preview) for reference when we get to the Wails frontend in Phase 6/7. Source archive: C:\Users\root\Downloads\app(1).zip (~1MB). Not wired into any build target yet — current GUI is the temporary MessageBox stub. Pulling these in is the goal of the Wails phase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
456 lines
21 KiB
React
456 lines
21 KiB
React
// 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 (
|
||
<div style={{
|
||
width: 480, height: 640, background: t.bg, color: t.text,
|
||
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
||
fontFamily: fontUI, fontSize: 13.5, lineHeight: 1.45,
|
||
borderRadius: 0,
|
||
}}>
|
||
<WizardTitleBar t={t} />
|
||
|
||
{/* Stepper */}
|
||
<div style={{
|
||
display: 'flex', padding: '14px 18px', alignItems: 'center',
|
||
borderBottom: `1px solid ${t.border}`, background: t.chrome, gap: 0,
|
||
}}>
|
||
{[[1, 'Configure'], [2, 'Verify'], [3, 'Connect']].map(([n, label], i) => {
|
||
const done = step > n;
|
||
const current = step === n;
|
||
const dim = step < n;
|
||
return (
|
||
<React.Fragment key={n}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<span style={{
|
||
width: 22, height: 22, borderRadius: '50%',
|
||
background: dim ? t.panelAlt : t.accent,
|
||
border: dim ? `1px solid ${t.borderHard}` : 'none',
|
||
color: dim ? t.dimmer : '#fff',
|
||
fontSize: 11, fontWeight: 600,
|
||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||
transition: 'background .2s',
|
||
}}>{done ? '✓' : n}</span>
|
||
<span style={{ fontSize: 12.5, fontWeight: current ? 600 : 400, color: dim ? t.dimmer : t.text }}>{label}</span>
|
||
</div>
|
||
{i < 2 && <div style={{
|
||
flex: 1, height: 1, background: step > n ? t.accent : t.borderHard,
|
||
margin: '0 10px', transition: 'background .2s',
|
||
}} />}
|
||
</React.Fragment>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Step content */}
|
||
<div style={{ flex: 1, overflow: 'auto', padding: '18px 22px', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||
{step === 1 && <WizConfigure t={t} D={D} fontMono={fontMono} />}
|
||
{step === 2 && <WizVerify t={t} D={D} fontMono={fontMono} palette={palette} themeKey={themeKey} />}
|
||
{step === 3 && <WizConnect t={t} D={D} fontMono={fontMono} />}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div style={{
|
||
height: 54, display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
|
||
gap: 8, padding: '0 18px', borderTop: `1px solid ${t.border}`, background: t.chrome,
|
||
}}>
|
||
<WizardSecondaryBtn t={t} onClick={() => {
|
||
if (step === 2) D.stopProxy?.(), D.setPhase('idle');
|
||
if (step === 3) D.stopProxy();
|
||
}} disabled={step === 1}>
|
||
{step === 3 ? 'Disconnect' : 'Back'}
|
||
</WizardSecondaryBtn>
|
||
<WizardPrimaryBtn t={t}
|
||
onClick={() => {
|
||
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'}
|
||
</WizardPrimaryBtn>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function WizardTitleBar({ t }) {
|
||
return (
|
||
<div style={{
|
||
height: 32, display: 'flex', alignItems: 'center', padding: '0 12px',
|
||
borderBottom: `1px solid ${t.border}`, fontSize: 12, color: t.text, background: t.chrome,
|
||
}}>
|
||
<window.BrandMark size={14} color={t.accent} />
|
||
<span style={{ marginLeft: 8, fontWeight: 600 }}>Drover-Go · Setup</span>
|
||
<div style={{ marginLeft: 'auto', display: 'flex' }}>
|
||
<WizTitleBtn t={t}><window.IconGear color={t.dim} /></WizTitleBtn>
|
||
<WizTitleBtn t={t}><window.IconMin color={t.dim} /></WizTitleBtn>
|
||
<WizTitleBtn t={t} hoverBg="#c0463f" hoverFg="#fff"><window.IconClose color={t.dim} /></WizTitleBtn>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
function WizTitleBtn({ children, t, hoverBg, hoverFg }) {
|
||
const [hover, setHover] = React.useState(false);
|
||
return (
|
||
<div onMouseEnter={() => 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}</div>
|
||
);
|
||
}
|
||
|
||
// ─── 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 (
|
||
<>
|
||
<div>
|
||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>Configure your proxy</h2>
|
||
<div style={{ fontSize: 12.5, color: t.dim, marginTop: 4 }}>
|
||
Введите адрес SOCKS5-сервера. На следующем шаге мы проверим, что Discord будет работать через него.
|
||
</div>
|
||
</div>
|
||
<div style={{
|
||
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
|
||
padding: 16, display: 'flex', flexDirection: 'column', gap: 12,
|
||
}}>
|
||
<div style={{ display: 'flex', gap: 10 }}>
|
||
<label style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Host</span>
|
||
<input value={D.form.host}
|
||
onChange={e => D.update({ host: e.target.value })}
|
||
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||
onFocus={onFocus} onBlur={onBlur}
|
||
placeholder="95.165.72.59 или example.com"
|
||
style={inputStyle(false)} />
|
||
</label>
|
||
<label style={{ width: 100, display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Port</span>
|
||
<input value={D.form.port}
|
||
onChange={e => D.update({ port: e.target.value.replace(/\D/g,'') })}
|
||
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||
onFocus={onFocus} onBlur={onBlur}
|
||
placeholder="12334" inputMode="numeric"
|
||
style={inputStyle(false)} />
|
||
</label>
|
||
</div>
|
||
|
||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 9, cursor: 'pointer', userSelect: 'none', fontSize: 13 }}>
|
||
<span style={{
|
||
width: 18, height: 18, borderRadius: 4,
|
||
border: `1.5px solid ${D.form.auth ? t.accent : t.borderHard}`,
|
||
background: D.form.auth ? t.accent : 'transparent',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
}}>
|
||
{D.form.auth && <svg width="11" height="11" viewBox="0 0 11 11"><path d="M2 5.5l2.5 2.5 4.5-5" stroke="#fff" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>}
|
||
</span>
|
||
<input type="checkbox" checked={D.form.auth}
|
||
onChange={e => { D.update({ auth: e.target.checked }); if (e.target.checked) setTimeout(() => document.getElementById('wiz-login')?.focus(), 30); }}
|
||
style={{ display: 'none' }} />
|
||
<span>Authentication</span>
|
||
</label>
|
||
|
||
<div style={{ display: 'flex', gap: 10, opacity: D.form.auth ? 1 : 0.4, pointerEvents: D.form.auth ? 'auto' : 'none' }}>
|
||
<label style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Login</span>
|
||
<input id="wiz-login" disabled={!D.form.auth} value={D.form.login}
|
||
onChange={e => D.update({ login: e.target.value })}
|
||
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||
onFocus={onFocus} onBlur={onBlur}
|
||
placeholder="user" style={inputStyle(!D.form.auth)} />
|
||
</label>
|
||
<label style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 500 }}>Password</span>
|
||
<input disabled={!D.form.auth} type="password" value={D.form.password}
|
||
onChange={e => D.update({ password: e.target.value })}
|
||
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||
onFocus={onFocus} onBlur={onBlur}
|
||
placeholder="••••••" style={inputStyle(!D.form.auth)} />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div style={{ fontSize: 12, color: t.dim, marginTop: 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<span style={{
|
||
width: 14, height: 14, borderRadius: '50%', border: `1px solid ${t.borderHard}`,
|
||
fontSize: 9, color: t.dim, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||
}}>i</span>
|
||
Нажмите <span style={{ color: t.text, fontWeight: 600 }}>Verify →</span>, чтобы продолжить.
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
// ─── 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 (
|
||
<>
|
||
<div>
|
||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
|
||
{phase === 'checking' ? 'Verifying your proxy' : (failed === 0 ? 'All checks passed' : 'Some checks failed')}
|
||
</h2>
|
||
<div style={{ fontSize: 12.5, color: t.dim, marginTop: 4 }}>
|
||
{phase === 'checking'
|
||
? <>Запускаем 7 проверок против <span style={{ fontFamily: fontMono, color: t.text }}>{D.form.host}:{D.form.port}</span></>
|
||
: (failed === 0
|
||
? <>Прокси работает. Discord — голос, чат и демонстрация — будет работать через него.</>
|
||
: <>{failed} из {total} проверок не прошли. Часть функций работать не будет.</>)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* progress + ring */}
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', gap: 16, padding: 14,
|
||
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
|
||
}}>
|
||
<svg width="64" height="64" viewBox="0 0 64 64">
|
||
<circle cx="32" cy="32" r="26" fill="none" stroke={t.border} strokeWidth="6" />
|
||
<circle cx="32" cy="32" r="26" fill="none"
|
||
stroke={failed > 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' }} />
|
||
<text x="32" y="36" textAnchor="middle" fontSize="13" fontWeight="600" fill={t.text}>
|
||
{phase === 'checked' ? `${total - failed}/${total}` : `${completed}/${total}`}
|
||
</text>
|
||
</svg>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontWeight: 600, fontSize: 13.5 }}>
|
||
{phase === 'checking' ? <>Testing <span style={{ fontFamily: fontMono }}>{D.tests.find(x => x.id === D.running)?.label || '…'}</span></> : (failed === 0 ? 'Готово к подключению' : 'Завершено с ошибками')}
|
||
</div>
|
||
<div style={{ fontSize: 12, color: t.dim, marginTop: 2 }}>
|
||
{phase === 'checking' ? 'This usually takes 5–15 seconds.' : 'См. отчёт ниже.'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* test list */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, fontSize: 12.5 }}>
|
||
{D.tests.map((test) => {
|
||
const r = D.results[test.id];
|
||
const state = r?.result || (D.running === test.id ? 'running' : 'pending');
|
||
return (
|
||
<div key={test.id}>
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', gap: 9, padding: '5px 10px',
|
||
background: D.running === test.id ? t.accentSoft : 'transparent',
|
||
borderRadius: 4, minHeight: 26,
|
||
}}>
|
||
<window.StatusDot state={state} palette={palette} size={12} />
|
||
<span style={{ color: state === 'pending' ? t.dimmer : t.text }} title={test.desc}>{test.label}</span>
|
||
<span style={{ marginLeft: 'auto', fontFamily: fontMono, fontSize: 11,
|
||
color: state === 'failed' ? t.danger : state === 'skipped' ? t.skip : t.dim }}>
|
||
{r?.metric || (state === 'running' ? 'running…' : '')}
|
||
</span>
|
||
{r?.result === 'failed' && (
|
||
<button onClick={() => D.toggleExpand(test.id)}
|
||
style={{ width: 20, height: 20, padding: 0, border: 'none', background: 'transparent', cursor: 'pointer' }}>
|
||
<window.IconChevron color={t.dim} dir={r.expanded ? 'up' : 'down'} />
|
||
</button>
|
||
)}
|
||
</div>
|
||
{r?.result === 'failed' && r.expanded && (
|
||
<div className="drv-fadein" style={{
|
||
margin: '2px 0 4px 22px', padding: '8px 10px', borderRadius: 6,
|
||
background: themeKey === 'l' ? '#fdf2f2' : '#3a1f1f',
|
||
border: `1px solid ${t.danger}`, fontSize: 12,
|
||
}}>
|
||
<div style={{ color: t.danger, fontWeight: 600, marginBottom: 2 }}>{r.error}</div>
|
||
<div style={{ color: t.dim }}>{r.hint}</div>
|
||
<button onClick={() => 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</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
// ─── Step 3: Connect (active) ───────────────────────────────────────────
|
||
function WizConnect({ t, D, fontMono }) {
|
||
const stats = D.stats;
|
||
const failed = D.lastSummary?.failed ?? 0;
|
||
return (
|
||
<>
|
||
<div>
|
||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<span className="drv-pulsedot" style={{
|
||
width: 10, height: 10, borderRadius: 5, background: failed > 0 ? t.warn : t.pass,
|
||
display: 'inline-block',
|
||
}} />
|
||
{failed > 0 ? 'Connected · UDP fallback' : 'Connected'}
|
||
</h2>
|
||
<div style={{ fontSize: 12.5, color: t.dim, marginTop: 4 }}>
|
||
Discord routes through <span style={{ fontFamily: fontMono, color: t.text }}>{D.form.host}:{D.form.port}</span>
|
||
{' · '}<span style={{ fontFamily: fontMono }}>{window.fmtUptime(stats.uptimeS)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* stats grid */}
|
||
<div style={{
|
||
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8,
|
||
}}>
|
||
<WizStat t={t} fontMono={fontMono} label="Upload" value={window.fmtBytes(stats.up)} accent={t.accent} />
|
||
<WizStat t={t} fontMono={fontMono} label="Download" value={window.fmtBytes(stats.down)} accent={t.pass} />
|
||
<WizStat t={t} fontMono={fontMono} label="TCP" value={stats.tcp} accent={t.accent} />
|
||
<WizStat t={t} fontMono={fontMono} label="UDP" value={stats.udp} accent={failed > 0 ? t.warn : t.pass} />
|
||
</div>
|
||
|
||
{/* logs panel inline */}
|
||
<div style={{
|
||
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
|
||
padding: 12, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column',
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: 11.5, color: t.dim, fontWeight: 600, letterSpacing: 0.2 }}>RECENT LOG</span>
|
||
<button onClick={() => 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</button>
|
||
<button onClick={D.clearLogs}
|
||
style={{ marginLeft: 6, padding: '2px 8px', fontSize: 11, fontFamily: fontMono,
|
||
background: 'transparent', color: t.dim, border: `1px solid ${t.borderHard}`,
|
||
borderRadius: 4, cursor: 'pointer' }}>clear</button>
|
||
</div>
|
||
<div className="drv-log" style={{
|
||
flex: 1, overflowY: 'auto', fontFamily: fontMono, fontSize: 11,
|
||
lineHeight: 1.55, color: t.dim, minHeight: 0,
|
||
}} ref={el => el && (el.scrollTop = el.scrollHeight)}>
|
||
{D.logs.slice(-30).map((l, i) => (
|
||
<div key={i}>
|
||
<span style={{ color: t.dimmer }}>{window.fmtTime(l.t)}</span>
|
||
{' '}
|
||
<span style={{ color: l.level === 'ERROR' ? t.danger : l.level === 'WARN' ? t.warn : t.pass, fontWeight: 600 }}>[{l.level}]</span>
|
||
{' '}
|
||
<span>{l.msg}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function WizStat({ t, fontMono, label, value, accent }) {
|
||
return (
|
||
<div style={{
|
||
background: t.panel, border: `1px solid ${t.border}`, borderRadius: 8,
|
||
padding: '10px 12px',
|
||
}}>
|
||
<div style={{ fontSize: 10.5, color: t.dim, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: 600 }}>{label}</div>
|
||
<div style={{ fontSize: 18, fontWeight: 600, fontFamily: fontMono, marginTop: 3, color: accent }}>{value}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function WizardPrimaryBtn({ t, onClick, disabled, children }) {
|
||
const [hover, setHover] = React.useState(false);
|
||
return (
|
||
<button onClick={onClick} disabled={disabled}
|
||
onMouseEnter={() => 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}</button>
|
||
);
|
||
}
|
||
function WizardSecondaryBtn({ t, onClick, disabled, children }) {
|
||
const [hover, setHover] = React.useState(false);
|
||
return (
|
||
<button onClick={onClick} disabled={disabled}
|
||
onMouseEnter={() => 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}</button>
|
||
);
|
||
}
|
||
|
||
window.WizardWindow = WizardWindow;
|