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
↓ peach-browser-61b35be7.tar.gzatlas at this save
files changed12 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 −1manifest.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": {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",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",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": {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 }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>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);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"] {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 ───────────────────────────────────────────────────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 });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>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 >