peach_demo_update_spec.md
Peach Website Demo Update Spec
Target: peachpasswords.com — HeroDemo.astro Extension version: 1.4.12 Goal: make the website demo show the real extension as faithfully as possible
1. Overview
The live demo at peachpasswords.com renders a simulated 320×520px popup shell directly in the browser. It currently shows a subset of the real popup's surfaces and uses some placeholder or outdated data/markup. This document is a complete spec for bringing the demo to parity with the real extension's v1.4.12 state.
The implementation approach is:
- Copy the compiled CSS from
popup.css+vars.cssinto the demo as-is (single source of truth). - Rebuild the demo HTML surfaces to match the real component markup exactly.
- Wire up the demo's click/hover interactivity to show as many surfaces as possible.
- Use realistic sample data that tells a story about Peach's capabilities.
2. CSS strategy (highest priority)
The demo currently has its own CSS. Replace it with a direct copy of the extension's compiled CSS:
src/popup/styles/vars.css → embedded in <style> in HeroDemo.astro
src/popup/styles/popup.css → embedded in <style> in HeroDemo.astro
Do not maintain a separate CSS copy. Instead add a build step (or Astro integration) that reads both files from the extension workspace and injects them at build time. This guarantees the demo never drifts from the real product.
The shell wrapper the website uses should be:
<div class="popup-root peach-theme" id="hd-root">
<!-- surfaces go here -->
</div>
Override only one thing at the demo level: scale the popup root down so it fits the marketing page layout:
#hd-root {
transform: scale(0.88);
transform-origin: top center;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 32px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.06);
}
3. Fonts
The real extension self-hosts fonts from public/fonts/. The website should reference the same files or load them from a CDN. Required faces:
| Family | Weights used | Role |
|---|---|---|
| Cormorant Garamond | 300 italic, 400 italic | Display headings (lock screen, entry name) |
| IBM Plex Mono | 400 | Metadata, labels, mono values, TOTP code |
| Inter Variable | 300, 400, 500 | UI body text |
The @font-face declarations in vars.css already handle this. Copy them verbatim.
4. Demo surface inventory
Show the following surfaces. Each has a tab/button in the demo chrome that reveals it. The demo should start on Surface A (unlocked entry detail) — this is the most impressive screen and what a visitor should see first.
| ID | Surface | Entry point |
|---|---|---|
| A | Unlocked — Entry detail (GitHub) | Default / "detail" button |
| B | Unlocked — Entry detail (Stripe, with TOTP) | Click "Stripe" in search bar |
| C | Vault list (with sort + tab-match badge) | Click search bar input |
| D | Add new entry | "+" button in header |
| E | Settings | Gear icon in header |
| F | Vault Health | From Settings → "open" |
| G | Lock screen (unlock) | Lock icon in header |
| H | Lock screen (onboarding / new vault) | Toggle inside demo |
The passkey approval, CJK export/import, sync pairing, and export surfaces can be shown as static screenshots below the interactive demo rather than as interactive surfaces (they are powerful features but too branched for a simple demo).
5. Sample data
Use this fixed vault for the demo. It demonstrates all entry types and features:
const DEMO_ENTRIES = [
{
id: "demo-1",
type: "login",
title: "GitHub",
url: "https://github.com",
username: "juni_patel",
password: "p#9Kx!mQ3rL@vZ",
totp: null,
tags: ["work", "dev"],
breach: 0,
createdAt: Date.now() - 86400000 * 90,
modifiedAt: Date.now() - 86400000 * 14,
},
{
id: "demo-2",
type: "login",
title: "Stripe",
url: "https://dashboard.stripe.com",
username: "[email protected]",
password: "Tr!p0d$2024#Acme",
totp: "JBSWY3DPEHPK3PXP", // shows TOTP band
tags: ["work"],
breach: 0,
createdAt: Date.now() - 86400000 * 120,
modifiedAt: Date.now() - 86400000 * 3,
},
{
id: "demo-3",
type: "login",
title: "Netflix",
url: "https://netflix.com",
username: "[email protected]",
password: "password123", // deliberately weak — shows breach badge
tags: ["personal"],
breach: 54231, // shows breach count
createdAt: Date.now() - 86400000 * 400,
modifiedAt: Date.now() - 86400000 * 400,
},
{
id: "demo-4",
type: "passkey",
title: "Apple ID",
rpId: "apple.com",
userName: "juni_patel",
userDisplayName: "Juni Patel",
tags: ["personal"],
createdAt: Date.now() - 86400000 * 30,
modifiedAt: Date.now() - 86400000 * 30,
},
{
id: "demo-5",
type: "note",
title: "Server SSH keys",
notes: "prod: ssh -i ~/.ssh/peach_prod [email protected]\nstaging: ssh -i ~/.ssh/peach_stage [email protected]",
tags: ["dev"],
createdAt: Date.now() - 86400000 * 60,
modifiedAt: Date.now() - 86400000 * 7,
},
{
id: "demo-6",
type: "login",
title: "Gmail",
url: "https://mail.google.com",
username: "[email protected]",
password: "Gm4!l$ecure#88",
tags: ["personal"],
breach: 0,
canary: { label: "honeypot", webhookUrl: "https://hooks.example.com/canary" }, // shows canary badge
createdAt: Date.now() - 86400000 * 200,
modifiedAt: Date.now() - 86400000 * 45,
}
];
The tab domain for the demo should be simulated as github.com — this causes "GitHub" to show the tab-match orange dot in the vault list.
6. Surface A — Entry detail (GitHub, login)
This is the default surface. HTML structure matches Entry.tsx exactly:
<section id="hd-shell" class="peach-shell peach-theme" data-surface="extension">
<!-- Top chrome -->
<header class="peach-chrome">
<div class="peach-wordmark-wrap">
<!-- PeachWordmark SVG with unlocked=true (no pip hidden class) -->
<svg class="peach-brand-mark" ...></svg>
<span class="peach-wordmark-text">peach</span>
<span class="peach-status-pip unlocked"></span>
</div>
<div class="peach-chrome-actions">
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-btn-settings" aria-label="Settings">
<!-- gear SVG -->
</button>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-btn-add" aria-label="New entry">
<!-- + SVG -->
</button>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-lock-btn" id="hd-btn-lock" aria-label="Lock vault">
<!-- padlock SVG -->
</button>
</div>
</header>
<!-- Content frame (entry detail) -->
<div class="peach-shell-body">
<div class="peach-shell-veil"></div>
<div class="peach-content-frame" data-content-phase="idle">
<!-- Entry detail -->
<div class="peach-ghost"></div>
<div class="peach-stage">
<!-- Entry name -->
<div class="peach-entry-name-wrap">
<button class="peach-entry-name-button">
<h1 class="peach-entry-name" aria-label="GitHub">
<span class="peach-entry-name-text">
<!-- Each char wrapped: -->
<span class="peach-entry-name-char" style="--char-index:0">G</span>
<span class="peach-entry-name-char" style="--char-index:1">i</span>
<span class="peach-entry-name-char" style="--char-index:2">t</span>
<span class="peach-entry-name-char" style="--char-index:3">H</span>
<span class="peach-entry-name-char" style="--char-index:4">u</span>
<span class="peach-entry-name-char" style="--char-index:5">b</span>
</span>
</h1>
</button>
</div>
<!-- Domain + tags -->
<div class="peach-domain">
<div class="peach-domain-row">
<!-- favicon img or login icon fallback -->
<img src="https://icons.duckduckgo.com/ip3/github.com.ico" width="16" height="16" style="border-radius:3px" alt="">
<span class="peach-domain-value">github.com</span>
</div>
<!-- Tags -->
<span class="peach-tag-chip" style="font-size:10px;padding:2px 8px;">work</span>
<span class="peach-tag-chip" style="font-size:10px;padding:2px 8px;">dev</span>
</div>
<!-- Fields section -->
<section class="peach-fields">
<!-- Username field -->
<div class="peach-field-card">
<span class="peach-label">username</span>
<div class="peach-field-value-row">
<span class="peach-field-value">juni_patel</span>
<div class="peach-field-actions">
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn" id="hd-copy-username">
<span class="peach-copy-icon">
<!-- copy SVG -->
</span>
<span class="peach-copy-sheen"></span>
</button>
</div>
</div>
</div>
<!-- Password field -->
<div class="peach-field-card">
<span class="peach-label">password</span>
<div class="peach-field-value-row">
<div class="peach-password-value">
<span class="peach-password-mask">...............</span>
</div>
<div class="peach-field-actions">
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-reveal-password" aria-label="Reveal password">
<!-- eye SVG -->
</button>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn" id="hd-copy-password">
<span class="peach-copy-icon"><!-- copy SVG --></span>
<span class="peach-copy-sheen"></span>
</button>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- Search bar -->
<section class="peach-search-wrap">
<div class="peach-search-bar">
<span class="peach-search-icon"><!-- search SVG --></span>
<input class="peach-search-input" placeholder="search vault..." readonly id="hd-search-input">
<div class="peach-search-meta">
<span class="peach-search-counter">1 / 6</span>
<span class="peach-divider"></span>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-prev"><!-- chevron left --></button>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-next"><!-- chevron right --></button>
</div>
</div>
</section>
</div>
</section>
A — Interactive behaviors
| Action | Effect |
|---|---|
| Click copy (username) | peach-copy-btn gets .is-copied for 1.5s; drain animation plays |
| Click copy (password) | Same copy animation |
| Click reveal | Toggle .peach-password-mask ↔ .peach-password-text content |
| Click prev/next arrows | Cycle through DEMO_ENTRIES; re-render entry name chars with --char-index delay |
| Click search bar | Show Surface C (vault list overlay) |
| Click settings gear | Show Surface E |
| Click "+" | Show Surface D |
| Click lock | Show Surface G |
7. Surface B — Entry detail (Stripe, with TOTP)
Same structure as A but with:
- Entry name: "Stripe"
- Domain:
dashboard.stripe.com - Username:
[email protected] - Tags:
work - TOTP band rendered below the password field:
<section class="peach-totp-band">
<div class="peach-totp-main">
<span class="peach-label">2-factor</span>
<div class="peach-totp-code-row">
<p class="peach-totp-code">
<span class="peach-totp-code-text">482 917</span>
</p>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn peach-totp-copy-btn">
<span class="peach-copy-icon"><!-- copy SVG --></span>
<span class="peach-copy-sheen"></span>
</button>
</div>
</div>
<div class="peach-totp-meta">
<span class="peach-label">expires</span>
<div class="peach-totp-countdown" id="hd-totp-remaining">18s</div>
<svg class="peach-totp-ring" width="44" height="44" viewBox="0 0 44 44">
<circle cx="22" cy="22" r="17" class="peach-totp-ring-track" />
<circle cx="22" cy="22" r="17" class="peach-totp-ring-progress"
style="stroke-dasharray: 106.8; stroke-dashoffset: 35.6;" />
</svg>
</div>
<div class="peach-totp-progress">
<div class="peach-totp-progress-bar" style="width: 60%;"></div>
</div>
</section>
Animate the TOTP ring/countdown in the demo: use a JavaScript interval to decrement a fake countdown from 30→0, updating stroke-dashoffset and the countdown text. Reset to 30 and generate a new fake 6-digit code when it hits 0. This makes the demo feel alive.
Surface B is reached by clicking "Stripe" in the vault list (Surface C).
8. Surface C — Vault list
<div class="peach-overlay-view ov-enter-up" id="hd-view-list" style="display:flex">
<!-- Top bar with back button + search -->
<div class="peach-list-top">
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-list-close">
<!-- chevron left -->
</button>
<input class="peach-search-input" placeholder="search vault..." style="font-size:14px" id="hd-list-search">
<span class="peach-search-counter peach-search-counter-list">6 entries</span>
</div>
<!-- Sort bar (new in 1.4.12) -->
<div class="peach-sort-bar">
<button class="peach-sort-btn is-active">smart</button>
<button class="peach-sort-btn">A–Z</button>
<button class="peach-sort-btn">Z–A</button>
<button class="peach-sort-btn">newest</button>
<span class="peach-sort-domain-hint">github.com</span>
</div>
<!-- Entry list -->
<div class="peach-vault-list">
<!-- GitHub — tab match (is-tab-match) -->
<button class="peach-entry-row is-active is-tab-match" style="animation-delay:0ms">
<div class="peach-entry-row-icon">
<img src="https://icons.duckduckgo.com/ip3/github.com.ico" width="16" height="16" style="border-radius:3px" alt="">
</div>
<div class="peach-entry-row-info">
<span class="peach-entry-row-name">
GitHub
<span class="peach-tab-match-badge" title="Matches current tab">●</span>
</span>
<span class="peach-entry-row-meta">github.com</span>
</div>
</button>
<!-- Stripe -->
<button class="peach-entry-row" style="animation-delay:20ms" id="hd-list-stripe">
<div class="peach-entry-row-icon">
<img src="https://icons.duckduckgo.com/ip3/dashboard.stripe.com.ico" width="16" height="16" style="border-radius:3px" alt="">
</div>
<div class="peach-entry-row-info">
<span class="peach-entry-row-name">Stripe</span>
<span class="peach-entry-row-meta">dashboard.stripe.com</span>
</div>
</button>
<!-- Netflix — breached -->
<button class="peach-entry-row" style="animation-delay:40ms">
<div class="peach-entry-row-icon is-breach">
<!-- login icon SVG -->
<svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">
<rect x="2" y="5" width="7" height="5" rx="1.2"/>
<path d="M3.5 5V3.75A2 2 0 0 1 5.5 1.75A2 2 0 0 1 7.5 3.75V5"/>
</svg>
</div>
<div class="peach-entry-row-info">
<span class="peach-entry-row-name">Netflix</span>
<span class="peach-entry-row-meta">netflix.com</span>
</div>
<span class="peach-entry-row-dot" aria-label="breach"></span>
</button>
<!-- Apple ID — passkey -->
<button class="peach-entry-row" style="animation-delay:60ms">
<div class="peach-entry-row-icon">
<!-- passkey icon SVG -->
<svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">
<circle cx="4" cy="4.5" r="3"/>
<path d="M6.5 7l3.5 3.5M8.5 8.5L10 10"/>
</svg>
</div>
<div class="peach-entry-row-info">
<span class="peach-entry-row-name">
Apple ID
<span class="peach-passkey-badge">passkey</span>
</span>
<span class="peach-entry-row-meta">apple.com</span>
</div>
</button>
<!-- Server SSH keys — note -->
<button class="peach-entry-row" style="animation-delay:80ms">
<div class="peach-entry-row-icon">
<!-- note icon SVG -->
<svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">
<rect x="1.5" y="1.5" width="8" height="9" rx="1.5"/>
<path d="M3.5 4.5h4M3.5 6.5h4M3.5 8.5h2"/>
</svg>
</div>
<div class="peach-entry-row-info">
<span class="peach-entry-row-name">Server SSH keys</span>
<span class="peach-entry-row-meta">prod: ssh -i ~/.ssh/p...</span>
</div>
</button>
<!-- Gmail — canary -->
<button class="peach-entry-row" style="animation-delay:100ms">
<div class="peach-entry-row-icon">
<img src="https://icons.duckduckgo.com/ip3/mail.google.com.ico" width="16" height="16" style="border-radius:3px" alt="">
</div>
<div class="peach-entry-row-info">
<span class="peach-entry-row-name">Gmail</span>
<span class="peach-entry-row-meta">mail.google.com</span>
</div>
<span class="peach-entry-row-canary" title="Canary: honeypot">🪤</span>
</button>
</div>
<!-- New entry row -->
<button class="peach-new-entry-row" id="hd-list-add">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round">
<path d="M5 1v8M1 5h8"/>
</svg>
new entry
</button>
</div>
C — Interactive behaviors
| Action | Effect |
|---|---|
| Type in search input | Filter rows client-side with JS (simple substring match on title/domain) |
| Click sort button | Toggle .is-active between sort buttons; demo doesn't need to re-sort, just show the UI working |
| Click "GitHub" row | Close list, show Surface A |
| Click "Stripe" row | Close list, show Surface B (with TOTP) |
| Click "Netflix" row | Close list, show breach version of entry detail (see §9) |
| Click "new entry" | Show Surface D |
| Click back / Esc | Close list, return to current entry detail |
9. Breach entry detail (Netflix)
Same structure as Surface A, but with the breach strip rendered:
<!-- Breach badge — above the entry name -->
<div class="peach-breach-badge">
<span>⚠ 54,231 data breach exposures</span>
<button class="peach-btn peach-btn--sm peach-btn--accent peach-breach-action">change password</button>
</div>
Also the icon shows with is-breach class applied.
10. Surface D — Add new entry
<div class="peach-overlay-view ov-enter-right" id="hd-view-new" style="display:flex">
<div class="peach-overlay-header">
<button class="peach-btn peach-btn--subtle peach-btn--sm peach-overlay-action" id="hd-new-cancel">cancel</button>
<span class="peach-overlay-title">new entry</span>
<button class="peach-btn peach-btn--accent peach-btn--sm peach-overlay-action" id="hd-new-create" disabled>create</button>
</div>
<div class="peach-new-body">
<!-- Type tabs -->
<div class="peach-type-tabs">
<button class="peach-type-tab is-active" id="hd-tab-login">login</button>
<button class="peach-type-tab" id="hd-tab-note">note</button>
</div>
<!-- Title input -->
<input class="peach-new-name-input" placeholder="entry name" id="hd-new-title">
<!-- URL row -->
<div class="peach-domain peach-new-domain">
<input class="peach-domain-value peach-new-field-input" placeholder="https://..." id="hd-new-url">
</div>
<!-- Fields section -->
<section class="peach-fields peach-new-fields">
<div class="peach-field-card">
<span class="peach-label">username</span>
<div class="peach-field-value-row">
<input class="peach-field-value peach-new-field-input" type="text" placeholder="username or email" id="hd-new-username" autocomplete="off">
</div>
</div>
<div class="peach-field-card">
<span class="peach-label">password</span>
<div class="peach-field-value-row">
<input class="peach-field-value peach-new-field-input" type="password" placeholder="password" id="hd-new-password" autocomplete="new-password">
<!-- Inline generator button -->
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-new-generate" title="Generate password">
<!-- dice/sparkle SVG -->
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">
<rect x="1.5" y="1.5" width="10" height="10" rx="2"/>
<circle cx="4.5" cy="4.5" r="1" fill="currentColor" stroke="none"/>
<circle cx="8.5" cy="8.5" r="1" fill="currentColor" stroke="none"/>
<circle cx="4.5" cy="8.5" r="1" fill="currentColor" stroke="none"/>
<circle cx="8.5" cy="4.5" r="1" fill="currentColor" stroke="none"/>
<circle cx="6.5" cy="6.5" r="1" fill="currentColor" stroke="none"/>
</svg>
</button>
</div>
<!-- Strength hairline (shown when password has content) -->
<div class="peach-strength-hairline" id="hd-strength-bar" style="display:none">
<div class="peach-strength-hairline-track">
<div class="peach-strength-hairline-fill" id="hd-strength-fill" style="width:0%"></div>
</div>
<span class="peach-strength-label" id="hd-strength-label"></span>
</div>
</div>
<div class="peach-field-card">
<span class="peach-label">totp secret</span>
<div class="peach-field-value-row">
<input class="peach-field-value peach-new-field-input" type="text" placeholder="otpauth:// or base32" id="hd-new-totp" autocomplete="off">
</div>
</div>
<div class="peach-field-card">
<span class="peach-label">notes</span>
<div class="peach-field-value-row">
<textarea class="peach-new-textarea" rows="3" placeholder="optional notes" id="hd-new-notes"></textarea>
</div>
</div>
</section>
<!-- Tags -->
<div class="peach-field-card" style="margin-top:12px">
<span class="peach-label">tags</span>
<div class="peach-tag-input-wrap">
<input class="peach-tag-input" placeholder="add tags..." id="hd-new-tags">
</div>
<div class="peach-tag-suggestions" style="margin-top:4px">
<button class="peach-tag-suggestion">work</button>
<button class="peach-tag-suggestion">personal</button>
<button class="peach-tag-suggestion">dev</button>
</div>
</div>
</div>
</div>
D — Interactive behaviors
| Action | Effect |
|---|---|
| Click "note" tab | Toggle tab active state; hide url/username/password/totp fields; show note textarea |
| Click "login" tab | Toggle back; restore login fields |
| Type in password field | Show strength bar; update fill width and label text (simple heuristic OK in demo) |
| Click generate button | Fill password field with a random 16-char password; trigger strength bar update |
| All fields filled | Enable "create" button |
| Click "create" | Briefly show "saving…" then return to entry detail with a success animation |
| Click "cancel" | Close overlay, return to entry detail |
11. Surface E — Settings
<div class="peach-overlay-view ov-enter-right" id="hd-view-settings" style="display:flex">
<div class="peach-overlay-header">
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-settings-back"><!-- chevron --></button>
<span class="peach-overlay-title">settings</span>
<div class="peach-overlay-spacer"></div>
</div>
<div class="peach-settings-body">
<!-- Security group -->
<div class="peach-settings-group">
<p class="peach-settings-label">security</p>
<div class="peach-settings-row">
<span>Lock on close</span>
<button class="peach-toggle is-on"><span class="peach-toggle-knob"></span></button>
</div>
<div class="peach-settings-row">
<span>Breach monitoring</span>
<button class="peach-toggle is-on"><span class="peach-toggle-knob"></span></button>
</div>
<div class="peach-settings-row peach-settings-row--meta">
<span>Check all passwords</span>
<button class="peach-btn peach-btn--subtle peach-btn--sm">run</button>
</div>
<div class="peach-settings-row peach-settings-row--meta">
<span>Add canary entry</span>
<button class="peach-btn peach-btn--subtle peach-btn--sm">open</button>
</div>
</div>
<!-- Duress password group (new in 1.4.12) -->
<div class="peach-settings-group">
<p class="peach-settings-label">duress password</p>
<div class="peach-settings-row peach-settings-row--top">
<div class="peach-settings-row-main">
<span>Duress password</span>
<span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">
If entered under coercion, shows a decoy empty vault instead of your real one.
</span>
</div>
<div class="peach-duress-status">off</div>
</div>
<div class="peach-settings-row peach-settings-row--meta">
<button class="peach-btn peach-btn--subtle peach-btn--sm">Set up</button>
</div>
</div>
<!-- Passkeys group -->
<div class="peach-settings-group">
<p class="peach-settings-label">passkeys</p>
<div class="peach-settings-row peach-settings-row--top">
<div class="peach-settings-row-main">
<span>Intercept passkey requests</span>
<span class="peach-settings-value" style="font-size:11px;color:var(--t2)">
Peach can handle passkey sign-in and creation on supported Chromium browsers.
</span>
</div>
<button class="peach-toggle is-on"><span class="peach-toggle-knob"></span></button>
</div>
<div class="peach-settings-row peach-settings-row--meta">
<span>Status</span>
<span class="peach-settings-value">Active</span>
</div>
</div>
<!-- Sync group — show Pro gate -->
<div class="peach-settings-group" style="position:relative">
<p class="peach-settings-label">sync</p>
<div class="peach-sync-pro-gate">
<div class="peach-sync-pro-gate-blur">
<div class="peach-settings-row">
<span>Status</span>
<span class="peach-settings-value peach-settings-value--status">
<span class="peach-sync-status-dot" style="background:var(--peach-sync-off)"></span>
Offline
</span>
</div>
</div>
<div class="peach-sync-pro-gate-overlay">
<span class="peach-sync-pro-badge">Pro</span>
<span style="font-size:13px;color:rgba(255,255,255,0.7)">Sync across all your devices</span>
<a href="https://peachpasswords.com/#pricing" target="_blank" rel="noopener noreferrer"
class="peach-link" style="color:#D96830;font-size:13px">Upgrade to Pro →</a>
</div>
</div>
</div>
<!-- Vault group -->
<div class="peach-settings-group">
<p class="peach-settings-label">vault</p>
<div class="peach-settings-row peach-settings-row--meta">
<span>Entries</span>
<span class="peach-settings-value">6</span>
</div>
<div class="peach-settings-row peach-settings-row--meta">
<span>Theme</span>
<button class="peach-btn peach-btn--subtle peach-btn--sm" id="hd-settings-theme">light</button>
</div>
<div class="peach-settings-row peach-settings-row--meta">
<span>Vault Health</span>
<button class="peach-btn peach-btn--subtle peach-btn--sm" id="hd-settings-health">open</button>
</div>
</div>
<!-- Footer -->
<div class="peach-settings-footer">
<span>Peach 1.4.12</span>
<span class="peach-divider"></span>
<span>Browser extension</span>
</div>
</div>
</div>
E — Interactive behaviors
| Action | Effect |
|---|---|
| Toggle "Lock on close" | Toggle .is-on class on button |
| Toggle "Breach monitoring" | Toggle .is-on |
| Toggle "Intercept passkey requests" | Toggle .is-on |
| Click "Theme" | Switch demo between dark/light by toggling .light on #hd-root |
| Click "Vault Health" → open | Show Surface F |
| Click back | Close settings, return to entry detail |
12. Surface F — Vault Health
<div class="peach-overlay-view ov-enter-right" id="hd-view-health" style="display:flex">
<div class="peach-overlay-header">
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" id="hd-health-back"><!-- chevron --></button>
<span class="peach-overlay-title">vault health</span>
<div class="peach-overlay-spacer"></div>
</div>
<div class="peach-health-body">
<div class="peach-health-summary">
<span class="peach-health-summary-warn">2 entries need attention</span>
</div>
<div class="peach-health-check-row">
<button class="peach-btn peach-btn--accent peach-btn--sm peach-health-check-btn">
Check all breaches now
</button>
</div>
<div class="peach-health-sections">
<!-- Breached section (1 entry) -->
<div class="peach-health-section">
<button class="peach-health-section-header">
<span class="peach-health-section-title">Breached</span>
<span class="peach-health-section-count">1</span>
<svg width="8" height="5" viewBox="0 0 8 5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
<path d="M1 1l3 3 3-3"/>
</svg>
</button>
<div class="peach-health-section-body">
<div class="peach-health-row">
<div class="peach-health-row-info">
<span class="peach-health-row-name">Netflix</span>
<span class="peach-health-row-meta" style="color:var(--peach-breach)">54,231 exposures</span>
</div>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-health-fix-btn">Fix</button>
</div>
</div>
</div>
<!-- Reused section (0) -->
<div class="peach-health-section">
<button class="peach-health-section-header">
<span class="peach-health-section-title">Reused</span>
<span class="peach-health-section-count">0</span>
<svg width="8" height="5" viewBox="0 0 8 5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
<path d="M1 1l3 3 3-3"/>
</svg>
</button>
<div class="peach-health-section-body" style="display:none">
<div class="peach-list-empty"><span>No reused passwords</span></div>
</div>
</div>
<!-- Weak section (1 entry — Netflix) -->
<div class="peach-health-section">
<button class="peach-health-section-header">
<span class="peach-health-section-title">Weak</span>
<span class="peach-health-section-count">1</span>
<svg width="8" height="5" viewBox="0 0 8 5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
<path d="M1 1l3 3 3-3"/>
</svg>
</button>
<div class="peach-health-section-body">
<div class="peach-health-row">
<div class="peach-health-row-info">
<span class="peach-health-row-name">netflix.com</span>
<span class="peach-health-row-meta">score: 0 (very weak)</span>
</div>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-health-fix-btn">Fix</button>
</div>
</div>
</div>
<!-- Old section (0) -->
<div class="peach-health-section">
<button class="peach-health-section-header">
<span class="peach-health-section-title">Old</span>
<span class="peach-health-section-count">1</span>
<svg width="8" height="5" viewBox="0 0 8 5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
<path d="M1 1l3 3 3-3"/>
</svg>
</button>
<div class="peach-health-section-body">
<div class="peach-health-row">
<div class="peach-health-row-info">
<span class="peach-health-row-name">Netflix</span>
<span class="peach-health-row-meta">last changed: over 1 year ago</span>
</div>
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-health-fix-btn">Fix</button>
</div>
</div>
</div>
</div>
</div>
</div>
F — Interactive behaviors
| Action | Effect |
|---|---|
| Click section header | Collapse/expand that section body (toggle display:none) |
| Click "Check all breaches" | Brief "checking…" label then return to "2 entries need attention" |
| Click back | Return to Settings |
13. Surface G — Lock screen (unlock)
<section class="peach-screen peach-theme lock-screen lock-screen--vault-stage" id="hd-view-lock">
<header class="peach-screen-header lock-screen-header">
<!-- PeachWordmark -->
<div class="peach-wordmark-wrap">
<svg class="peach-brand-mark" ...></svg>
<span class="peach-wordmark-text">peach</span>
</div>
</header>
<div class="peach-lock-hero peach-lock-hero--unlock">
<div class="peach-lock-hero-art" aria-hidden="true">
<!-- PeachMark SVG (large, faint background mark) -->
</div>
<span class="peach-lock-eyebrow">vault locked</span>
<h1 class="peach-onboarding-headline peach-lock-headline--unlock">
unlock<br>your vault.
</h1>
<p class="peach-subline peach-subline--lock">
Everything stays local and encrypted until you unlock.
</p>
</div>
<section class="peach-lock-panel peach-lock-panel--unlock">
<label class="peach-label" for="hd-unlock-password">master password</label>
<input id="hd-unlock-password"
class="peach-underline-input peach-unlock-input lock-password-input"
type="password"
placeholder="master password"
autocomplete="current-password">
<button class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit" id="hd-unlock-btn" disabled>
unlock
</button>
<p class="peach-hint">encrypted vault · stored locally</p>
</section>
</section>
G — Interactive behaviors
| Action | Effect |
|---|---|
| Type in password field | Enable "unlock" button when field is non-empty |
| Click "unlock" | Show brief "unlocking…" state then transition back to Surface A with spring animation |
| Toggle button inside demo | Show Surface H (onboarding/new vault) instead |
14. Surface H — Lock screen (onboarding / new vault)
<section class="peach-screen peach-theme lock-screen lock-screen--vault-stage lock-screen--onboarding" id="hd-view-onboarding">
<header class="peach-screen-header lock-screen-header">
<div class="peach-wordmark-wrap"><!-- PeachWordmark --></div>
</header>
<div class="peach-lock-hero">
<div class="peach-lock-hero-art" aria-hidden="true"><!-- PeachMark --></div>
<span class="peach-lock-eyebrow">new vault</span>
<h1 class="peach-onboarding-headline">
your secrets,<br>kept close.
</h1>
<p class="peach-subline peach-subline--lock">
Create a master password to encrypt your vault. Or restore from an existing backup.
</p>
</div>
<section class="peach-lock-panel peach-lock-panel--primary">
<label class="peach-label" for="hd-create-pw">master password</label>
<input id="hd-create-pw" class="peach-underline-input peach-unlock-input lock-password-input"
type="password" placeholder="master password" autocomplete="new-password">
<label class="peach-label" for="hd-create-confirm">confirm password</label>
<input id="hd-create-confirm" class="peach-underline-input peach-unlock-input"
type="password" placeholder="confirm password" autocomplete="new-password">
<!-- Strength bar (new in 1.4.12 — uses shared peach-password-strength classes) -->
<div class="peach-password-strength" id="hd-create-strength" style="display:none">
<div class="peach-strength-bar peach-strength-bar--fair">
<div class="peach-strength-fill"></div>
</div>
<span class="peach-strength-label">fair</span>
</div>
<button class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit" id="hd-create-btn" disabled>
create vault
</button>
<p class="peach-hint">If you lose this password, Peach cannot recover your vault.</p>
<div class="peach-lock-panel-footer">
<p class="peach-hint">Already have a backup?</p>
<div class="peach-onboarding-alt-actions">
<button class="peach-lock-restore-link">restore from CJK phrase</button>
<button class="peach-lock-restore-link">import from backup file</button>
</div>
</div>
</section>
</section>
H — Interactive behaviors
| Action | Effect |
|---|---|
| Type in master password | Show strength bar; animate fill; update label (weak/fair/good/strong) |
| Both fields match, ≥8 chars | Enable "create vault" button |
| Click "create vault" | Show "creating…" then transition to Surface A |
15. JavaScript interactivity sketch
All demo interactivity lives in a single <script> tag at the bottom of HeroDemo.astro. Keep it simple — no framework.
// Surface routing
const surfaces = {
detail: document.getElementById('hd-shell'),
list: document.getElementById('hd-view-list'),
add: document.getElementById('hd-view-new'),
settings: document.getElementById('hd-view-settings'),
health: document.getElementById('hd-view-health'),
lock: document.getElementById('hd-view-lock'),
onboarding: document.getElementById('hd-view-onboarding'),
};
let activeSurface = 'detail';
function show(id) {
// Add ov-enter-up / ov-enter-right class on show to trigger animation
Object.values(surfaces).forEach(s => s && (s.style.display = 'none'));
const el = surfaces[id];
if (el) {
el.style.display = 'flex';
// Restart animation
el.classList.remove('ov-enter-up', 'ov-enter-right');
void el.offsetWidth;
if (id === 'list') el.classList.add('ov-enter-up');
else if (['add','settings','health'].includes(id)) el.classList.add('ov-enter-right');
activeSurface = id;
}
}
// Copy animation
function animateCopy(btn) {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}
// TOTP countdown
let totpInterval = null;
let totpRemaining = 18;
function startTotpCountdown() {
clearInterval(totpInterval);
totpInterval = setInterval(() => {
totpRemaining--;
if (totpRemaining <= 0) {
totpRemaining = 30;
// Generate new fake code
const newCode = String(Math.floor(100000 + Math.random() * 900000));
const formatted = newCode.slice(0,3) + ' ' + newCode.slice(3);
document.getElementById('hd-totp-code')?.textContent = formatted;
}
const el = document.getElementById('hd-totp-remaining');
if (el) el.textContent = totpRemaining + 's';
// Update ring
const circumference = 106.8;
const progress = totpRemaining / 30;
const offset = circumference * (1 - progress);
document.querySelector('.peach-totp-ring-progress')
?.style.setProperty('stroke-dashoffset', offset);
document.querySelector('.peach-totp-progress-bar')
?.style.setProperty('width', (progress * 100) + '%');
}, 1000);
}
// Password strength (simple heuristic for demo)
function getStrength(pw) {
if (!pw) return null;
let s = 0;
if (pw.length >= 8) s++;
if (/[A-Z]/.test(pw) && /[a-z]/.test(pw)) s++;
if (/\d/.test(pw)) s++;
if (/[^A-Za-z0-9]/.test(pw)) s++;
const labels = ['weak','fair','fair','good','strong'];
const widths = ['20%','40%','55%','75%','100%'];
return { label: labels[s], width: widths[s], cls: labels[s] };
}
// Wire up all the event handlers here...
// (full handler list per section above)
16. Feature callouts beneath the demo
Below the interactive popup, add a row of feature callout chips that reinforce what was just seen. Each chip links to the relevant section on the page:
<div class="hd-feature-chips">
<span class="hd-chip">🔐 AES-256-GCM encryption</span>
<span class="hd-chip">🛡 HIBP breach monitoring</span>
<span class="hd-chip">🗝 Passkeys (WebAuthn)</span>
<span class="hd-chip">🟠 Canary honeypots</span>
<span class="hd-chip">🈶 CJK phrase backup</span>
<span class="hd-chip">🔄 Peach Sync (Pro)</span>
<span class="hd-chip">📊 Vault Health</span>
<span class="hd-chip">💡 Duress password</span>
</div>
17. Static screenshot surfaces
The following surfaces are too branched to demo interactively but should appear as static annotated screenshots in a "More features" section or carousel on the page:
- Passkey approval — the pop-up that appears when a site requests a passkey (
save a passkey?/sign in with passkey?) - CJK export — the 2,000+ character Chinese Ideograph backup phrase output
- CJK import — the phrase validation flow with HIBP check
- Sync pairing — the 6-digit pairing code UI
- Autofill in-page — screenshot of the injected Peach icon on a login field + identity-helper dropdown
- Duress password setup — the settings flow for configuring the decoy vault
Produce these screenshots by loading the real built extension in a screenshot-capable Chrome instance.
18. Accessibility requirements
The demo is focusable and keyboard-navigable:
- All interactive buttons have
aria-labelattributes - Copy confirmation is announced via
aria-live="polite"region - Vault list rows are
<button>elements (already the case in real code) - Password reveal toggle updates
aria-labelto reflect state ("Reveal password" / "Hide password") - TOTP countdown is wrapped in
aria-live="polite"so screen readers can announce refresh
19. Sync checklist — keeping demo current
After every extension release, verify these items before shipping the website update:
- [ ] CSS — re-copy
vars.cssandpopup.cssfrom extension source - [ ] Version number in Surface E footer
- [ ] Settings group order matches real Settings.tsx (currently: security → duress → passkeys → sync → vault → license)
- [ ] Sort bar labels match real Vault.tsx (
smart,A–Z,Z–A,newest) - [ ] Strength bar classes used in Surface H match real Lock.tsx (
peach-strength-bar--weak/fair/good/strong) - [ ] Feature chip list reflects any new features added in the release
- [ ] TOTP code animation ring physics (
stroke-dasharray: 106.8matchesr=17, circumference = 2π×17 ≈ 106.8)