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
8 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 −51
manifest.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": {

view file

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",

view file

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": {

view file

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 }

view file

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>

view file

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;

view file

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 {

view file

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

view file