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
↓ peach-browser-a7684e67.tar.gzatlas at this save
files changed6 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 −4manifest.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": {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",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": {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"); }}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">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"