61b35be7f039

fix: Stabilize Settings scaling and local Health updates (v3.6.46)

time
2026-05-19 22:59 UTC · 5 months ago
machine
koh2
parent
aa433d6c6c49
12 files manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/popup/App.tsx+1 −1src/popup/components/settings/StackDeck.tsx+9 −3src/popup/styles/popup.css+8 −0src/popup/styles/settings-stack.css+40 −13src/popup/styles/vars.css+33 −16src/popup/views/Health.test.tsx+9 −3src/popup/views/Health.tsx+34 −14src/popup/views/Settings.tsx+1 −1
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.45",5+  "version": "3.6.46",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.45",4+  "version": "3.6.46",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package-lock.json+2 −2
@@ -1,12 +1,12 @@11 {22   "name": "peach",3-  "version": "3.6.45",3+  "version": "3.6.46",44   "lockfileVersion": 3,55   "requires": true,66   "packages": {77     "": {88       "name": "peach",9-      "version": "3.6.45",9+      "version": "3.6.46",1010       "dependencies": {1111         "@bokuweb/zstd-wasm": "^0.0.27",1212         "@stablelib/blake2s": "^2.0.1",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.45",3+  "version": "3.6.46",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/App.tsx+1 −1
@@ -1039,10 +1039,10 @@10391039   const isShellOverlayOpen = () => view() !== "entry" || restoreView() !== null;10401040 10411041   function handleFixEntry(entry: VaultEntry) {1042+    setSelectedEntry(entry);10421043     // Passkey-only entries have no editable surface — drop into view mode10431044     // instead of opening the Add editor.10441045     if (isPasskeyOnly(entry)) {1045-      setSelectedEntry(entry);10461046       setView("entry");10471047       return;10481048     }

view file

src/popup/components/settings/StackDeck.tsx+9 −3
@@ -20,6 +20,7 @@2020 export function StackDeck(props: StackDeckProps) {2121   const [deckOpen, setDeckOpen] = createSignal(true);2222   const [focusHeight, setFocusHeight] = createSignal(DEFAULT_FOCUS_H);23+  const [textScale, setTextScale] = createSignal(1);2324   let focusedAt = 0;2425   let deckEl: HTMLDivElement | undefined;2526 @@ -37,7 +38,11 @@3738 3839     const updateFocusHeight = () => {3940       if (!deckEl) return;40-      setFocusHeight(Math.round(deckEl.getBoundingClientRect().height));41+      const scale = Number.parseFloat(42+        getComputedStyle(document.documentElement).getPropertyValue("--peach-text-scale")43+      );44+      setTextScale(Number.isFinite(scale) && scale > 0 ? scale : 1);45+      setFocusHeight(deckEl.clientHeight || DEFAULT_FOCUS_H);4146     };4247     updateFocusHeight();4348     const observer =@@ -58,6 +63,7 @@5863     const idx = props.cards.findIndex((card) => card.id === props.active);5964     return idx >= 0 ? idx : 0;6065   });66+  const scaled = (px: number) => px / textScale();6167 6268   return (6369     <section class="stk-shell" aria-label="Settings">@@ -111,7 +117,7 @@111117             const top = () => {112118               if (active()) return 0;113119               if (!deckOpen()) return focusHeight() + 44 + distance() * 8;114-              return EXPANDED_H - TAB_OVERLAP + distance() * (TAB_H - TAB_OVERLAP);120+              return scaled(EXPANDED_H - TAB_OVERLAP) + distance() * scaled(TAB_H - TAB_OVERLAP);115121             };116122             const zIndex = () => (active() ? 99 : 50 - distance());117123 @@ -128,7 +134,7 @@128134                   props.onActiveChange(card.id);129135                   setDeckOpen(false);130136                 }}131-                height={active() ? (deckOpen() ? EXPANDED_H : focusHeight()) : TAB_H + 4}137+                height={active() ? (deckOpen() ? scaled(EXPANDED_H) : focusHeight()) : scaled(TAB_H + 4)}132138               >133139                 {props.bodies[card.id]}134140               </StackCard>

view file

src/popup/styles/popup.css+8 −0
@@ -6369,6 +6369,14 @@63696369   min-width: 0;63706370 }63716371 6372+.peach-health-pro-action {6373+  flex: 0 0 auto;6374+  padding: 4px 8px;6375+  font-size: 0.5625rem;6376+  letter-spacing: 0.08em;6377+  text-transform: uppercase;6378+}6379+63726380 .peach-health-pro-row-name {63736381   font-size: 0.8125rem;63746382   color: var(--peach-t0);

view file

src/popup/styles/settings-stack.css+40 −13
@@ -65,8 +65,8 @@6565 6666 .stk-deck {6767   position: relative;68-  height: min(462px, calc(100% - 36px));69-  min-height: 462px;68+  height: min(calc(462px / var(--peach-text-scale)), calc(100% - 36px));69+  min-height: calc(462px / var(--peach-text-scale));7070   margin-top: 0;7171 }7272 @@ -118,9 +118,9 @@118118 .stk-card.is-tab:hover {119119   transform: translateY(calc(var(--stk-card-top) - 7px));120120   box-shadow:121-    0 1px 0 color-mix(in srgb, var(--peach-t0) 7%, transparent) inset,122-    0 16px 32px rgba(0, 0, 0, 0.42),123-    0 0 28px color-mix(in srgb, currentColor 28%, transparent);121+    0 1px 0 color-mix(in srgb, #fff 9%, transparent) inset,122+    0 16px 32px rgba(0, 0, 0, 0.36),123+    0 0 0 1px color-mix(in srgb, #fff 10%, transparent);124124   transition:125125     transform 90ms ease-out,126126     box-shadow 90ms ease-out;@@ -487,23 +487,50 @@487487   min-height: 116px;488488 }489489 490-.light .stk-shell,491-[data-theme="light"] .stk-shell {490+.peach-settings-overlay {491+  --bg: #0C0A06;492+  --s1: #131109;493+  --s2: #1A1710;494+  --s3: #221F14;495+  --t0: #F0E8D8;496+  --t1: rgba(240, 232, 216, 0.50);497+  --t2: rgba(240, 232, 216, 0.20);498+  --t3: rgba(240, 232, 216, 0.07);499+  --t4: rgba(240, 232, 216, 0.04);500+  --surface-card: rgba(240, 232, 216, 0.02);501+  --surface-elevated: rgba(19, 17, 9, 0.88);502+  --shell-accent-a: color-mix(in srgb, var(--peach-accent) 8%, transparent);503+  --shell-accent-b: color-mix(in srgb, var(--peach-accent) 6%, transparent);504+  --peach-bg: var(--bg);505+  --peach-s1: var(--s1);506+  --peach-s2: var(--s2);507+  --peach-s3: var(--s3);508+  --peach-t0: var(--t0);509+  --peach-t1: var(--t1);510+  --peach-t2: var(--t2);511+  --peach-t3: var(--t3);512+  --peach-t4: var(--t4);513+  --peach-bg-card: var(--surface-card);514+  --peach-border: var(--peach-t3);515+  --peach-text-dim: var(--peach-t1);492516   background: var(--peach-bg);517+  color: var(--peach-t0);493518 }494519 495-.light .stk-card,496-[data-theme="light"] .stk-card {520+.peach-settings-overlay .stk-shell {521+  background: var(--peach-bg);522+}523+524+.peach-settings-overlay .stk-card {497525   box-shadow:498526     0 1px 0 color-mix(in srgb, var(--peach-t0) 5%, transparent) inset,499-    0 8px 20px rgba(78, 56, 36, 0.18);527+    0 8px 20px rgba(0, 0, 0, 0.25);500528 }501529 502-.light .stk-card.is-active,503-[data-theme="light"] .stk-card.is-active {530+.peach-settings-overlay .stk-card.is-active {504531   box-shadow:505532     0 1px 0 color-mix(in srgb, var(--peach-t0) 8%, transparent) inset,506-    0 18px 36px rgba(78, 56, 36, 0.35);533+    0 18px 36px rgba(0, 0, 0, 0.42);507534 }508535 509536 :root[data-native="true"] {

view file

src/popup/styles/vars.css+33 −16
@@ -223,31 +223,48 @@223223 }224224 225225 /* ─── Text size scaling ────────────────────────────────────────────────226-   Implemented via the `zoom` property on the popup body so every existing227-   px-based font-size in popup.css scales proportionally without rewriting228-   any individual rule. Capped at 1.08 — large enough to noticeably help,229-   small enough that the dense Vault and Health views don't clip the230-   fixed-size popup surface. The CSS custom property is also exposed so231-   desktop / mobile builds can use a calc() multiplier on layouts that232-   don't fit `zoom` semantics. */226+   Implemented via `zoom` on html so every existing px-based font-size in227+   popup.css scales proportionally without rewriting individual rules.228+   Capped at 1.08 — large enough to noticeably help, small enough that the229+   dense Vault and Health views don't clip the fixed-size popup surface.230+   The CSS custom property is also exposed so desktop / mobile builds can231+   use a calc() multiplier on layouts that don't fit `zoom` semantics. */233232 [data-text-size="small"]  { --peach-text-scale: 0.92; }234233 [data-text-size="medium"] { --peach-text-scale: 1; }235234 [data-text-size="large"]  { --peach-text-scale: 1.08; }236235 /* Zoom on html (not body) for both sizes so Chrome auto-resizes the popup237236    window in both directions. Zooming body for small left a visible gap at238-   the bottom because the window stayed at full height.239-240-   For "small" we also unfix html's width — Chrome's popup width is driven241-   by html's CSS width, not its post-zoom rendered width. With a fixed242-   320 px html, zoom 0.92 paints inside that 320 and leaves a strip of243-   Chrome backdrop on the right. Letting html auto-size to its content244-   (which is the 320 px body, rendered at 294 after zoom) lets the popup245-   window itself shrink with the zoom. */237+   the bottom because the window stayed at full height. Small text needs the238+   fixed popup surfaces widened before zoom; otherwise the 320px shell paints239+   as 294px and leaves unowned backdrop on the right edge. */246240 html { background: var(--bg); }247241 html[data-text-size="large"] { zoom: 1.08; }248242 html[data-text-size="small"] {243+  --peach-popup-scaled-width: 347.826087px;  /* 320 / 0.92 */244+  --peach-popup-scaled-height: 565.217391px; /* 520 / 0.92 */249245   zoom: 0.92;250-  width: auto;246+  width: var(--peach-popup-scaled-width);247+  min-width: var(--peach-popup-scaled-width);248+  height: var(--peach-popup-scaled-height);249+  min-height: var(--peach-popup-scaled-height);250+  overflow: hidden;251+}252+253+html[data-text-size="small"] body,254+html[data-text-size="small"] #app,255+html[data-text-size="small"] #pre-lock,256+html[data-text-size="small"] .popup-root,257+html[data-text-size="small"] .peach-shell[data-surface="extension"],258+html[data-text-size="small"] .peach-screen,259+html[data-text-size="small"] .peach-pro-celebration {260+  width: var(--peach-popup-scaled-width);261+  min-width: var(--peach-popup-scaled-width);262+  height: var(--peach-popup-scaled-height);263+  min-height: var(--peach-popup-scaled-height);264+}265+266+html[data-text-size="small"] .peach-shell[data-surface="extension"] {267+  max-height: var(--peach-popup-scaled-height);251268 }252269 253270 /* ─── Reduced motion ───────────────────────────────────────────────────

view file

src/popup/views/Health.test.tsx+9 −3
@@ -35,7 +35,7 @@3535     document.body.innerHTML = "";3636   });3737 38-  it("renders the Pro remediation queue without the retired password-change button", async () => {38+  it("opens the local entry editor from the Pro remediation queue", async () => {3939     const report: AdvancedHealthReport = {4040       vaultScore: 65,4141       generatedAt: Date.now(),@@ -63,13 +63,14 @@6363       ],6464     };6565     sendMessageMock.mockResolvedValue({ ok: true, data: report });66+    const onFixEntry = vi.fn();6667 6768     const { HealthView } = await import("./Health");6869     const mounted = renderView(6970       <HealthView7071         entries={[entry()]}7172         isPro={true}72-        onFixEntry={vi.fn()}73+        onFixEntry={onFixEntry}7374         onBack={vi.fn()}7475         onRefresh={vi.fn()}7576       />,@@ -81,6 +82,11 @@8182 8283     expect(mounted.container.textContent).toContain("Remediation queue");8384     expect(mounted.container.textContent).toContain("Example");84-    expect(mounted.container.querySelectorAll(".peach-health-pro-row button")).toHaveLength(0);85+    expect(mounted.container.textContent).not.toContain("Change password");86+87+    const button = mounted.container.querySelector<HTMLButtonElement>(".peach-health-pro-row button");88+    expect(button?.textContent).toContain("Update");89+    button?.click();90+    expect(onFixEntry).toHaveBeenCalledWith(expect.objectContaining({ id: "entry-1" }));8591   });8692 });

view file

src/popup/views/Health.tsx+34 −14
@@ -125,6 +125,12 @@125125       .sort((a, b) => a.lastActivity - b.lastActivity)126126   );127127 128+  const entriesById = createMemo(() => {129+    const map = new Map<string, VaultEntry>();130+    for (const entry of props.entries) map.set(entry.id, entry);131+    return map;132+  });133+128134   async function computeReused() {129135     const loginEntries = props.entries.filter((e) => e.type !== "note" && e.password);130136     if (loginEntries.length === 0) {@@ -223,22 +229,36 @@223229                 </span>224230               </div>225231               <For each={advancedReport()!.remediation.slice(0, 8)}>226-                {(item) => (227-                  <div class="peach-health-pro-row">228-                    <div class="peach-health-pro-row-info">229-                      <span class="peach-health-pro-row-name">{item.title}</span>230-                      <span class="peach-health-pro-row-meta">231-                        {item.reasons.join(" · ")}232-                      </span>233-                      <div class="peach-health-pro-bar" aria-hidden="true">234-                        <div235-                          class="peach-health-pro-bar-fill"236-                          style={{ width: `${Math.min(item.score, 100)}%` }}237-                        ></div>232+                {(item) => {233+                  const entry = () => entriesById().get(item.id);234+                  return (235+                    <div class="peach-health-pro-row">236+                      <div class="peach-health-pro-row-info">237+                        <span class="peach-health-pro-row-name">{item.title}</span>238+                        <span class="peach-health-pro-row-meta">239+                          {item.reasons.join(" · ")}240+                        </span>241+                        <div class="peach-health-pro-bar" aria-hidden="true">242+                          <div243+                            class="peach-health-pro-bar-fill"244+                            style={{ width: `${Math.min(item.score, 100)}%` }}245+                          ></div>246+                        </div>238247                       </div>248+                      <Show when={entry()}>249+                        {(target) => (250+                          <button251+                            class="peach-btn peach-btn--ghost peach-btn--sm peach-health-pro-action"252+                            onClick={() => props.onFixEntry(target())}253+                            aria-label={`Update password for ${item.title}`}254+                          >255+                            Update256+                          </button>257+                        )}258+                      </Show>239259                     </div>240-                  </div>241-                )}260+                  );261+                }}242262               </For>243263             </div>244264           </Show>

view file

src/popup/views/Settings.tsx+1 −1
@@ -1747,7 +1747,7 @@17471747       when={showNewRecoveryWords()}17481748       fallback={17491749         <div1750-          class="peach-overlay-view"1750+          class="peach-overlay-view peach-settings-overlay"17511751           classList={{ "ov-enter-right": !props.isExiting, "ov-exit-right": !!props.isExiting }}17521752           style="display:flex;flex-direction:column"17531753         >

view file