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
14 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/

view file

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

view file

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

view file

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

view file

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+}

view file

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

view file

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);

view file

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");

view file

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                 )}

view file

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 }

view file

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>

view file

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                 <button

view file

src/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 () => {

view file

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;

view file