peach_shine_spec.md
"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:
/* 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.
// Unlock surface (hasVault = true) — password only, nothing else
<section class="peach-screen peach-theme lock-screen lock-screen--unlock">
<div class="lock-centered">
<PeachWordmark />
<h1 class="peach-unlock-headline">
unlock<br/>your vault.
</h1>
<input ... />
<Show when={error()}><p class="peach-hint peach-hint-error">{error()}</p></Show>
<button class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit">
unlock
</button>
</div>
</section>
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:
.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:
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:
// Replace:
const [showGenerator, setShowGenerator] = createSignal(false); // replaces onOpenGenerator
// Password field row becomes:
<div class="peach-field-value-row">
<input class="peach-field-value peach-new-field-input" ... />
<button
class={`peach-btn peach-btn--ghost peach-btn--sm peach-inline-action ${showGenerator() ? "is-active" : ""}`}
onClick={() => setShowGenerator(!showGenerator())}
>
generate
</button>
</div>
<GeneratorDrawer
open={showGenerator()}
onUse={(pwd) => { setPassword(pwd); setShowGenerator(false); }}
/>
GeneratorDrawer 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.
.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:
.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:
// Before:
<div class="peach-field-card" style="border:1px solid rgba(217,160,48,0.25);background:rgba(217,160,48,0.04);">
// After:
<div class="peach-field-card peach-canary-card">
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-formcontainer with consistent padding - The textarea to use a class
peach-note-textareawith appropriate sizing - A character count hint in the bottom-right corner (
{notes().length} chars)
.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:
.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
openGeneratorfunction call that sets view to"generator"when coming from Add - AddView no longer receives
onOpenGeneratorprop - The
GeneratorViewstandalone 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.
.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.
.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:
.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):
.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:
.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 <GeneratorDrawer>; 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.