# Peach — Every Surface Faithful to the Demo _v1.4.1 target_ --- ## Purpose Every popup view has been read in full. This spec prescribes the exact changes needed to make every surface — including the newer ones (Passkeys, Health, Sync, Tags, Canary) that postdate the website demo — feel like they were designed together. The goal is visual confidence and coherence: one system, no escaped inline styles, no unstyled improvised surfaces. The demo's design language is established and non-negotiable: - **Cormorant Garamond** (italic, weight 300–400) for display text and numbers - **IBM Plex Mono** (weight 300–400) for codes, passwords, mono values - **Inter Variable** for all UI chrome, labels, buttons - Amber bone palette: `--peach-accent: #D96830`, `--t0: #F0E8D8`, `--t1`, `--t2`, `--t3` scale - Zero `border-radius` on the popup root shell; small radii (6–12px) inside surfaces only - Muted amber glow on interactive elements; no harsh white or saturated colors - All copy lowercase or sentence-case — never ALL-CAPS labels except in `.peach-label` --- ## Files Modified | File | Type | |------|------| | `src/popup/styles/popup.css` | CSS additions + fixes | | `src/popup/views/Entry.tsx` | TOTP ring trigger; regen panel CSS classes | | `src/popup/views/PasskeyApproval.tsx` | Full inline-style → CSS class migration | | `src/popup/views/PasskeyEntry.tsx` | `window.confirm()` replacement; badge CSS | | `src/popup/views/Vault.tsx` | Passkey badge CSS class | | `src/popup/views/Settings.tsx` | `.peach-info-notice`, sync row CSS, color literal fix | | `src/popup/views/Export.tsx` | Input class fix | | `src/popup/views/CJKExport.tsx` | Inline style → CSS class migration | | `src/popup/views/CJKImport.tsx` | Inline style → CSS class migration | | `src/popup/views/Lock.tsx` | Restore link wrapper CSS class | | `src/popup/components/TOTP.tsx` | Class rename to `peach-` prefix | **No changes to `detect.ts`, `inject.ts`, `fill.ts`, `save.ts`, background files, or the Rust sync host.** --- ## Issue 1 — TOTP font: missing `font-style: italic` ### Root cause `popup.css` declares: ```css .peach-totp-code { font-family: "Cormorant Garamond", "Times New Roman", serif; font-size: 38px; color: var(--peach-accent); line-height: 0.95; min-height: 1em; } ``` No `font-style` is set, so the browser renders Cormorant Garamond Regular (upright). The demo showed the code in **italic** — the defining characteristic of Peach's display numerals. Everything else that uses Cormorant Garamond (`.peach-entry-name`, `.peach-unlock-headline`, `.peach-wordmark`, the entry-row name) explicitly declares `font-style: italic`. The TOTP code is the only exception, making it look like a mistake. ### Fix — `popup.css` ```css /* BEFORE */ .peach-totp-code { margin: 2px 0 0; font-family: "Cormorant Garamond", "Times New Roman", serif; font-size: 38px; color: var(--peach-accent); line-height: 0.95; min-height: 1em; } /* AFTER */ .peach-totp-code { margin: 2px 0 0; font-family: "Cormorant Garamond", "Times New Roman", serif; font-style: italic; font-weight: 300; font-size: 38px; color: var(--peach-accent); line-height: 0.95; min-height: 1em; } ``` `font-weight: 300` (Light Italic) matches the `CormorantGaramond-LightItalic.woff2` loaded in `vars.css` and gives the numerals the same airy quality as the large display heading. --- ## Issue 2 — TOTP ring does not fire on code refresh ### Root cause `Ring.tsx` exposes `trigger()` via `(window as any).__peachRingTrigger`. `triggerRing()` in `App.tsx` calls this, and it fires correctly on copy. But Entry.tsx never calls it when the TOTP period resets — the ring only glows when you manually copy the code, not at the natural moment of anticipation (the new code appearing). ### Fix — `Entry.tsx` Add a `createEffect` that watches the TOTP code string itself. When the code changes after the initial render, trigger the ring. This fires once per 30-second period, at the exact instant the new code appears. ```tsx // After the existing totpState memo, add: // Trigger the accent ring every time the TOTP code changes (new period) let prevTotpCode: string | undefined; createEffect(() => { const state = totpState(); if (!state) { prevTotpCode = undefined; return; } if (prevTotpCode !== undefined && state.code !== prevTotpCode) { triggerRing(); } prevTotpCode = state.code; }); ``` This is completely passive — it reads `totpState()` (already reactive) and calls `triggerRing()` when the code flips. No new intervals needed. The ring animation in `Ring.tsx` self-terminates with a 1200ms fade, so it won't persist. --- ## Issue 3 — `TOTP.tsx` standalone component has unstyled class names The file at `src/popup/components/TOTP.tsx` uses `totp-display`, `totp-code`, `totp-countdown` — class names without the `peach-` prefix. These are not defined in `popup.css`. If this component renders anywhere in the popup, those elements are completely unstyled. ### Fix — `TOTP.tsx` Rename classes to the established `peach-` convention: ```tsx export function TOTPDisplay(props: TOTPProps) { const [totp, setTotp] = createSignal(generateTOTP(props.secret)); const interval = setInterval(() => { setTotp(generateTOTP(props.secret)); }, 1000); onCleanup(() => clearInterval(interval)); return (
2-factor

{totp().code.slice(0, 3)} {totp().code.slice(3)}

expires
{totp().remaining}s
); } ``` This reuses the exact same structure and CSS classes as the full `Entry.tsx` TOTP band, ensuring visual consistency wherever this simpler component is used. --- ## Issue 4 — `PasskeyApproval.tsx`: all layout via inline styles ### Root cause Every structural element in `PasskeyApproval.tsx` uses inline `style` attributes. The class `peach-overlay-card` is used but not defined in `popup.css` (Entry.tsx uses it in the QR modal, so it may be defined there — verify and add if missing). The icon wrapper, text hierarchy, and action area have no CSS classes. ### New CSS classes — add to `popup.css` after the existing overlay section ```css /* ── Passkey approval overlay ─────────────────────────────── */ .peach-passkey-approval { display: flex; flex-direction: column; align-items: center; padding: 28px 22px 22px; gap: 14px; text-align: center; } .peach-passkey-approval-icon { width: 46px; height: 46px; border-radius: 12px; background: var(--peach-s2); border: 1px solid var(--peach-t3); display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .peach-passkey-approval-icon img, .peach-passkey-approval-icon svg { width: 28px; height: 28px; border-radius: 6px; } .peach-passkey-approval-rp { font-family: var(--font-ui); font-size: 12px; color: var(--peach-t2); letter-spacing: 0.03em; } .peach-passkey-approval-action { font-family: var(--font-display); font-style: italic; font-weight: 300; font-size: 26px; color: var(--peach-t0); line-height: 1; } .peach-passkey-approval-user { font-family: var(--font-mono); font-size: 11px; color: var(--peach-t1); background: var(--peach-s2); border: 1px solid var(--peach-t3); border-radius: 4px; padding: 3px 8px; } .peach-passkey-approval-notice { font-size: 10px; color: var(--peach-t2); line-height: 1.5; max-width: 220px; } .peach-passkey-approval-actions { display: flex; gap: 8px; width: 100%; margin-top: 6px; } .peach-passkey-approval-actions .peach-btn { flex: 1; } /* Ensure peach-overlay-card is defined (also used in Entry.tsx QR modal) */ .peach-overlay-card { background: var(--peach-s1); border: 1px solid var(--peach-t3); border-radius: 12px; overflow: hidden; width: 100%; } ``` ### Fix — `PasskeyApproval.tsx` Replace the inline-styled structure with the new classes: ```tsx ``` --- ## Issue 5 — `PasskeyEntry.tsx`: `window.confirm()` for delete ### Root cause `window.confirm()` opens a native OS dialog — completely outside the Peach visual system, jarring, and especially wrong in a popup context where the OS dialog usually appears behind the extension popup. ### Fix — In-popup confirmation pattern Add a `deleteConfirmPending` signal. When the user clicks "Remove passkey", show an inline confirmation row instead of calling `window.confirm()`: ```tsx const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false); // Replace the delete button with: setDeleteConfirmPending(true)} > Remove passkey } >
Remove this passkey?
``` ### New CSS classes — add to `popup.css` ```css /* ── Passkey entry view ───────────────────────────────────── */ .peach-passkey-badge { margin-left: 8px; font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--peach-t2); font-family: var(--font-ui); vertical-align: middle; } .peach-passkey-delete-btn { color: var(--peach-breach); border-color: rgba(229, 85, 85, 0.25); } .peach-passkey-delete-confirm { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: rgba(229, 85, 85, 0.06); border: 1px solid rgba(229, 85, 85, 0.2); border-radius: 6px; } .peach-passkey-delete-confirm-copy { flex: 1; font-size: 11px; color: var(--peach-t1); } .peach-passkey-delete-btn--confirm { color: var(--peach-breach); border-color: rgba(229, 85, 85, 0.4); } ``` ### Fix — `PasskeyEntry.tsx`: inline badge in domain row Replace the inline badge style: ```tsx /* BEFORE */ passkey /* AFTER */ passkey ``` Replace the inline delete button style: ```tsx /* BEFORE */ /* AFTER */ ``` The `.peach-restore-warning` used for the "private key stored encrypted" notice is semantically wrong — that class was designed for destructive action warnings. Replace it with the new `.peach-info-notice` (defined below in Issue 7). --- ## Issue 6 — `Vault.tsx`: passkey badge inline styles The passkey badge in the vault list uses a long inline style string. Replace with the same `.peach-passkey-badge` class defined above: ```tsx /* BEFORE */ passkey /* AFTER */ passkey ``` --- ## Issue 7 — `Settings.tsx`: semantic misuse of `.peach-restore-warning` and inline styles ### `.peach-restore-warning` misused for non-destructive notices `.peach-restore-warning` has a red-tinted border and background — visually communicating danger. It is used in Settings for: 1. "Only one passkey provider can be active at a time" — informational, not dangerous 2. The passkey private key storage notice — informational These should use a new neutral notice class. ### New CSS class — add to `popup.css` ```css /* ── Info notice (neutral, non-destructive) ───────────────── */ .peach-info-notice { font-size: 11px; color: var(--peach-t2); line-height: 1.5; background: rgba(240, 232, 216, 0.04); border: 1px solid var(--peach-t3); border-radius: 6px; padding: 8px 10px; } ``` Replace the passkey provider conflict notice and the private key storage notice with `class="peach-info-notice"` instead of `class="peach-restore-warning"`. ### Sync section: color literals and inline styles The sync section in `Settings.tsx` uses: - Literal color strings: `"#5e5"`, `"#dd0"`, `"#888"`, `"#e55"` in `getStatusColor()` - Inline `style="..."` on every device and network row **Fix for `getStatusColor()`** — replace literals with CSS vars defined in `vars.css`: Add to `vars.css`: ```css --peach-sync-ok: #5ebd5e; --peach-sync-busy: #c8a840; --peach-sync-off: var(--peach-t2); --peach-sync-err: var(--peach-breach); ``` Update `getStatusColor()`: ```ts function getStatusColor(status: SyncStatus): string { switch (status) { case "synced": return "var(--peach-sync-ok)"; case "syncing": return "var(--peach-sync-busy)"; case "no_peers": return "var(--peach-sync-off)"; case "not_trusted": return "var(--peach-sync-err)"; case "offline": return "var(--peach-sync-off)"; default: return "var(--peach-sync-off)"; } } ``` **New CSS classes for sync device/network rows** — add to `popup.css`: ```css /* ── Settings sync rows ───────────────────────────────────── */ .peach-sync-section-label { font-size: 11px; color: var(--peach-t2); text-transform: uppercase; letter-spacing: 0.05em; } .peach-sync-device-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--peach-t3); } .peach-sync-device-info { display: flex; flex-direction: column; gap: 2px; } .peach-sync-device-name { font-size: 13px; color: var(--peach-t0); } .peach-sync-device-meta { font-size: 11px; color: var(--peach-t2); } .peach-sync-status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; vertical-align: middle; } .peach-sync-remove-btn { color: var(--peach-breach); } ``` **Fix — `Settings.tsx`**: Replace all inline-styled device/network row elements with these classes. The `style={...}` template literals on device rows become: ```tsx
{device.displayName} Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}
``` The status row inline dot becomes: ```tsx ``` (The single `style={{ background }}` is acceptable here since it's driven by a dynamic value. All structural layout moves to CSS.) --- ## Issue 8 — `Entry.tsx`: regen panel inline styles The regen panel, breach badge, canary badge, and canary suppress row all use inline styles. These should be CSS classes. ### New CSS classes — add to `popup.css` ```css /* ── Breach badge ─────────────────────────────────────────── */ .peach-breach-badge { display: flex; position: relative; z-index: 1; align-items: center; gap: 8px; font-size: 11px; color: var(--peach-breach); padding: 6px 0; margin-bottom: 4px; } /* ── Canary badge ─────────────────────────────────────────── */ .peach-canary-badge { display: flex; position: relative; z-index: 1; align-items: center; gap: 5px; padding: 4px 10px; background: rgba(217, 160, 48, 0.12); border: 1px solid rgba(217, 160, 48, 0.35); border-radius: 6px; margin-bottom: 6px; width: fit-content; } .peach-canary-badge-label { font-size: 10px; letter-spacing: 0.06em; color: rgba(217, 160, 48, 0.9); font-family: var(--font-ui); text-transform: uppercase; } /* ── Canary suppress row ──────────────────────────────────── */ .peach-canary-suppress-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: rgba(217, 160, 48, 0.08); border-radius: 6px; margin-bottom: 8px; } .peach-canary-suppress-label { font-size: 11px; color: rgba(217, 160, 48, 0.85); cursor: pointer; font-family: var(--font-ui); letter-spacing: 0.04em; } /* ── Regen panel ──────────────────────────────────────────── */ .peach-regen-panel { position: relative; z-index: 1; background: rgba(217, 104, 48, 0.08); border: 1px solid rgba(217, 104, 48, 0.25); border-radius: 8px; padding: 12px; margin-bottom: 8px; } .peach-regen-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .peach-regen-panel-title { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: rgba(217, 104, 48, 0.9); } .peach-regen-output { background: rgba(0, 0, 0, 0.2); border-radius: 6px; padding: 10px; margin-bottom: 10px; font-family: var(--font-mono); font-size: 13px; word-break: break-all; color: var(--peach-t1); } .peach-regen-actions { display: flex; gap: 6px; } .peach-regen-success { position: relative; z-index: 1; background: rgba(40, 180, 80, 0.1); border: 1px solid rgba(40, 180, 80, 0.3); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; font-size: 11px; color: rgba(40, 180, 80, 0.9); } ``` **Fix — `Entry.tsx`**: replace inline styles with these classes. The most important one is `peach-regen-output` — it currently uses `font-family:monospace` (generic), which must become `var(--font-mono)` (IBM Plex Mono). --- ## Issue 9 — `Lock.tsx`: restore link wrapper inline style The restore link buttons under the onboarding create-vault panel are wrapped in a div with inline style: ```tsx
``` ### New CSS class — add to `popup.css` ```css .peach-onboarding-alt-actions { display: flex; flex-direction: column; gap: 2px; padding: 0 2px; } ``` ### Fix — `Lock.tsx` ```tsx /* BEFORE */
...
/* AFTER */
...
``` --- ## Issue 10 — `Export.tsx`: password input class mismatch `Export.tsx` uses `peach-search-input` for the export password field and CJK description text: ```tsx PassphraseScreen`: ```tsx 0}>
{STRENGTH_LABEL[props.strengthTier]}
``` ### Warning block Both `CJKExport.tsx` and `Export.tsx` use an inline-styled red notice block. Add: ```css .peach-danger-notice { background: rgba(220, 80, 60, 0.08); border: 1px solid rgba(220, 80, 60, 0.2); border-radius: 6px; padding: 9px 11px; font-size: 11px; color: var(--peach-t1); line-height: 1.5; } ``` Replace: ```tsx
``` with: ```tsx
``` ### Spinner (generating/restoring screens) Both CJK views declare `@keyframes peach-spin` inline inside a `