peach_faithful_spec.md
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,--t3scale - Zero
border-radiuson 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:
.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
/* 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.
// 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:
export function TOTPDisplay(props: TOTPProps) {
const [totp, setTotp] = createSignal(generateTOTP(props.secret));
const interval = setInterval(() => { setTotp(generateTOTP(props.secret)); }, 1000);
onCleanup(() => clearInterval(interval));
return (
<div class="peach-totp-band">
<div>
<span class="peach-label">2-factor</span>
<p class="peach-totp-code">
<span class="peach-totp-code-text">
{totp().code.slice(0, 3)} {totp().code.slice(3)}
</span>
</p>
</div>
<div class="peach-totp-meta">
<span class="peach-label">expires</span>
<div class="peach-totp-countdown">{totp().remaining}s</div>
</div>
</div>
);
}
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
/* ── 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:
<div class="peach-overlay-view" role="dialog" aria-modal="true">
<div class="peach-overlay-card">
<div class="peach-passkey-approval">
<div class="peach-passkey-approval-icon">
<Show when={props.request.iconUrl} fallback={<PasskeyGlyph />}>
<img src={props.request.iconUrl} alt={props.request.rpId} />
</Show>
</div>
<div class="peach-passkey-approval-rp">{props.request.rpId}</div>
<div class="peach-passkey-approval-action">
{props.request.type === "create" ? "create passkey" : "sign in"}
</div>
<Show when={props.request.userName}>
<div class="peach-passkey-approval-user">{props.request.userName}</div>
</Show>
<div class="peach-passkey-approval-notice">
{props.request.type === "create"
? "Peach will generate and store a passkey for this site."
: "Peach will authenticate you using your stored passkey."}
</div>
<div class="peach-passkey-approval-actions">
<button class="peach-btn peach-btn--ghost peach-btn--sm" onClick={props.onDeny}>
Deny
</button>
<button class="peach-btn peach-btn--accent peach-btn--sm" onClick={props.onApprove}>
{props.request.type === "create" ? "Create" : "Sign in"}
</button>
</div>
</div>
</div>
</div>
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():
const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false);
// Replace the delete button with:
<Show
when={deleteConfirmPending()}
fallback={
<button
class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn"
onClick={() => setDeleteConfirmPending(true)}
>
Remove passkey
</button>
}
>
<div class="peach-passkey-delete-confirm">
<span class="peach-passkey-delete-confirm-copy">Remove this passkey?</span>
<button
class="peach-btn peach-btn--ghost peach-btn--sm"
onClick={() => setDeleteConfirmPending(false)}
>
Cancel
</button>
<button
class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn--confirm"
onClick={() => {
setDeleteConfirmPending(false);
props.onDelete();
}}
>
Remove
</button>
</div>
</Show>
New CSS classes — add to popup.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:
/* BEFORE */
<span style="margin-left:8px;font-size:10px;...">passkey</span>
/* AFTER */
<span class="peach-passkey-badge">passkey</span>
Replace the inline delete button style:
/* BEFORE */
<button style="color:#e55;border-color:rgba(229,85,85,0.25)">Remove passkey</button>
/* AFTER */
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn">Remove passkey</button>
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:
/* BEFORE */
<span style="margin-left:6px;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:var(--t2)">passkey</span>
/* AFTER */
<span class="peach-passkey-badge">passkey</span>
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:
- "Only one passkey provider can be active at a time" — informational, not dangerous
- The passkey private key storage notice — informational
These should use a new neutral notice class.
New CSS class — add to popup.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"ingetStatusColor() - Inline
style="..."on every device and network row
Fix for getStatusColor() — replace literals with CSS vars defined in vars.css:
Add to vars.css:
--peach-sync-ok: #5ebd5e;
--peach-sync-busy: #c8a840;
--peach-sync-off: var(--peach-t2);
--peach-sync-err: var(--peach-breach);
Update getStatusColor():
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:
/* ── 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:
<div class="peach-sync-device-row">
<div class="peach-sync-device-info">
<span class="peach-sync-device-name">{device.displayName}</span>
<span class="peach-sync-device-meta">
Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}
</span>
</div>
<button
class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"
onClick={() => void handleRemoveDevice(device.deviceId)}
>
Remove
</button>
</div>
The status row inline dot becomes:
<span
class="peach-sync-status-dot"
style={{ background: getStatusColor(syncStatus()) }}
/>
(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
/* ── 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:
<div style="display:flex;flex-direction:column;gap:2px;padding:0 2px;">
New CSS class — add to popup.css
.peach-onboarding-alt-actions {
display: flex;
flex-direction: column;
gap: 2px;
padding: 0 2px;
}
Fix — Lock.tsx
/* BEFORE */
<div style="display:flex;flex-direction:column;gap:2px;padding:0 2px;">
...
</div>
/* AFTER */
<div class="peach-onboarding-alt-actions">
...
</div>
Issue 10 — Export.tsx: password input class mismatch
Export.tsx uses peach-search-input for the export password field and CJK description text:
<input class="peach-search-input" style="width:100%;box-sizing:border-box;padding:8px 10px;..."
.peach-search-input is the vault search bar — it's styled as an italic Cormorant Garamond field with no border. Using it for a password input with additional inline overrides is wrong.
Fix — Export.tsx and CJKExport.tsx / CJKImport.tsx
Replace all instances of peach-search-input used for password/text inputs with a proper class:
New CSS class — add to popup.css
/* ── Settings-context text/password input ─────────────────── */
.peach-settings-input {
width: 100%;
box-sizing: border-box;
padding: 8px 10px;
background: var(--peach-s2);
border: 1px solid var(--peach-t3);
border-radius: 6px;
color: var(--peach-t0);
font-family: var(--font-ui);
font-size: 12px;
outline: none;
transition: border-color 120ms ease;
}
.peach-settings-input:focus {
border-color: var(--peach-accent);
}
.peach-settings-input[type="password"] {
font-family: var(--font-mono);
letter-spacing: 0.04em;
}
Replace all:
class="peach-search-input" style="width:100%;box-sizing:border-box;padding:8px 10px;background:var(--s2);border:1px solid var(--t3);border-radius:6px;color:var(--t0);font-size:12px"
with:
class="peach-settings-input"
This applies to Export.tsx (export password), CJKExport.tsx (passphrase + confirm), and CJKImport.tsx (passphrase + master password inputs).
Issue 11 — CJKExport.tsx and CJKImport.tsx: inline-styled shells
Strength meter
The strength meter bar in CJKExport.tsx is built with inline styles. Add CSS classes:
/* ── CJK export passphrase strength ──────────────────────── */
.peach-strength-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.peach-strength-track {
flex: 1;
height: 4px;
background: var(--peach-s2);
border-radius: 2px;
overflow: hidden;
}
.peach-strength-fill {
height: 100%;
border-radius: 2px;
transition: width 120ms ease, background 120ms ease;
}
.peach-strength-label {
font-size: 10px;
font-weight: 500;
min-width: 64px;
text-align: right;
}
Replace the inline strength meter in CJKExport.tsx > PassphraseScreen:
<Show when={props.passphrase.length > 0}>
<div class="peach-strength-row">
<div class="peach-strength-track">
<div
class="peach-strength-fill"
style={{
width: `${barFillPercent()}%`,
background: STRENGTH_COLOUR[props.strengthTier],
}}
/>
</div>
<span
class="peach-strength-label"
style={{ color: STRENGTH_COLOUR[props.strengthTier] }}
>
{STRENGTH_LABEL[props.strengthTier]}
</span>
</div>
</Show>
Warning block
Both CJKExport.tsx and Export.tsx use an inline-styled red notice block. Add:
.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:
<div style="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(--t1);margin:12px 0">
with:
<div class="peach-danger-notice" style="margin:12px 0">
Spinner (generating/restoring screens)
Both CJK views declare @keyframes peach-spin inline inside a <style> tag inside the component. This duplicates the animation and pollutes the style sheet at runtime. Extract to popup.css:
/* ── Spinner (shared) ─────────────────────────────────────── */
@keyframes peach-spin {
to { transform: rotate(360deg); }
}
.peach-spinner {
width: 32px;
height: 32px;
border: 2px solid var(--peach-s2);
border-top-color: var(--peach-accent);
border-radius: 50%;
margin: 0 auto 16px;
animation: peach-spin 0.8s linear infinite;
}
.peach-progress-screen {
text-align: center;
padding: 32px 16px;
}
.peach-progress-screen-title {
font-size: 13px;
color: var(--peach-t0);
margin-bottom: 16px;
}
.peach-progress-screen-stage {
font-size: 11px;
color: var(--peach-t2);
min-height: 14px;
}
.peach-progress-screen-note {
font-size: 10px;
color: var(--peach-t2);
margin-top: 8px;
}
Remove the inline <style> blocks from GeneratingScreen and RestoringScreen. Replace the spinner <div> and surrounding layout with:
function GeneratingScreen(props: GeneratingScreenProps) {
return (
<div class="peach-settings-group peach-progress-screen">
<div class="peach-progress-screen-title">Creating your backup phrase…</div>
<div class="peach-spinner" aria-label="Working" />
<div class="peach-progress-screen-stage">{PROGRESS_COPY[props.stage]}</div>
<Show when={props.highSecurity}>
<div class="peach-progress-screen-note">Using High Security — this takes about 3 seconds.</div>
</Show>
</div>
);
}
CJK QR overlay
The QR overlay in CJKExport.tsx is entirely inline-styled. Replace with the .peach-overlay-view + .peach-overlay-card pattern already used throughout the extension:
<Show when={props.showQrOverlay && props.qrDataUrl}>
<div class="peach-overlay-view" role="dialog" aria-modal="true" onClick={props.onCloseQr}>
<div class="peach-overlay-card peach-cjk-qr-card" onClick={(e) => e.stopPropagation()}>
<div class="peach-overlay-header">
<span class="peach-overlay-title">scan with Peach</span>
<div class="peach-overlay-spacer" />
<button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" onClick={props.onCloseQr} aria-label="Close">
<CloseGlyph />
</button>
</div>
<div class="peach-cjk-qr-body">
<p class="peach-cjk-qr-hint">Open Peach → Import → Restore from CJK phrase</p>
<img src={props.qrDataUrl!} alt="Vault QR code" class="peach-cjk-qr-image" width="320" height="320" />
</div>
</div>
</div>
</Show>
New CSS:
.peach-cjk-qr-card {
max-width: 400px;
margin: auto;
}
.peach-cjk-qr-body {
padding: 0 20px 20px;
text-align: center;
}
.peach-cjk-qr-hint {
font-size: 11px;
color: var(--peach-t2);
margin: 0 0 14px;
}
.peach-cjk-qr-image {
border-radius: 4px;
display: block;
margin: 0 auto;
width: 100%;
max-width: 320px;
height: auto;
}
Issue 12 — SyncPairing.tsx: verify peach-pairing-* CSS coverage
SyncPairing.tsx is well-structured — it uses peach-pairing-* class names throughout with no inline styles. Verify that all referenced classes exist in popup.css:
| Class | Expected |
|---|---|
.peach-pairing-code-display |
Container for the 6-digit pairing code |
.peach-pairing-code-digit |
Individual digit cell — mono, large |
.peach-pairing-countdown-ring |
SVG ring wrapper |
.peach-pairing-ring-track |
SVG circle background |
.peach-pairing-ring-progress |
SVG circle fill |
.peach-pairing-countdown-num |
Countdown number overlay |
.peach-pairing-content |
Centered flex column |
.peach-pairing-body |
Scrollable body |
.peach-pairing-headline |
Large display text |
.peach-pairing-subline |
Secondary copy |
.peach-pairing-status |
Spinner + status text row |
.peach-pairing-error |
Error text |
.peach-pairing-error-msg |
Error message paragraph |
.peach-pairing-code-inputs |
6-box code input grid |
.peach-pairing-code-box |
Individual code input |
.peach-pairing-spinner |
CSS spinner |
.peach-pairing-success-icon |
Success checkmark wrapper |
.peach-pairing-error-icon |
Error X wrapper |
If any of these are missing from popup.css, add them. The critical ones for visual coherence:
/* ── Sync pairing ─────────────────────────────────────────── */
.peach-pairing-body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.peach-pairing-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
padding: 28px 20px;
text-align: center;
}
.peach-pairing-headline {
font-family: var(--font-display);
font-style: italic;
font-weight: 300;
font-size: 28px;
color: var(--peach-t0);
margin: 0;
line-height: 1;
}
.peach-pairing-subline {
font-size: 12px;
color: var(--peach-t2);
max-width: 220px;
line-height: 1.5;
}
.peach-pairing-subline--center {
text-align: center;
}
.peach-pairing-code-display {
display: flex;
gap: 6px;
}
.peach-pairing-code-digit {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 48px;
background: var(--peach-s2);
border: 1px solid var(--peach-t3);
border-radius: 6px;
font-family: var(--font-mono);
font-size: 22px;
color: var(--peach-t0);
letter-spacing: 0;
}
.peach-pairing-countdown-ring {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}
.peach-pairing-countdown-num {
position: absolute;
font-family: var(--font-mono);
font-size: 14px;
color: var(--peach-t1);
}
.peach-pairing-ring-track {
fill: none;
stroke: var(--peach-t3);
stroke-width: 2;
}
.peach-pairing-ring-progress {
fill: none;
stroke: var(--peach-accent);
stroke-width: 2;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: 50% 50%;
transition: stroke-dashoffset 0.5s linear;
}
.peach-pairing-code-inputs {
display: flex;
gap: 8px;
}
.peach-pairing-code-box {
width: 36px;
height: 48px;
text-align: center;
background: var(--peach-s2);
border: 1px solid var(--peach-t3);
border-radius: 6px;
font-family: var(--font-mono);
font-size: 20px;
color: var(--peach-t0);
outline: none;
transition: border-color 120ms ease;
}
.peach-pairing-code-box:focus {
border-color: var(--peach-accent);
}
.peach-pairing-code-box.has-error {
border-color: var(--peach-breach);
}
.peach-pairing-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--peach-t1);
}
.peach-pairing-error {
font-size: 11px;
color: var(--peach-breach);
}
.peach-pairing-error-msg {
color: var(--peach-breach);
}
.peach-pairing-success-icon,
.peach-pairing-error-icon {
display: flex;
align-items: center;
justify-content: center;
}
.peach-pairing-spinner {
width: 20px;
height: 20px;
border: 1.5px solid var(--peach-t3);
border-top-color: var(--peach-accent);
border-radius: 50%;
animation: peach-spin 0.8s linear infinite;
flex-shrink: 0;
}
Issue 13 — GeneratorDrawer.tsx: verify CSS coverage
GeneratorDrawer.tsx uses peach-generator-drawer, peach-generator-drawer-inner, peach-generator-drawer-content, peach-pwgen-display-row, peach-pwgen-output, peach-pwgen-length-row, peach-pwgen-slider, peach-pwgen-len-num, peach-pwgen-toggles, peach-pwgen-toggle-row, peach-pwgen-toggle-label. Verify all are in popup.css. The drawer animation uses grid-template-rows: 0fr → 1fr for the smooth open. Confirm:
.peach-generator-drawer {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 240ms var(--ease-out);
}
.peach-generator-drawer.is-open {
grid-template-rows: 1fr;
}
.peach-generator-drawer-inner {
overflow: hidden;
}
The password output (peach-pwgen-output) must use var(--font-mono):
.peach-pwgen-output {
font-family: var(--font-mono);
font-size: 13px;
color: var(--peach-t0);
word-break: break-all;
flex: 1;
}
Summary of CSS additions to popup.css
| New class | Purpose |
|---|---|
.peach-passkey-approval |
Passkey approval overlay layout |
.peach-passkey-approval-icon |
Icon wrapper with border |
.peach-passkey-approval-rp |
RP domain text |
.peach-passkey-approval-action |
Large italic action verb |
.peach-passkey-approval-user |
Mono username chip |
.peach-passkey-approval-notice |
Informational fine print |
.peach-passkey-approval-actions |
Button row |
.peach-overlay-card |
Shared overlay card shell (if missing) |
.peach-passkey-badge |
"passkey" chip in Vault + PasskeyEntry |
.peach-passkey-delete-btn |
Red-tinted delete trigger |
.peach-passkey-delete-confirm |
Inline confirm row |
.peach-passkey-delete-confirm-copy |
Confirm text |
.peach-passkey-delete-btn--confirm |
Confirm action button |
.peach-info-notice |
Neutral informational notice |
.peach-sync-section-label |
Devices/networks section label |
.peach-sync-device-row |
Device/network row |
.peach-sync-device-info |
Device name + meta column |
.peach-sync-device-name |
Device name text |
.peach-sync-device-meta |
Last seen / last synced text |
.peach-sync-status-dot |
Status indicator dot |
.peach-sync-remove-btn |
Red remove button |
.peach-breach-badge |
Breach warning row in Entry |
.peach-canary-badge |
Canary indicator |
.peach-canary-badge-label |
Canary label text |
.peach-canary-suppress-row |
Session suppress checkbox row |
.peach-canary-suppress-label |
Suppress label text |
.peach-regen-panel |
Password regen overlay |
.peach-regen-panel-header |
Regen header row |
.peach-regen-panel-title |
Regen title text |
.peach-regen-output |
Password preview (IBM Plex Mono) |
.peach-regen-actions |
Regen button row |
.peach-regen-success |
Success confirmation row |
.peach-onboarding-alt-actions |
Restore link column on onboarding |
.peach-settings-input |
Settings-context text/password input |
.peach-strength-row |
Passphrase strength meter row |
.peach-strength-track |
Strength bar background |
.peach-strength-fill |
Strength bar fill |
.peach-strength-label |
Strength tier label |
.peach-danger-notice |
Red-tinted destructive warning block |
.peach-spinner |
Loading spinner |
.peach-progress-screen |
Centered progress screen wrapper |
.peach-progress-screen-title |
Progress title |
.peach-progress-screen-stage |
Stage label |
.peach-progress-screen-note |
Supplemental note |
.peach-cjk-qr-card |
CJK QR overlay card width |
.peach-cjk-qr-body |
QR body padding |
.peach-cjk-qr-hint |
QR hint text |
.peach-cjk-qr-image |
QR image |
@keyframes peach-spin |
Shared spinner keyframe (extracted from JSX) |
.peach-pairing-* (11 classes) |
Full SyncPairing visual coverage |
Summary of vars.css additions
--peach-sync-ok: #5ebd5e;
--peach-sync-busy: #c8a840;
--peach-sync-off: var(--peach-t2);
--peach-sync-err: var(--peach-breach);
What this does NOT change
- The autofill selector, passkey proxy, or any content script
- The vault encryption or crypto layer
- The Ring.tsx component itself (it works; we're only adding the TOTP trigger)
- Health.tsx (correctly styled with
peach-health-*classes; no changes needed) - Vault.tsx list layout (correctly styled; only the passkey badge inline style changes)
- The overall layout shell, nav, overlay entry/exit animations