feat(ext/setup): wizard Style C progress track, glyph mode icons, recovery QR banner

- Replace dot-based progress indicator with colored horizontal segment track
  (completed=green, active=gold, pending=border) via renderProgressTrack()
- Add SETUP_STEP_NAMES constant for track segment titles
- Update Step 0 mode cards with glyph icons (◈ create, ⌥ attach)
- Add recovery QR banner in Step 5 (new-vault only, verifiedHandle present)
  with Generate now / Skip buttons wired in attachStep5()
- Add CSS for .setup-progress-track, .setup-progress-segment variants,
  and .recovery-qr-banner to styles.css

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-03 21:17:05 -04:00
parent f17944a404
commit 33d2a4a311
2 changed files with 109 additions and 10 deletions

View File

@@ -424,6 +424,41 @@ textarea {
background: #aa812a; background: #aa812a;
} }
/* Setup wizard — Style C progress track */
.setup-progress-track {
display: flex;
gap: 4px;
width: 100%;
max-width: 560px;
margin: 8px auto 16px;
}
.setup-progress-segment {
flex: 1;
height: 4px;
border-radius: 2px;
}
.setup-progress-segment--completed { background: var(--success, #238636); }
.setup-progress-segment--active { background: var(--gold, #b8860b); }
.setup-progress-segment--pending { background: var(--border, #30363d); }
/* Setup wizard — Recovery QR banner */
.recovery-qr-banner {
padding: 12px 16px;
background: var(--bg-elevated, #161b22);
border: 1px solid var(--gold, #b8860b);
border-radius: 8px;
}
.recovery-qr-banner__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.recovery-qr-banner__actions {
display: flex;
gap: 8px;
}
/* Spinner */ /* Spinner */
.spinner { .spinner {
display: inline-block; display: inline-block;

View File

@@ -93,6 +93,17 @@ const state: WizardState = {
deviceName: '', deviceName: '',
}; };
// --- Progress track ---
const SETUP_STEP_NAMES = ['mode', 'host', 'connection', 'vault', 'device', 'done'];
function renderProgressTrack(current: number): string {
return `<div class="setup-progress-track">${SETUP_STEP_NAMES.map((_, i) => {
const cls = i < current ? 'completed' : i === current ? 'active' : 'pending';
return `<div class="setup-progress-segment setup-progress-segment--${cls}" title="${SETUP_STEP_NAMES[i]}"></div>`;
}).join('')}</div>`;
}
// --- State-coupled helpers (pure helpers live in ./setup-helpers.ts) --- // --- State-coupled helpers (pure helpers live in ./setup-helpers.ts) ---
/// Update just the meter DOM without a full re-render (so the input keeps /// Update just the meter DOM without a full re-render (so the input keeps
@@ -168,16 +179,7 @@ function render(): void {
const app = document.getElementById('app'); const app = document.getElementById('app');
if (!app) return; if (!app) return;
const progressHtml = ` const progressHtml = renderProgressTrack(state.step);
<div class="progress-bar">
<div class="step ${state.step > 0 ? 'done' : state.step === 0 ? 'current' : ''}"></div>
<div class="step ${state.step > 1 ? 'done' : state.step === 1 ? 'current' : ''}"></div>
<div class="step ${state.step > 2 ? 'done' : state.step === 2 ? 'current' : ''}"></div>
<div class="step ${state.step > 3 ? 'done' : state.step === 3 ? 'current' : ''}"></div>
<div class="step ${state.step > 4 ? 'done' : state.step === 4 ? 'current' : ''}"></div>
<div class="step ${state.step > 5 ? 'done' : state.step === 5 ? 'current' : ''}"></div>
</div>
`;
let stepHtml = ''; let stepHtml = '';
switch (state.step) { switch (state.step) {
@@ -224,6 +226,7 @@ function renderStep0(): string {
</p> </p>
<div class="mode-cards"> <div class="mode-cards">
<button class="mode-card glass ${isNew ? 'active' : ''}" data-mode="new"> <button class="mode-card glass ${isNew ? 'active' : ''}" data-mode="new">
<span class="mode-card__icon" style="font-size:28px;">◈</span>
<div class="mode-card-title">create new vault</div> <div class="mode-card-title">create new vault</div>
<p class="mode-card-blurb"> <p class="mode-card-blurb">
I'm setting up Relicario for the first time. This will create a fresh I'm setting up Relicario for the first time. This will create a fresh
@@ -231,6 +234,7 @@ function renderStep0(): string {
</p> </p>
</button> </button>
<button class="mode-card glass ${isAttach ? 'active' : ''}" data-mode="attach"> <button class="mode-card glass ${isAttach ? 'active' : ''}" data-mode="attach">
<span class="mode-card__icon" style="font-size:28px;">⌥</span>
<div class="mode-card-title">attach this device</div> <div class="mode-card-title">attach this device</div>
<p class="mode-card-blurb"> <p class="mode-card-blurb">
I already have a vault on another device. Connect this browser to it I already have a vault on another device. Connect this browser to it
@@ -981,6 +985,22 @@ function renderStep5(): string {
const configJson = JSON.stringify(config, null, 2); const configJson = JSON.stringify(config, null, 2);
const isAttach = state.mode === 'attach'; const isAttach = state.mode === 'attach';
const qrBannerHtml = (!isAttach && state.verifiedHandle !== null) ? `
<div class="recovery-qr-banner" id="recovery-qr-banner" style="margin-bottom:16px;">
<div class="recovery-qr-banner__header">
<span style="font-size:20px;">◫</span>
<strong>Generate a recovery QR before you go</strong>
</div>
<p class="muted" style="font-size:12px;margin:4px 0 8px;">
If you lose your reference image, this QR lets you recover your vault. Print it and store it safely.
</p>
<div class="recovery-qr-banner__actions">
<button class="btn btn-primary" id="setup-gen-qr">Generate now</button>
<button class="btn" id="setup-skip-qr">Skip — I'll do this in Settings</button>
</div>
</div>
` : '';
return ` return `
<div class="wizard-step glass" style="padding: 24px; margin-top: 16px;"> <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
<div class="success-box"> <div class="success-box">
@@ -992,6 +1012,8 @@ function renderStep5(): string {
</p> </p>
</div> </div>
${qrBannerHtml}
${isAttach ? '' : ` ${isAttach ? '' : `
<div class="form-group"> <div class="form-group">
<label class="label">reference image</label> <label class="label">reference image</label>
@@ -1026,6 +1048,48 @@ function renderStep5(): string {
} }
function attachStep5(): void { function attachStep5(): void {
document.getElementById('setup-gen-qr')?.addEventListener('click', async () => {
if (!state.verifiedHandle) return;
const btn = document.getElementById('setup-gen-qr') as HTMLButtonElement | null;
if (btn) { btn.disabled = true; btn.textContent = 'Generating…'; }
try {
const { sendMessage } = await import('../shared/state');
const resp = await sendMessage({
type: 'generate_recovery_qr',
sessionHandle: state.verifiedHandle.value,
passphrase: state.passphrase,
} as any) as any;
if (!resp.ok || !resp.data) throw new Error(resp.error ?? 'unknown error');
const svg = (resp.data as { svg: string }).svg;
await new Promise<void>((resolve) => {
chrome.storage.local.set({ recovery_qr_generated_at: Date.now() }, resolve);
});
const banner = document.getElementById('recovery-qr-banner');
if (banner) {
banner.innerHTML = `
<div style="text-align:center;">${svg}</div>
<p style="font-size:12px;color:var(--success,#238636);margin:8px 0 0;">
◉ Recovery QR generated — save or print this now.
</p>
<div style="margin-top:8px;">
<button class="btn" id="setup-qr-done">Done</button>
</div>
`;
document.getElementById('setup-qr-done')?.addEventListener('click', () => {
banner.style.display = 'none';
});
}
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = 'Generate now'; }
alert(`Failed to generate QR: ${err instanceof Error ? err.message : String(err)}`);
}
});
document.getElementById('setup-skip-qr')?.addEventListener('click', () => {
const banner = document.getElementById('recovery-qr-banner');
if (banner) banner.style.display = 'none';
});
document.getElementById('download-ref-btn')?.addEventListener('click', () => { document.getElementById('download-ref-btn')?.addEventListener('click', () => {
if (!state.referenceImageBytes) return; if (!state.referenceImageBytes) return;
const blob = new Blob([state.referenceImageBytes.buffer as ArrayBuffer], { type: 'image/jpeg' }); const blob = new Blob([state.referenceImageBytes.buffer as ArrayBuffer], { type: 'image/jpeg' });