3e534fd912b8

fix: Restore inline entry editing and constraint legibility (v5.2.2)

time
2026-09-06 19:05 UTC · 1 month ago
machine
koh2
parent
3b0b39558887
7 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/content.selector-ui.test.ts+36 −2src/content/inject.ts+150 −6src/popup/components/GeneratorDrawer.test.ts+7 −0src/popup/styles/popup.css+1 −1
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.2.1",4+  "version": "5.2.2",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "5.2.1",5+  "version": "5.2.2",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.2.1",3+  "version": "5.2.2",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/content/content.selector-ui.test.ts+36 −2
@@ -152,12 +152,25 @@152152     }153153   });154154 155-  it("routes inline edit to the extension popup without requesting raw edit data", async () => {155+  it("opens the selected entry editor inside the inline helper", async () => {156156     const originalAttachShadow = Element.prototype.attachShadow;157157     const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) {158158       return originalAttachShadow.call(this, { mode: "open" });159159     });160160     const anchor = makeInput({ type: "password" });161+    setMessageResponder((message) =>162+      message.type === "GET_ENTRY_FOR_FILL"163+        ? {164+            ok: true,165+            data: {166+              title: "Acme",167+              username: "[email protected]",168+              password: "current-password",169+              url: "https://example.com",170+            },171+          }172+        : { ok: true, data: null },173+    );161174 162175     try {163176       const dismiss = showAutofillSelector(anchor, [ENTRIES[0]!], vi.fn(), { domain: "example.com" });@@ -167,9 +180,30 @@167180       edit?.click();168181 169182       await vi.waitFor(() => {170-        expect(sentMessages).toContainEqual({ type: "OPEN_POPUP", payload: { entryId: "e1" } });183+        expect(host?.shadowRoot?.querySelector<HTMLInputElement>('.create-form-wrap input[type="password"]')?.value)184+          .toBe("current-password");171185       });186+      expect(sentMessages).toContainEqual({187+        type: "GET_ENTRY_FOR_FILL",188+        payload: { id: "e1", capability: undefined },189+      });190+      expect(sentMessages.some((message) => message.type === "OPEN_POPUP")).toBe(false);172191       expect(sentMessages.some((message) => message.type === "GET_ENTRY_FOR_EDIT")).toBe(false);192+193+      const title = host?.shadowRoot?.querySelector<HTMLInputElement>('.create-form-wrap input[type="text"]');194+      if (title) title.value = "Acme Updated";195+      host?.shadowRoot?.querySelector<HTMLButtonElement>(".create-form-wrap .save-btn")?.click();196+      await vi.waitFor(() => {197+        expect(sentMessages.find((message) => message.type === "UPDATE_ENTRY")).toMatchObject({198+          payload: {199+            id: "e1",200+            patch: { title: "Acme Updated", password: "current-password" },201+          },202+        });203+      });204+      const patch = sentMessages.find((message) => message.type === "UPDATE_ENTRY")?.payload?.patch;205+      expect(patch).not.toHaveProperty("totp");206+      expect(patch).not.toHaveProperty("passwordHistory");173207       dismiss();174208     } finally {175209       attachSpy.mockRestore();

view file

src/content/inject.ts+150 −6
@@ -2982,13 +2982,157 @@29822982     container.appendChild(platter);29832983   }29842984 2985-  /** Raw TOTP seeds and password history stay inside the extension popup. */2985+  /** Raw TOTP seeds, password history, and passkeys stay inside extension-owned UI. */29862986   async function renderInlineEdit(summary: SelectorEntry) {2987-    await chrome.runtime.sendMessage({2988-      type: "OPEN_POPUP",2989-      payload: { entryId: summary.id },2990-    }).catch(() => null);2991-    dismiss();2987+    currentMode = "edit";2988+    container.innerHTML = "";2989+2990+    const wrap = document.createElement("div");2991+    wrap.className = "create-form-wrap";2992+    const head = document.createElement("div");2993+    head.className = "create-head";2994+    const back = document.createElement("button");2995+    back.className = "back-btn";2996+    back.type = "button";2997+    back.title = "Cancel";2998+    back.innerHTML = `<svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><path d="M7 2L3 5.5L7 9"/></svg>`;2999+    back.addEventListener("click", () => renderSilverPlatter(summary));3000+    const label = document.createElement("div");3001+    label.className = "create-label";3002+    label.textContent = "Edit entry";3003+    head.append(back, label);3004+    wrap.appendChild(head);3005+3006+    const body = document.createElement("div");3007+    body.className = "create-body";3008+    const loading = document.createElement("div");3009+    loading.className = "empty-hint";3010+    loading.textContent = "loading…";3011+    body.appendChild(loading);3012+    wrap.appendChild(body);3013+    container.appendChild(wrap);3014+3015+    const response = await chrome.runtime.sendMessage({3016+      type: "GET_ENTRY_FOR_FILL",3017+      payload: { id: summary.id, capability: options.capability },3018+    }).catch(() => null) as {3019+      ok?: boolean;3020+      data?: {3021+        title?: string;3022+        username?: string;3023+        email?: string;3024+        password?: string;3025+        url?: string;3026+        notes?: string;3027+      };3028+    } | null;3029+    if (!response?.ok || !response.data) {3030+      loading.textContent = "couldn't load this entry";3031+      return;3032+    }3033+    body.innerHTML = "";3034+3035+    function field(labelText: string, value: string, type: "text" | "password" = "text") {3036+      const group = document.createElement("div");3037+      group.className = "f-group";3038+      const fieldLabel = document.createElement("div");3039+      fieldLabel.className = "f-label";3040+      fieldLabel.textContent = labelText;3041+      const input = document.createElement("input");3042+      input.className = "f-input";3043+      input.type = type;3044+      input.value = value;3045+      input.autocomplete = type === "password" ? "new-password" : "off";3046+      group.append(fieldLabel, input);3047+      body.appendChild(group);3048+      return input;3049+    }3050+3051+    const entry = response.data;3052+    const titleInput = field("title", entry.title ?? summary.title);3053+    const urlInput = field("url", entry.url ?? summary.url ?? "");3054+    const usernameInput = field("username", entry.username ?? summary.username ?? "");3055+    const emailInput = field("email", entry.email ?? summary.email ?? "");3056+    const passwordInput = field("password", entry.password ?? "", "password");3057+3058+    const generator = createSmartGenerator({3059+      policy: detectPasswordPolicy(),3060+      initialPassword: entry.password,3061+      onChange: (password) => { passwordInput.value = password; },3062+    });3063+    generator.root.hidden = true;3064+    const generatorToggle = document.createElement("button");3065+    generatorToggle.className = "gen-toggle-link";3066+    generatorToggle.type = "button";3067+    generatorToggle.textContent = "⚙ generate";3068+    generatorToggle.addEventListener("click", () => {3069+      generator.root.hidden = !generator.root.hidden;3070+      generatorToggle.textContent = generator.root.hidden ? "⚙ generate" : "☰ hide generator";3071+    });3072+    body.append(generatorToggle, generator.root);3073+3074+    const notesGroup = document.createElement("div");3075+    notesGroup.className = "f-group";3076+    const notesLabel = document.createElement("div");3077+    notesLabel.className = "f-label";3078+    notesLabel.textContent = "notes";3079+    const notesInput = document.createElement("textarea");3080+    notesInput.className = "f-input";3081+    notesInput.rows = 3;3082+    notesInput.value = entry.notes ?? "";3083+    notesGroup.append(notesLabel, notesInput);3084+    body.appendChild(notesGroup);3085+3086+    const actions = document.createElement("div");3087+    actions.className = "f-row";3088+    const save = document.createElement("button");3089+    save.className = "save-btn";3090+    save.type = "button";3091+    save.textContent = "Save changes";3092+    save.style.flex = "1";3093+    const cancel = document.createElement("button");3094+    cancel.className = "gen-btn";3095+    cancel.type = "button";3096+    cancel.textContent = "cancel";3097+    cancel.addEventListener("click", () => renderSilverPlatter(summary));3098+    actions.append(save, cancel);3099+    body.appendChild(actions);3100+3101+    save.addEventListener("click", async () => {3102+      save.disabled = true;3103+      save.textContent = "Saving…";3104+      const patch = {3105+        title: titleInput.value.trim(),3106+        url: urlInput.value.trim(),3107+        username: usernameInput.value.trim(),3108+        email: emailInput.value.trim() || undefined,3109+        password: passwordInput.value,3110+        notes: notesInput.value,3111+      };3112+      const update = await chrome.runtime.sendMessage({3113+        type: "UPDATE_ENTRY",3114+        payload: { id: summary.id, patch },3115+      }).catch(() => null) as { ok?: boolean; error?: string } | null;3116+      if (!update?.ok) {3117+        save.disabled = false;3118+        save.textContent = "Save changes";3119+        const error = document.createElement("div");3120+        error.className = "empty-hint";3121+        error.textContent = update?.error ?? "couldn't save this entry";3122+        body.appendChild(error);3123+        setTimeout(() => error.remove(), 3000);3124+        return;3125+      }3126+      renderSilverPlatter({3127+        ...summary,3128+        title: patch.title,3129+        url: patch.url,3130+        username: patch.username,3131+        email: patch.email,3132+      });3133+    });3134+3135+    queueMicrotask(() => titleInput.focus());29923136   }29933137 29943138   function renderLocked() {

view file

src/popup/components/GeneratorDrawer.test.ts+7 −0
@@ -1,7 +1,14 @@1+import { readFileSync } from 'node:fs';12 import { describe, it, expect } from 'vitest';23 import { initFromPolicy, initGeneratorState, handleLengthChange } from '../generator-policy';34 45 describe('generator site policy constraints', () => {6+  it('renders the constraint note with a readable secondary-text token', () => {7+    const css = readFileSync('src/popup/styles/popup.css', 'utf8');8+    const rule = css.match(/\.peach-generator-constraint-note\s*\{([^}]*)\}/)?.[1];9+10+    expect(rule).toContain('color: var(--peach-t1)');11+  });512 613   describe('Case 1 — no policy', () => {714     it('defaults to 64 when unconstrained and fresh', () => {

view file

src/popup/styles/popup.css+1 −1
@@ -2764,7 +2764,7 @@27642764 .peach-generator-constraint-note {27652765   font-family: 'IBM Plex Mono', monospace;27662766   font-size: 10px;2767-  color: var(--peach-t3);2767+  color: var(--peach-t1);27682768   letter-spacing: 0.04em;27692769   margin: 0;27702770   transition: opacity 200ms ease;

view file