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:
@@ -424,6 +424,41 @@ textarea {
|
||||
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 {
|
||||
display: inline-block;
|
||||
|
||||
@@ -93,6 +93,17 @@ const state: WizardState = {
|
||||
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) ---
|
||||
|
||||
/// 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');
|
||||
if (!app) return;
|
||||
|
||||
const progressHtml = `
|
||||
<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>
|
||||
`;
|
||||
const progressHtml = renderProgressTrack(state.step);
|
||||
|
||||
let stepHtml = '';
|
||||
switch (state.step) {
|
||||
@@ -224,6 +226,7 @@ function renderStep0(): string {
|
||||
</p>
|
||||
<div class="mode-cards">
|
||||
<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>
|
||||
<p class="mode-card-blurb">
|
||||
I'm setting up Relicario for the first time. This will create a fresh
|
||||
@@ -231,6 +234,7 @@ function renderStep0(): string {
|
||||
</p>
|
||||
</button>
|
||||
<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>
|
||||
<p class="mode-card-blurb">
|
||||
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 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 `
|
||||
<div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
|
||||
<div class="success-box">
|
||||
@@ -992,6 +1012,8 @@ function renderStep5(): string {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
${qrBannerHtml}
|
||||
|
||||
${isAttach ? '' : `
|
||||
<div class="form-group">
|
||||
<label class="label">reference image</label>
|
||||
@@ -1026,6 +1048,48 @@ function renderStep5(): string {
|
||||
}
|
||||
|
||||
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', () => {
|
||||
if (!state.referenceImageBytes) return;
|
||||
const blob = new Blob([state.referenceImageBytes.buffer as ArrayBuffer], { type: 'image/jpeg' });
|
||||
|
||||
Reference in New Issue
Block a user