a7684e67e614

identity helper in Entry.tsx username/email; fix Add.tsx blur race when switching fields (v2.41.0)

time
2026-05-13 17:54 UTC · 5 months ago
machine
koh2
parent
886121cd569c
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/App.tsx+1 −0src/popup/views/Add.tsx+3 −3src/popup/views/Entry.tsx+70 −4
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.40.9",4+  "version": "2.41.0",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": "2.40.9",4+  "version": "2.41.0",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

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

view file

src/popup/App.tsx+1 −0
@@ -1318,6 +1318,7 @@13181318                 <EntryView13191319                   entry={selectedEntry()}13201320                   currentTabUrl={currentTabUrl()}1321+                  entries={entries()}13211322                   onDelete={handleDelete}13221323                   onFieldSaved={() => void loadEntries()}13231324                   onEscape={() => { setView("list"); }}

view file

src/popup/views/Add.tsx+3 −3
@@ -371,7 +371,7 @@371371                   onFocus={() => openIdentityField("username")}372372                   onBlur={(e) => {373373                     if (usernameSuggestEl?.contains(e.relatedTarget as Node)) return;374-                    setTimeout(() => setActiveIdentityField(null), 150);374+                    setTimeout(() => { if (activeIdentityField() === "username") setActiveIdentityField(null); }, 150);375375                   }}376376                   onKeyDown={(e) => {377377                     if (activeIdentityField() !== "username") return;@@ -411,7 +411,7 @@411411                   onFocus={() => openIdentityField("email")}412412                   onBlur={(e) => {413413                     if (emailSuggestEl?.contains(e.relatedTarget as Node)) return;414-                    setTimeout(() => setActiveIdentityField(null), 150);414+                    setTimeout(() => { if (activeIdentityField() === "email") setActiveIdentityField(null); }, 150);415415                   }}416416                   onKeyDown={(e) => {417417                     if (activeIdentityField() !== "email") return;@@ -701,7 +701,7 @@701701   inputRef?: () => HTMLInputElement | undefined;702702 }703703 704-function IdentitySuggestList(props: IdentitySuggestListProps) {704+export function IdentitySuggestList(props: IdentitySuggestListProps) {705705   const [collapsed, setCollapsed] = createSignal(false);706706   const ChevronIcon = () => (707707     <svg width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">

view file

src/popup/views/Entry.tsx+70 −4
@@ -9,6 +9,7 @@99 import { popupCopy } from "../copy";1010 import { GeneratorDrawer } from "../components/GeneratorDrawer";1111 import { NoteEditorInline, Toolbar } from "./NoteEditor";12+import { IdentitySuggestList } from "./Add";1213 1314 type EditableField = "title" | "url" | "username" | "email" | "password" | "notes" | "totp" | null;1415 @@ -24,6 +25,7 @@2425   /** Called after any inline save so App can reload entries */2526   onFieldSaved: () => void | Promise<void>;2627   onEscape?: () => void;28+  entries?: VaultEntry[];2729 }2830 2931 export function EntryView(props: EntryProps) {@@ -48,6 +50,11 @@4850   const [noteEditorEl, setNoteEditorEl] = createSignal<HTMLDivElement | null>(null);4951   // Auto-open advanced when entry already has passkey or canary configured5052   const [showAdvanced, setShowAdvanced] = createSignal(false);53+  const [identityHelperSuppressed, setIdentityHelperSuppressed] = createSignal(false);54+  let usernameInlineInput: HTMLInputElement | undefined;55+  let emailInlineInput: HTMLInputElement | undefined;56+  let usernameIdentityEl: HTMLDivElement | undefined;57+  let emailIdentityEl: HTMLDivElement | undefined;5158   let lastSavedNote: string | null = null;5259   let totpClickTimer: ReturnType<typeof setTimeout> | null = null;5360   onCleanup(() => { if (totpClickTimer) clearTimeout(totpClickTimer); });@@ -79,6 +86,36 @@7986     }8087   }8188 89+  const allIdentities = createMemo(() => {90+    const emailMap = new Map<string, number>();91+    const usernameMap = new Map<string, number>();92+    for (const e of props.entries ?? []) {93+      const uname = e.username?.trim() ?? "";94+      const em = e.email?.trim() ?? "";95+      if (uname) usernameMap.set(uname, (usernameMap.get(uname) ?? 0) + 1);96+      if (em) emailMap.set(em, (emailMap.get(em) ?? 0) + 1);97+    }98+    const result: Array<{ value: string; count: number; type: "email" | "username" }> = [];99+    const seen = new Set<string>();100+    for (const v of new Set([...emailMap.keys(), ...usernameMap.keys()])) {101+      if (seen.has(v)) continue;102+      seen.add(v);103+      const ec = emailMap.get(v) ?? 0;104+      const uc = usernameMap.get(v) ?? 0;105+      result.push({ value: v, count: Math.max(ec, uc), type: ec >= uc ? "email" : "username" });106+    }107+    return result.sort((a, b) => b.count - a.count);108+  });109+110+  const identitySuggestions = createMemo(() => {111+    const field = editingField();112+    if (field !== "username" && field !== "email") return [];113+    const input = draft().toLowerCase();114+    return allIdentities()115+      .filter(s => s.type === field && (!input || s.value.toLowerCase().includes(input)))116+      .slice(0, 8);117+  });118+82119   const interval = window.setInterval(() => setTotpTick((n) => n + 1), 1000);83120   onCleanup(() => window.clearInterval(interval));84121 @@ -167,6 +204,7 @@167204   function startEdit(field: EditableField, value: string) {168205     setEditingField(field);169206     setDraft(value);207+    setIdentityHelperSuppressed(false);170208     setShowGenerator(false);171209     setConfirmDelete(false);172210     if (field === "url") {@@ -476,6 +514,16 @@476514     if (e.key === "Escape") { e.preventDefault(); cancelEdit(); }477515   }478516 517+  function identityKeyDown(e: KeyboardEvent, field: "username" | "email") {518+    const listEl = field === "username" ? usernameIdentityEl : emailIdentityEl;519+    if (e.key === "ArrowDown" && listEl) {520+      e.preventDefault();521+      listEl.querySelector<HTMLElement>(".peach-identity-suggestion")?.focus();522+      return;523+    }524+    onEditKeyDown(e);525+  }526+479527   return (480528     <>481529       <div@@ -892,12 +940,21 @@892940                             class="peach-inline-input"893941                             value={draft()}894942                             onInput={(e) => setDraft(e.currentTarget.value)}895-                            onKeyDown={onEditKeyDown}896-                            ref={autoFocus}943+                            onKeyDown={(e) => identityKeyDown(e, "username")}944+                            ref={(el) => { usernameInlineInput = el; autoFocus(el); }}897945                             disabled={saving()}898946                             autocomplete="off"899947                           />900948                           <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />949+                          <Show when={!identityHelperSuppressed() && identitySuggestions().length > 0 && editingField() === "username"}>950+                            <IdentitySuggestList951+                              listRef={(el) => (usernameIdentityEl = el)}952+                              inputRef={() => usernameInlineInput}953+                              suggestions={identitySuggestions()}954+                              onPick={(v) => { setDraft(v); setIdentityHelperSuppressed(true); usernameInlineInput?.focus(); }}955+                              onClose={() => { setIdentityHelperSuppressed(true); usernameInlineInput?.focus(); }}956+                            />957+                          </Show>901958                           {/* +add the OTHER identity. If the slot is acting as902959                               email (value has @, no separate email), offer903960                               "+ add username" — clicking migrates the email@@ -965,13 +1022,22 @@9651022                             class="peach-inline-input"9661023                             value={draft()}9671024                             onInput={(e) => setDraft(e.currentTarget.value)}968-                            onKeyDown={onEditKeyDown}969-                            ref={autoFocus}1025+                            onKeyDown={(e) => identityKeyDown(e, "email")}1026+                            ref={(el) => { emailInlineInput = el; autoFocus(el); }}9701027                             disabled={saving()}9711028                             autocomplete="email"9721029                             type="email"9731030                           />9741031                           <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />1032+                          <Show when={!identityHelperSuppressed() && identitySuggestions().length > 0 && editingField() === "email"}>1033+                            <IdentitySuggestList1034+                              listRef={(el) => (emailIdentityEl = el)}1035+                              inputRef={() => emailInlineInput}1036+                              suggestions={identitySuggestions()}1037+                              onPick={(v) => { setDraft(v); setIdentityHelperSuppressed(true); emailInlineInput?.focus(); }}1038+                              onClose={() => { setIdentityHelperSuppressed(true); emailInlineInput?.focus(); }}1039+                            />1040+                          </Show>9751041                           <Show when={!entry().username || entry().username?.includes("@")}>9761042                             <button9771043                               class="peach-add-field-btn"

view file