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;
|
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;
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
Reference in New Issue
Block a user