8a204ae5858d
snappy hover (90ms), deck-level scroll gestures, vault history collapse, extend active card to 258px (v3.6.24)
- time
- 2026-05-19 14:55 UTC · 5 months ago
- machine
- koh2
- parent
- e25c0982ba33
↓ peach-browser-8a204ae5.tar.gzatlas at this save
files changed8 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/components/settings/StackCard.tsx+1 −10src/popup/components/settings/StackDeck.tsx+16 −3src/popup/styles/popup.css+27 −0src/popup/styles/settings-stack.css+7 −4src/popup/views/Settings.tsx+67 −51manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22 "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33 "manifest_version": 3,44 "name": "Peach",5- "version": "3.6.23",5+ "version": "3.6.24",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "3.6.23",4+ "version": "3.6.24",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.23",3+ "version": "3.6.24",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/components/settings/StackCard.tsx+1 −10
@@ -11,7 +11,6 @@1111 zIndex: number;1212 height: number;1313 onClick: () => void;14- onScrollToTop?: () => void;1514 children: JSX.Element;1615 }1716 @@ -52,15 +51,7 @@5251 </div>5352 <h2 class="stk-card-title">{props.spec.title}</h2>5453 <p class="stk-card-lede">{props.spec.lede}</p>55- <div56- class="stk-card-body"57- onWheel={(e) => {58- if (!props.onScrollToTop) return;59- if (e.deltaY < 0 && (e.currentTarget as HTMLElement).scrollTop === 0) {60- props.onScrollToTop();61- }62- }}63- >{props.children}</div>54+ <div class="stk-card-body">{props.children}</div>6455 </article>6556 );6657 }src/popup/components/settings/StackDeck.tsx+16 −3
@@ -10,7 +10,7 @@1010 bodies: StackBodies;1111 }1212 13-const EXPANDED_H = 248;13+const EXPANDED_H = 258;1414 const FOCUS_H = 420;1515 const TAB_H = 46;1616 const TAB_OVERLAP = 14;@@ -56,7 +56,21 @@5656 </div>5757 </div>5858 59- <div class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}>59+ <div60+ class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}61+ onWheel={(e) => {62+ const cardEl = (e.target as HTMLElement).closest<HTMLElement>("[data-settings-card-id]");63+ if (!cardEl) return;64+ const isActive = cardEl.dataset.active === "true";65+ if (!isActive && deckOpen() && e.deltaY > 3) {66+ props.onActiveChange(cardEl.dataset.settingsCardId as CardId);67+ setDeckOpen(false);68+ } else if (isActive && !deckOpen() && e.deltaY < -3) {69+ const body = cardEl.querySelector<HTMLElement>(".stk-card-body");70+ if (!body || body.scrollTop === 0) setDeckOpen(true);71+ }72+ }}73+ >6074 <For each={props.cards}>6175 {(card, index) => {6276 const active = () => card.id === props.active;@@ -89,7 +103,6 @@89103 props.onActiveChange(card.id);90104 setDeckOpen(false);91105 }}92- onScrollToTop={active() && !deckOpen() ? () => setDeckOpen(true) : undefined}93106 >94107 {props.bodies[card.id]}95108 </StackCard>src/popup/styles/popup.css+27 −0
@@ -7364,6 +7364,33 @@73647364 73657365 /* Vault History — Settings → Vault sub-view */73667366 7367+.peach-history-toggle {7368+ display: flex;7369+ align-items: center;7370+ justify-content: space-between;7371+ width: 100%;7372+ margin: 12px 0 4px;7373+ padding: 0;7374+ background: none;7375+ border: none;7376+ cursor: pointer;7377+ font-family: "IBM Plex Mono", monospace;7378+ font-size: 0.5rem;7379+ text-transform: uppercase;7380+ letter-spacing: 0.18em;7381+ color: var(--peach-t2);7382+}7383+.peach-history-toggle:hover {7384+ color: var(--peach-t1);7385+}7386+.peach-history-chevron {7387+ transition: transform 200ms var(--peach-curve-arrival);7388+ flex-shrink: 0;7389+}7390+.peach-history-chevron.is-open {7391+ transform: rotate(180deg);7392+}7393+73677394 .peach-history-list {73687395 border: 1px solid var(--peach-border);73697396 border-radius: 8px;src/popup/styles/settings-stack.css+7 −4
@@ -113,11 +113,14 @@113113 }114114 115115 .stk-card.is-tab:hover {116- transform: translateY(calc(var(--stk-card-top) - 3px));116+ transform: translateY(calc(var(--stk-card-top) - 7px));117117 box-shadow:118- 0 1px 0 color-mix(in srgb, var(--peach-t0) 5%, transparent) inset,119- 0 8px 20px rgba(0, 0, 0, 0.25),120- 0 0 18px color-mix(in srgb, currentColor 12%, transparent);118+ 0 1px 0 color-mix(in srgb, var(--peach-t0) 7%, transparent) inset,119+ 0 16px 32px rgba(0, 0, 0, 0.42),120+ 0 0 28px color-mix(in srgb, currentColor 28%, transparent);121+ transition:122+ transform 90ms ease-out,123+ box-shadow 90ms ease-out;121124 }122125 123126 .stk-card.is-concealed {src/popup/views/Settings.tsx+67 −51
@@ -202,6 +202,7 @@202202 const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);203203 const [historyLoading, setHistoryLoading] = createSignal(false);204204 const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);205+ const [historyOpen, setHistoryOpen] = createSignal(false);205206 206207 async function loadVaultHistory() {207208 setHistoryLoading(true);@@ -1054,58 +1055,73 @@10541055 </button>10551056 </div>10561057 1057- <p class="peach-settings-sub-label">VAULT HISTORY</p>1058- <Show when={historyLoading()}>1059- <p class="peach-settings-hint">Loading…</p>1060- </Show>1061- <Show when={!historyLoading() && historySnapshots().length === 0}>1062- <p class="peach-settings-hint">1063- No snapshots yet. Peach saves automatically before every change —1064- saves, syncs, and imports.1065- </p>1066- </Show>1067- <Show when={!historyLoading() && historySnapshots().length > 0}>1068- <p class="peach-settings-hint">1069- Restoring locks your vault. Unlock with the password that1070- encrypted that snapshot to confirm.1071- </p>1072- <div class="peach-history-list">1073- <For each={historySnapshots()}>1074- {(snap) => (1075- <div class="peach-history-row">1076- <div class="peach-history-meta">1077- <span class="peach-history-time">1078- {formatSnapshotTime(snap.savedAt)}1079- </span>1080- <span class="peach-history-sub">1081- {[1082- snap.entryCount !== null1083- ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1084- : null,1085- reasonLabel(snap.reason),1086- ]1087- .filter(Boolean)1088- .join(" · ")}1089- </span>1058+ <button1059+ class="peach-history-toggle"1060+ onClick={() => setHistoryOpen((v) => !v)}1061+ aria-expanded={historyOpen()}1062+ >1063+ <span>VAULT HISTORY</span>1064+ <svg1065+ class={`peach-history-chevron${historyOpen() ? " is-open" : ""}`}1066+ width="10" height="6" viewBox="0 0 10 6" fill="none"1067+ stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"1068+ >1069+ <path d="M1 1L5 5L9 1" />1070+ </svg>1071+ </button>1072+ <Show when={historyOpen()}>1073+ <Show when={historyLoading()}>1074+ <p class="peach-settings-hint">Loading…</p>1075+ </Show>1076+ <Show when={!historyLoading() && historySnapshots().length === 0}>1077+ <p class="peach-settings-hint">1078+ No snapshots yet. Peach saves automatically before every change —1079+ saves, syncs, and imports.1080+ </p>1081+ </Show>1082+ <Show when={!historyLoading() && historySnapshots().length > 0}>1083+ <p class="peach-settings-hint">1084+ Restoring locks your vault. Unlock with the password that1085+ encrypted that snapshot to confirm.1086+ </p>1087+ <div class="peach-history-list">1088+ <For each={historySnapshots()}>1089+ {(snap) => (1090+ <div class="peach-history-row">1091+ <div class="peach-history-meta">1092+ <span class="peach-history-time">1093+ {formatSnapshotTime(snap.savedAt)}1094+ </span>1095+ <span class="peach-history-sub">1096+ {[1097+ snap.entryCount !== null1098+ ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1099+ : null,1100+ reasonLabel(snap.reason),1101+ ]1102+ .filter(Boolean)1103+ .join(" · ")}1104+ </span>1105+ </div>1106+ <button1107+ class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1108+ historyRestoring() === snap.id ? " is-busy" : ""1109+ }`}1110+ onClick={() => void handleRestoreVaultSnapshot(snap.id)}1111+ disabled={historyRestoring() !== null}1112+ >1113+ <Show when={historyRestoring() === snap.id} fallback="Restore">1114+ …1115+ </Show>1116+ </button>10901117 </div>1091- <button1092- class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1093- historyRestoring() === snap.id ? " is-busy" : ""1094- }`}1095- onClick={() => void handleRestoreVaultSnapshot(snap.id)}1096- disabled={historyRestoring() !== null}1097- >1098- <Show when={historyRestoring() === snap.id} fallback="Restore">1099- …1100- </Show>1101- </button>1102- </div>1103- )}1104- </For>1105- </div>1106- <p class="peach-history-footer">1107- {historySnapshots().length} of 10 snapshots1108- </p>1118+ )}1119+ </For>1120+ </div>1121+ <p class="peach-history-footer">1122+ {historySnapshots().length} of 10 snapshots1123+ </p>1124+ </Show>11091125 </Show>11101126 11111127 <button