9caa1b
fix: popup polish and firefox packaging refresh (v1.4.42)
- time
- 2026-04-23 04:49 UTC · 6 months ago
- machine
- koh2
- parent
- c8201d
↓ peach-browser-9caa1b.tar.gzatlas at this save
files changed14 files
.gitignore+2 −0manifest.firefox.json+1 −1manifest.json+1 −1package.json+3 −3scripts/clean-firefox-artifacts.mjs+13 −0src/popup/App.tsx+4 −4src/popup/styles/popup.css+23 −10src/popup/views/CJKExport.tsx+13 −0src/popup/views/Entry.tsx+45 −11src/popup/views/Export.tsx+5 −1src/popup/views/PasskeyEntry.tsx+8 −1src/popup/views/Vault.tsx+0 −3src/popup/views/cjk-export.test.tsx+8 −6src/popup/views/contracts.test.tsx+1 −1.gitignore+2 −0
@@ -1,5 +1,7 @@11 node_modules/22 dist/3+dist-chrome/4+dist-firefox/35 .DS_Store46 .playwright-mcp/57 .gstack/manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "1.4.40",4+ "version": "1.4.42",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "1.4.40",4+ "version": "1.4.42",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+3 −3
@@ -1,16 +1,16 @@11 {22 "name": "peach",3- "version": "1.4.40",3+ "version": "1.4.42",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {77 "dev": "vite",88 "build": "vite build && node scripts/sync-chrome-dist.mjs",99 "build:chrome": "BUILD_TARGET=chrome vite build && node scripts/sync-chrome-dist.mjs",10- "build:firefox": "BUILD_TARGET=firefox vite build",10+ "build:firefox": "node scripts/clean-firefox-artifacts.mjs && BUILD_TARGET=firefox vite build",1111 "build:e2e": "PEACH_E2E_PASSKEYS=1 BUILD_TARGET=chrome vite build && node scripts/sync-chrome-dist.mjs",1212 "build:ci": "npm run build:chrome && npm run build:firefox",13- "package:firefox": "cd dist-firefox && web-ext build --overwrite-dest",13+ "package:firefox": "node scripts/clean-firefox-artifacts.mjs && npm run build:firefox && cd dist-firefox && web-ext build --overwrite-dest",1414 "typecheck": "tsc --noEmit",1515 "test": "vitest run",1616 "test:watch": "vitest",scripts/clean-firefox-artifacts.mjs+13 −0
@@ -0,0 +1,13 @@1+import { existsSync, rmSync } from "node:fs";2+import path from "node:path";3+import { fileURLToPath } from "node:url";4+5+const __filename = fileURLToPath(import.meta.url);6+const __dirname = path.dirname(__filename);7+const rootDir = path.resolve(__dirname, "..");8+const artifactsDir = path.join(rootDir, "dist-firefox", "web-ext-artifacts");9+10+if (existsSync(artifactsDir)) {11+ rmSync(artifactsDir, { recursive: true, force: true });12+ console.log(`Removed stale Firefox package artifacts from ${path.relative(rootDir, artifactsDir)}`);13+}src/popup/App.tsx+4 −4
@@ -54,14 +54,14 @@5454 [5555 { offset: 0, boxShadow: "var(--copy-shadow)" },5656 {57- offset: 0.25,57+ offset: 0.3,5858 boxShadow:59- "0 2px 8px rgba(0,0,0,0.5), 0 14px 48px rgba(0,0,0,0.6), 0 0 0 1.5px rgba(217,104,48,0.55), 0 0 28px rgba(217,104,48,0.2)",59+ "0 2px 8px rgba(0,0,0,0.5), 0 14px 48px rgba(0,0,0,0.6), 0 0 0 1.5px rgba(217,104,48,0.58), 0 0 22px rgba(217,104,48,0.18)",6060 },6161 { offset: 1, boxShadow: "var(--copy-shadow)" },6262 ],6363 {64- duration: 1000,64+ duration: 720,6565 easing: "cubic-bezier(0.4, 0, 0.2, 1)",6666 fill: "none",6767 }@@ -806,7 +806,7 @@806806 </Show>807807 <Show when={view() === "export"}>808808 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>809- <ExportView onBack={handleBack} />809+ <ExportView onBack={handleBack} onRequiresUnlock={handleLock} />810810 </Suspense>811811 </Show>812812 <Show when={view() === "health"}>src/popup/styles/popup.css+23 −10
@@ -303,8 +303,12 @@303303 -webkit-line-clamp: 2;304304 -webkit-box-orient: vertical;305305 overflow: hidden;306+ text-wrap: balance;306307 }307-.peach-entry-name-text { display: flex; flex-wrap: wrap; overflow: hidden; max-width: 100%; }308+.peach-entry-name--lg { font-size: 40px; }309+.peach-entry-name--xl { font-size: 34px; line-height: 0.98; }310+.peach-entry-name--xxl { font-size: 30px; line-height: 1.02; }311+.peach-entry-name-text { display: block; overflow: hidden; max-width: 100%; overflow-wrap: anywhere; }308312 309313 /* Trash icon in entry header */310314 .peach-entry-trash-btn {@@ -688,21 +692,27 @@688692 stroke-linecap: round;689693 transition: stroke-dashoffset 1s linear, stroke 500ms ease;690694 }691-.peach-totp-progress {692- position: absolute; left: 0; right: 0; bottom: 0;693- height: 1px; background: transparent; opacity: 0.4;694-}695-.peach-totp-progress-bar {696- height: 100%; background: var(--peach-accent);697- transition: width 1s linear;698-}699-700695 /* Search bar */701696 .peach-search-wrap {702697 border-top: 1px solid var(--peach-t3);703698 background: var(--peach-s1);704699 min-height: 54px; position: relative;705700 }701+.peach-search-wrap::before {702+ content: "";703+ position: absolute;704+ left: 0;705+ top: 0;706+ height: 2px;707+ width: var(--peach-totp-progress, 0%);708+ background: var(--peach-accent);709+ opacity: 0;710+ transition: width 1s linear, opacity 180ms ease;711+ pointer-events: none;712+}713+.peach-shell[data-totp-active="true"] .peach-search-wrap::before {714+ opacity: 0.9;715+}706716 .peach-search-bar {707717 min-height: 54px;708718 display: grid;@@ -1120,7 +1130,10 @@11201130 overflow-y: auto;11211131 color: var(--peach-t0);11221132 user-select: all;1133+ scrollbar-width: none;1134+ -ms-overflow-style: none;11231135 }1136+.peach-cjk-phrase-box::-webkit-scrollbar { display: none; }11241137 11251138 .peach-danger-notice {11261139 background: rgba(220, 80, 60, 0.08);src/popup/views/CJKExport.tsx+13 −0
@@ -82,6 +82,7 @@8282 8383 interface CJKExportProps {8484 onBack: () => void;85+ onRequiresUnlock?: () => void;8586 }8687 8788 function getPipelineErrorCopy(error: string | undefined): string {@@ -98,6 +99,14 @@9899 return `Export failed: ${error}`;99100 }100101 102+function isLockRecoveryError(error: string | undefined): boolean {103+ return (104+ error === "vault locked" ||105+ error === "VAULT_LOCKED" ||106+ error === "SERVICE_WORKER_UNAVAILABLE"107+ );108+}109+101110 export function CJKExportView(props: CJKExportProps) {102111 const [screen, setScreen] = createSignal<Screen>("passphrase");103112 @@ -262,6 +271,10 @@262271 setScreen("phrase");263272 } catch (err) {264273 const message = err instanceof Error ? err.message : String(err);274+ if (isLockRecoveryError(message)) {275+ props.onRequiresUnlock?.();276+ return;277+ }265278 setPipelineError(getPipelineErrorCopy(message));266279 setScreen("passphrase");267280 setProgress("idle");src/popup/views/Entry.tsx+45 −11
@@ -59,6 +59,38 @@5959 return ((state.remaining - 1) / 29) * 100;6060 });6161 62+ createEffect(() => {63+ const shell = document.getElementById("hd-shell");64+ if (!shell) return;65+66+ const state = totpState();67+ if (!state) {68+ delete shell.dataset.totpActive;69+ shell.style.removeProperty("--peach-totp-progress");70+ return;71+ }72+73+ shell.dataset.totpActive = "true";74+ shell.style.setProperty(75+ "--peach-totp-progress",76+ `${Math.max(0, totpProgress())}%`77+ );78+79+ onCleanup(() => {80+ delete shell.dataset.totpActive;81+ shell.style.removeProperty("--peach-totp-progress");82+ });83+ });84+85+ const titleSizeClass = createMemo(() => {86+ const length = props.entry?.title.length ?? 0;87+ if (length >= 28) return "peach-entry-name--xxl";88+ if (length >= 20) return "peach-entry-name--xl";89+ if (length >= 14) return "peach-entry-name--lg";90+ return "";91+ });92+ const useAnimatedTitle = createMemo(() => (props.entry?.title.length ?? 0) <= 13);93+6294 // ── Inline editing ────────────────────────────────────────────────6395 6496 function startEdit(field: EditableField, value: string) {@@ -238,15 +270,20 @@238270 title="Click to edit title"239271 onClick={() => startEdit("title", entry().title)}240272 >241- <h1 class="peach-entry-name" aria-label={entry().title}>273+ <h1 class={`peach-entry-name ${titleSizeClass()}`} aria-label={entry().title}>242274 <span class="peach-entry-name-text">243- <For each={entry().title.split("")}>244- {(ch, i) => (245- <span class="peach-entry-name-char" style={`--char-index:${i()}`}>246- {ch === " " ? "\u00A0" : ch}247- </span>248- )}249- </For>275+ <Show276+ when={useAnimatedTitle()}277+ fallback={entry().title}278+ >279+ <For each={entry().title.split("")}>280+ {(ch, i) => (281+ <span class="peach-entry-name-char" style={`--char-index:${i()}`}>282+ {ch === " " ? "\u00A0" : ch}283+ </span>284+ )}285+ </For>286+ </Show>250287 </span>251288 </h1>252289 </button>@@ -600,9 +637,6 @@600637 stroke-dashoffset={(1 - state().remaining / 30) * 106.8}601638 />602639 </svg>603- </div>604- <div class="peach-totp-progress">605- <div class="peach-totp-progress-bar" style={`width:${Math.max(0, totpProgress())}%`} />606640 </div>607641 </section>608642 )}src/popup/views/Export.tsx+5 −1
@@ -6,6 +6,7 @@66 77 interface ExportProps {88 onBack: () => void;9+ onRequiresUnlock?: () => void;910 }1011 1112 const MIN_EXPORT_PASSWORD = 12;@@ -14,7 +15,10 @@1415 const [showCjkExport, setShowCjkExport] = createSignal(false);1516 return (1617 <Show when={showCjkExport()} fallback={<MainExportView onBack={props.onBack} onOpenCjk={() => setShowCjkExport(true)} />}>17- <CJKExportView onBack={() => setShowCjkExport(false)} />18+ <CJKExportView19+ onBack={() => setShowCjkExport(false)}20+ onRequiresUnlock={props.onRequiresUnlock}21+ />1822 </Show>1923 );2024 }src/popup/views/PasskeyEntry.tsx+8 −1
@@ -21,6 +21,13 @@2121 return algorithm === -8 ? "EdDSA (Ed25519)" : "ES256 (P-256)";2222 }2323 24+function titleSizeClass(title: string): string {25+ if (title.length >= 28) return "peach-entry-name--xxl";26+ if (title.length >= 20) return "peach-entry-name--xl";27+ if (title.length >= 14) return "peach-entry-name--lg";28+ return "";29+}30+2431 export function PasskeyEntryView(props: PasskeyEntryProps) {2532 const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false);2633 @@ -38,7 +45,7 @@3845 </div>3946 4047 <div class="peach-entry-name-wrap" style="position:relative;z-index:1;">41- <h1 class="peach-entry-name" aria-label={entry().title}>48+ <h1 class={`peach-entry-name ${titleSizeClass(entry().title)}`} aria-label={entry().title}>4249 <span class="peach-entry-name-text">{entry().title}</span>4350 </h1>4451 </div>src/popup/views/Vault.tsx+0 −3
@@ -295,9 +295,6 @@295295 </button>296296 )}297297 </For>298- <Show when={props.currentTabDomain && props.sortOrder === "url-first"}>299- <span class="peach-sort-domain-hint">{props.currentTabDomain}</span>300- </Show>301298 <span style="flex:1" />302299 <Show when={props.onOpenTrash}>303300 <buttonsrc/popup/views/cjk-export.test.tsx+8 −6
@@ -102,7 +102,8 @@102102 };103103 }104104 105- it("shows a worker error instead of generating a fake 0-entry phrase", async () => {105+ it("routes back to unlock when the background is unavailable during export", async () => {106+ const onRequiresUnlock = vi.fn();106107 sendMessageMock.mockImplementation(async (msg: unknown) => {107108 const type = (msg as { type?: string }).type;108109 if (type === "GET_ENTRIES") {@@ -110,7 +111,11 @@110111 }111112 return { ok: true, data: {} };112113 });113- const container = mount();114+ const mounted = renderView(115+ <CJKExportView onBack={vi.fn()} onRequiresUnlock={onRequiresUnlock} />116+ );117+ disposers.push(mounted.dispose);118+ const container = mounted.container;114119 115120 await unlockExport(container);116121 findButton(container, "Create backup phrase →").click();@@ -118,10 +123,7 @@118123 await flushView();119124 120125 expect(exportToCJKMock).not.toHaveBeenCalled();121- expect(container.textContent).toContain(122- "Peach couldn't reach its background worker. Reopen the popup and try again."123- );124- expect(container.textContent).not.toContain("0 entries");126+ expect(onRequiresUnlock).toHaveBeenCalled();125127 });126128 127129 it("exports only live entries and shows the returned counts", async () => {src/popup/views/contracts.test.tsx+1 −1
@@ -90,7 +90,7 @@9090 9191 (globalThis as { chrome?: typeof chrome }).chrome = {9292 runtime: {93- getManifest: () => ({ version: "1.4.39" }),93+ getManifest: () => ({ version: "1.4.42" }),9494 onMessage: { addListener: vi.fn() },9595 },9696 } as unknown as typeof chrome;