# "Make Every Surface Shine" — Polish & Cohesion Spec _Peach v1.4.0 target_ --- ## Philosophy Every surface should feel like it was designed as a single, deliberate object — not assembled from parts. The existing design tokens (Cormorant Garamond, IBM Plex Mono, the amber/bone palette, the spring curves) are excellent. The problems are structural and consistency failures, not aesthetic ones. No new visual language is needed. The work here is surgical: fix what breaks the spell. --- ## 1. Strip the border-radius from the popup root **Problem:** `.peach-screen`, `.peach-shell`, and `.peach-shell[data-surface="extension"]` all declare `border-radius: 20px`. Chrome clips the popup viewport with a hard rectangular frame. The rounded corners are invisible — the popup just looks like it has 20px of wasted inset on each corner, slightly narrowing the effective content area. This is purely wasteful. **Fix:** In `popup.css`, remove `border-radius` from all popup root rules: ```css /* REMOVE these lines: */ .peach-shell[data-surface="extension"] { border-radius: 20px; } /* delete */ .peach-screen { border-radius: 20px; } /* delete */ /* On the outer container, zero out: */ .peach-shell, .peach-screen { border-radius: 0; } ``` The `border-radius` token on *inner* cards (`.peach-lock-panel`, `.peach-field-card`, etc.) stays — those are visible and intentional. Only the popup root shell gets squared off. --- ## 2. Rebuild the Lock / Unlock screen **Problem:** The lock screen tries to do three jobs at once: unlock, onboard, and provide restore paths. On the *unlock* path (user already has a vault), the "restore instead" panel is terrifying — it leads to actions that replace the locked vault, and the tag even says "replaces locked vault". There's no reason a user who just wants to type their password should see those options at all. On the *onboarding* path it makes more sense, but the layout is still dense and slightly panicked. **Unlock screen redesign (hasVault = true):** Strip it to a single focused surface. No secondary panel. No footer links. No restore paths. The lock screen's entire job is to verify identity. If someone doesn't know their master password, the answer is in Settings → Vault, not here. ```tsx // Unlock surface (hasVault = true) — password only, nothing else

unlock
your vault.

{error()}

``` The `openRestore` function, the `restoreSurface` signal, the `RestoreSurface` type, and the secondary panel JSX are all deleted from `Lock.tsx` when `hasVault = true`. The `replacesExistingVault` prop passed to `ImportView` becomes irrelevant on the lock screen and can be removed from that code path entirely. **Onboarding screen redesign (hasVault = false):** The onboarding headline, subline, and the two restore/import paths make sense here — there's no existing vault to destroy, so offering these paths is genuinely helpful. Keep the structure but clean up the visual hierarchy: - Wordmark top-left - Hero: headline + one-sentence subline (shorten current copy) - Card for "create vault" (primary action) - Below it, flat link buttons for the two restore paths — not a full secondary panel with its own header. They're supplementary actions. The current onboarding note `"If you lose this password, Peach cannot recover your vault."` should be styled as a `peach-hint` below the create button, not as a standalone paragraph. It reads as an afterthought right now; it should feel like a grounding statement. **Restore vault → Settings:** The `openRestore("cjk")` and `openRestore("file")` paths move to **Settings → Vault** as a dedicated "Replace vault from backup" section. It should open with a disclaimer card styled like `.peach-restore-warning` (see below), require the user to confirm they understand the action is destructive, and then present the two restore options. This is a deliberate navigation, not something a user stumbles into while trying to log in. **CSS additions for `.lock-screen--unlock`:** ```css .lock-screen--unlock { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0; padding: 28px 28px 24px; } .lock-centered { width: 100%; max-width: 260px; display: flex; flex-direction: column; align-items: center; gap: 16px; text-align: center; } .lock-centered .peach-wordmark-wrap { margin-bottom: 8px; } .lock-centered .peach-unlock-headline { font-size: 36px; line-height: 0.95; margin: 0; } .lock-centered .peach-underline-input { text-align: center; width: 100%; } .lock-centered .peach-lock-submit { width: 100%; } .peach-lock-restore-link { background: transparent; border: none; cursor: pointer; font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.12em; color: var(--t2); padding: 4px 0; transition: color 150ms ease; } .peach-lock-restore-link:hover { color: var(--t1); } .peach-restore-warning { font-size: 11px; line-height: 1.6; color: var(--t1); background: rgba(220, 80, 60, 0.07); border: 1px solid rgba(220, 80, 60, 0.15); border-radius: 8px; padding: 10px 12px; } ``` --- ## 3. Inline the Generator into Add.tsx **Problem:** The password generator is a full navigation target. You click "generate" in Add.tsx, navigate away to GeneratorView, configure, click "use", navigate back. This is jarring. The generator controls are simple enough (one slider, four toggles, one output) to live inline as a collapsible drawer. **Solution:** Replace the `onOpenGenerator` navigation prop with an inline `GeneratorDrawer` component that mounts directly in the Add form below the password field. It is hidden (`height: 0`, `overflow: hidden`) until the user clicks "generate", then it expands with a smooth animation. **GeneratorDrawer interface:** ```tsx interface GeneratorDrawerProps { open: boolean; policy?: PasswordPolicy; onUse: (password: string) => void; } ``` The drawer contains the same logic as `GeneratorView` — length slider, four toggles, output display, regenerate and copy buttons — minus the overlay header and navigation. The "use" button calls `onUse(password())` and `setDrawerOpen(false)`. The "generate" button in the password field row becomes a toggle: click once to open, click again (or after using) to close. **Add.tsx changes:** ```tsx // Replace: const [showGenerator, setShowGenerator] = createSignal(false); // replaces onOpenGenerator // Password field row becomes:
{ setPassword(pwd); setShowGenerator(false); }} /> ``` **GeneratorDrawer CSS:** ```css .peach-generator-drawer { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 320ms var(--peach-curve-arrival); overflow: hidden; } .peach-generator-drawer.is-open { grid-template-rows: 1fr; } .peach-generator-drawer-inner { min-height: 0; overflow: hidden; } .peach-generator-drawer-content { border-top: 1px solid var(--t3); padding: 12px 0 4px; display: flex; flex-direction: column; gap: 12px; } ``` The `grid-template-rows: 0fr → 1fr` trick gives a smooth height animation without needing a fixed height or JS measurement. **Fix long password overflow:** `peach-pwgen-output` must not overflow. Current state: no `overflow` or `word-break` set, so long generated passwords (128 chars) can push the container horizontally. ```css .peach-pwgen-output { font-family: var(--font-mono); font-size: 13px; line-height: 1.6; color: var(--t0); word-break: break-all; /* ← key line */ overflow-wrap: break-word; white-space: normal; /* not nowrap */ margin: 0; padding: 10px 12px; background: var(--t4); border-radius: 6px; min-height: 36px; } ``` **The GeneratorView file** (`src/popup/views/Generator.tsx`) stays as-is — it's still used for the standalone generator accessible from the main vault toolbar. The inline drawer is a new `GeneratorDrawer` component extracted into `src/popup/components/GeneratorDrawer.tsx`. --- ## 4. Fix Add.tsx — type tabs, canary card, note form ### 4a. Type selector tabs The `.peach-type-tab` class has no defined styles in popup.css. The tabs render as raw browser buttons. They need to be styled as a segmented pill control that matches the rest of the design system. **CSS:** ```css .peach-type-tabs { display: flex; gap: 0; background: var(--t4); border-radius: 8px; padding: 3px; } .peach-type-tab { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 6px 10px; border-radius: 6px; border: none; background: transparent; cursor: pointer; font-family: var(--font-ui); font-size: 11px; font-weight: 400; letter-spacing: 0.04em; color: var(--t1); transition: background 150ms ease, color 150ms ease; } .peach-type-tab.is-active { background: var(--s2); color: var(--t0); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } .peach-type-tab:not(.is-active):hover { color: var(--t0); } .peach-type-tab svg { opacity: 0.7; flex-shrink: 0; } .peach-type-tab.is-active svg { opacity: 1; } ``` ### 4b. Canary card The canary section in Add.tsx uses `style="border:1px solid rgba(217,160,48,0.25);background:rgba(217,160,48,0.04);"` inline — meaning it can't be themed, can't be overridden cleanly, and breaks the light theme. Extract to a CSS class. **Replace the inline style on the canary `.peach-field-card`:** ```tsx // Before:
// After:
``` **CSS:** ```css .peach-canary-card { border: 1px solid rgba(217, 160, 48, 0.2); background: rgba(217, 160, 48, 0.03); border-radius: 8px; padding: 12px; } .peach-canary-settings-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; user-select: none; } .peach-canary-card .peach-label { color: rgba(217, 160, 48, 0.85); } .peach-canary-card .peach-new-field-input { border-color: rgba(217, 160, 48, 0.25); } .peach-canary-card .peach-new-field-input:focus { border-color: rgba(217, 160, 48, 0.5); } .peach-canary-body { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; } .peach-canary-hint { font-size: 10px; color: var(--t2); font-family: var(--font-ui); line-height: 1.5; margin-top: 4px; } .peach-canary-actions-row { display: flex; align-items: center; gap: 8px; } .peach-canary-test-btn { border-color: rgba(217, 160, 48, 0.3); color: rgba(217, 160, 48, 0.85); } .peach-canary-test-result { font-size: 10px; font-family: var(--font-ui); } .peach-canary-test-result.ok { color: rgba(100, 210, 100, 0.9); } .peach-canary-test-result.err { color: rgba(220, 80, 60, 0.9); } ``` Remove all the matching inline styles from Add.tsx and replace with the class names above. ### 4c. Note entry form The note form (`selectedType() === "note"`) renders just a textarea with `style="min-height:200px;"`. It needs: - A proper `.peach-note-form` container with consistent padding - The textarea to use a class `peach-note-textarea` with appropriate sizing - A character count hint in the bottom-right corner (`{notes().length} chars`) ```css .peach-note-textarea { width: 100%; min-height: 180px; max-height: 280px; resize: vertical; background: var(--t4); border: 1px solid var(--t3); border-radius: 8px; padding: 10px 12px; font-family: var(--font-mono); font-size: 12px; line-height: 1.7; color: var(--t1); outline: none; transition: border-color 150ms ease; } .peach-note-textarea:focus { border-color: var(--t2); } .peach-note-char-count { font-family: var(--font-mono); font-size: 9px; color: var(--t3); text-align: right; margin-top: 4px; letter-spacing: 0.1em; } ``` ### 4d. Field input baseline All `peach-new-field-input` and `peach-new-name-input` elements need consistent focus rings and a visible resting border. Currently they likely inherit nothing or use browser defaults. Define: ```css .peach-new-field-input { width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--t3); padding: 4px 0; color: var(--t1); font-family: var(--font-mono); font-size: 13px; outline: none; transition: border-color 150ms ease; } .peach-new-field-input:focus { border-bottom-color: var(--t1); color: var(--t0); } .peach-new-name-input { width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--t3); padding: 6px 0; font-family: var(--font-display); font-style: italic; font-size: 28px; color: var(--t0); outline: none; transition: border-color 150ms ease; } .peach-new-name-input:focus { border-bottom-color: var(--t1); } .peach-new-name-input::placeholder { color: var(--t2); font-style: italic; } .peach-new-field-input::placeholder { color: var(--t3); } ``` --- ## 5. Remove the standalone Generator navigation path from Add.tsx In `App.tsx`, the `"generator"` view is reachable from Add via `onOpenGenerator`. Once the drawer is in place, the Add form no longer needs this prop. The `"generator"` view from the main vault toolbar (the wand/sparkle button) remains available — that's a valid standalone use. Just remove it as a destination from Add's `onBack`/`onOpenGenerator` flow. In `App.tsx`: - Remove the `openGenerator` function call that sets view to `"generator"` when coming from Add - AddView no longer receives `onOpenGenerator` prop - The `GeneratorView` standalone entry point from the toolbar is preserved --- ## 6. General surface cohesion items ### 6a. Overlay headers All overlay views (Add, Health, CJKExport, etc.) share `.peach-overlay-header`. Make sure the cancel/back button on the left and the action button on the right are always vertically centered and have consistent spacing. Audit that no overlay header has a different height from any other. ```css .peach-overlay-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--t3); flex-shrink: 0; min-height: 44px; /* lock the height */ } .peach-overlay-title { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--t1); position: absolute; left: 50%; transform: translateX(-50%); } .peach-overlay-action { position: relative; z-index: 1; min-width: 44px; /* prevent layout shift as label changes */ text-align: center; } ``` ### 6b. Scrollable body overflow Any overlay body that can exceed 520px (Add.tsx with all fields, Health with many entries) must be scrollable within the popup, not invisible below the fold. The body element must not overflow the popup frame. ```css .peach-overlay-view { display: flex; flex-direction: column; height: 520px; /* match popup height exactly */ overflow: hidden; } .peach-overlay-body, .peach-add-form, .peach-health-body, .peach-pwgen-body { flex: 1; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--t3) transparent; padding: 14px 16px; } ``` This is the fix for long passwords rendering off-screen — the symptom is actually the container not scrolling, not the password field itself overflowing. ### 6c. Toggle component The `.peach-toggle` / `.peach-toggle-knob` pattern is used in Generator, Settings, and Canary. Add a consistent `transition` so the knob slides smoothly: ```css .peach-toggle { position: relative; width: 28px; height: 16px; border-radius: 8px; background: var(--t3); border: none; cursor: pointer; flex-shrink: 0; transition: background 200ms ease; } .peach-toggle.is-on { background: var(--peach-accent); } .peach-toggle-knob { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--t0); transition: transform 200ms var(--peach-curve-settle); } .peach-toggle.is-on .peach-toggle-knob { transform: translateX(12px); } ``` ### 6d. Button states Every `peach-btn` needs a `:focus-visible` ring that doesn't use the browser default outline (which looks wrong on the dark background): ```css .peach-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px rgba(217, 104, 48, 0.5); } ``` ### 6e. Scrollbar in Vault list The vault entry list scrollbar is currently browser-default (visible and ugly on Windows/Linux). Add: ```css .peach-vault-list { scrollbar-width: thin; scrollbar-color: var(--t3) transparent; } .peach-vault-list::-webkit-scrollbar { width: 3px; } .peach-vault-list::-webkit-scrollbar-thumb { background: var(--t3); border-radius: 2px; } ``` --- ## 7. File change summary | File | Changes | |------|---------| | `src/popup/styles/popup.css` | Remove root border-radius; add `.lock-screen--unlock`, `.lock-centered`, `.peach-lock-restore-link`, `.peach-restore-sheet`, `.peach-type-tabs`, `.peach-type-tab`, `.peach-canary-card` and all child classes, `.peach-note-textarea`, `.peach-new-field-input`, `.peach-new-name-input`, `.peach-generator-drawer`, `.peach-pwgen-output` overflow fix, `.peach-overlay-*` height lock, scrollbar polish, toggle transition, focus-visible rings | | `src/popup/views/Lock.tsx` | Two distinct layouts: unlock (password only, nothing else) and onboarding (create + two flat restore links); delete `restoreSurface` signal, secondary panel, and all restore code from unlock path | | `src/popup/views/Settings.tsx` | Add "Vault" section with "Replace vault from backup" — disclaimer card + CJK and file restore paths behind a destructive-action confirmation | | `src/popup/views/Add.tsx` | Remove `onOpenGenerator` prop; add `showGenerator` signal; inline ``; replace canary inline styles with classes; replace note textarea with `peach-note-textarea` class + char count; replace type tab inline styles | | `src/popup/components/GeneratorDrawer.tsx` | New file — extracts generator logic as a collapsible inline drawer | | `src/popup/views/Generator.tsx` | No changes — remains available standalone from vault toolbar | | `src/popup/App.tsx` | Remove `openGenerator` as Add destination; keep it as toolbar destination | --- ## 8. What this is NOT - Not a redesign. Every color, every font, every animation curve stays. - Not feature work. No new functionality. - Not a theme change. The amber/bone palette is correct. - Not a layout overhaul of the vault list or entry view — those are polished. Touch only the surfaces called out above.