768b657719fc
identity suggestions on username+email in Add.tsx (email field added, type filter toggle, allIdentities memo); inject.ts: on-demand fetch when savedIdentities empty, type badges + filter pills (v2.38.5)
- time
- 2026-05-10 19:44 UTC · 5 months ago
- machine
- koh2
- parent
- 90500f481978
↓ peach-browser-768b6577.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/inject.ts+74 −9src/popup/styles/popup.css+48 −6src/popup/views/Add.tsx+94 −31manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.38.4",4+ "version": "2.38.5",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.38.4",4+ "version": "2.38.5",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.38.4",3+ "version": "2.38.5",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/content/inject.ts+74 −9
@@ -1089,9 +1089,22 @@10891089 outline: none; padding: 0; min-width: 22px;10901090 }10911091 .identity-suggest-list { display: flex; flex-direction: column; gap: 1px; margin-top: 3px; }1092+ .identity-filter-row { display: flex; gap: 4px; padding: 2px 0 4px; }1093+ .identity-filter-pill {1094+ padding: 2px 7px; border-radius: 999px;1095+ border: 1px solid var(--peach-input-border);1096+ background: transparent; color: var(--peach-text-muted);1097+ font-family: "IBM Plex Mono", monospace; font-size: 9px;1098+ cursor: pointer; transition: border-color 80ms, background 80ms, color 80ms;1099+ }1100+ .identity-filter-pill.is-on {1101+ border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1102+ background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1103+ color: var(--peach-accent);1104+ }10921105 .identity-suggest-row {1093- display: flex; align-items: center; justify-content: space-between;1094- width: 100%; padding: 4px 8px; border-radius: 5px; border: none;1106+ display: flex; align-items: center; gap: 5px;1107+ width: 100%; padding: 4px 6px; border-radius: 5px; border: none;10951108 background: transparent; cursor: pointer; text-align: left;10961109 transition: background 80ms;10971110 }@@ -1100,9 +1113,21 @@11001113 font-family: "IBM Plex Mono", monospace; font-size: 10px; color: var(--peach-text);11011114 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;11021115 }1116+ .identity-suggest-type {1117+ font-family: "IBM Plex Mono", monospace; font-size: 8px; font-weight: 600;1118+ flex-shrink: 0; padding: 1px 3px; border-radius: 3px;1119+ }1120+ .identity-suggest-type--email {1121+ color: color-mix(in srgb, var(--peach-accent) 70%, transparent);1122+ background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1123+ }1124+ .identity-suggest-type--username {1125+ color: var(--peach-text-muted);1126+ background: color-mix(in srgb, var(--peach-text) 8%, transparent);1127+ }11031128 .identity-suggest-meta {11041129 font-family: "IBM Plex Mono", monospace; font-size: 9px;1105- color: var(--peach-text-muted); flex-shrink: 0; margin-left: 6px;1130+ color: var(--peach-text-muted); flex-shrink: 0;11061131 }11071132 .pwgen-pills { display: flex; gap: 5px; flex-wrap: wrap; }11081133 .pwgen-pill {@@ -1566,19 +1591,49 @@15661591 15671592 function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[]): void {15681593 if (!identities.length) return;1594+ let activeFilter: "all" | "username" | "email" = "all";1595+15691596 const list = g.appendChild(document.createElement("div"));15701597 list.className = "identity-suggest-list";15711598 list.style.display = "none";1599+1600+ // Filter pill row1601+ const filterRow = list.appendChild(document.createElement("div"));1602+ filterRow.className = "identity-filter-row";1603+ function makePill(label: string, value: "all" | "username" | "email") {1604+ const b = filterRow.appendChild(document.createElement("button"));1605+ b.type = "button"; b.className = "identity-filter-pill"; b.textContent = label;1606+ b.addEventListener("mousedown", (e) => {1607+ e.preventDefault();1608+ activeFilter = value;1609+ filterRow.querySelectorAll(".identity-filter-pill").forEach(p => p.classList.remove("is-on"));1610+ b.classList.add("is-on");1611+ render();1612+ });1613+ if (value === "all") b.classList.add("is-on");1614+ return b;1615+ }1616+ makePill("all", "all");1617+ makePill("user", "username");1618+ makePill("email", "email");1619+1620+ const rows = list.appendChild(document.createElement("div"));1621+15721622 function render() {15731623 const q = inp.value.toLowerCase();1574- const filtered = identities.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 6);1575- list.innerHTML = "";1576- if (!filtered.length) { list.style.display = "none"; return; }1624+ const filtered = identities1625+ .filter(s => (activeFilter === "all" || s.type === activeFilter) && (!q || s.value.toLowerCase().includes(q)))1626+ .slice(0, 8);1627+ rows.innerHTML = "";1628+ if (!filtered.length) { list.style.display = rows.children.length === 0 ? "none" : ""; return; }15771629 for (const s of filtered) {1578- const row = list.appendChild(document.createElement("button"));1630+ const row = rows.appendChild(document.createElement("button"));15791631 row.type = "button"; row.className = "identity-suggest-row";15801632 const val = row.appendChild(document.createElement("span"));15811633 val.className = "identity-suggest-val"; val.textContent = s.value;1634+ const badge = row.appendChild(document.createElement("span"));1635+ badge.className = `identity-suggest-type identity-suggest-type--${s.type}`;1636+ badge.textContent = s.type === "email" ? "@" : "u";15821637 const meta = row.appendChild(document.createElement("span"));15831638 meta.className = "identity-suggest-meta"; meta.textContent = `${s.useCount}×`;15841639 row.addEventListener("mousedown", (e) => {@@ -1611,6 +1666,16 @@16111666 // regenerate click so the user always gets a compliant password.16121667 const sitePolicy = detectPasswordPolicy();16131668 1669+ // Fetch identities on-demand — they're passed in signup context but not1670+ // in login/search context where the user still clicks "New entry".1671+ let formIdentities: SavedIdentity[] = options.savedIdentities ?? [];1672+ if (!formIdentities.length) {1673+ try {1674+ const idRes = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as { ok: boolean; data?: SavedIdentity[] } | null;1675+ if (idRes?.ok && Array.isArray(idRes.data)) formIdentities = idRes.data;1676+ } catch { /* silent fallback */ }1677+ }1678+16141679 const wrap = document.createElement("div");16151680 wrap.className = "create-form-wrap";16161681 @@ -1650,7 +1715,7 @@16501715 xg.inp.value = preUsername && !preUsername.includes("@") ? preUsername : "";16511716 xg.inp.placeholder = "johndoe";16521717 xg.inp.setAttribute("autocomplete", "off");1653- if (options.savedIdentities?.length) attachIdentitySuggestions(xg.g, xg.inp, options.savedIdentities);1718+ if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities);16541719 body.appendChild(xg.g);16551720 16561721 // Email — pre-fill from anchor if it looks like an email@@ -1659,7 +1724,7 @@16591724 eg.inp.value = preUsername && preUsername.includes("@") ? preUsername : "";16601725 eg.inp.placeholder = "[email protected]";16611726 eg.inp.setAttribute("autocomplete", "off");1662- if (options.savedIdentities?.length) attachIdentitySuggestions(eg.g, eg.inp, options.savedIdentities);1727+ if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities);16631728 body.appendChild(eg.g);16641729 16651730 // Password — full smart generator with charset pills + length slider +src/popup/styles/popup.css+48 −6
@@ -5127,21 +5127,45 @@51275127 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);51285128 }51295129 5130-/* Identity suggestions — shown below username input in new-entry form */5130+/* Identity suggestions — shown below username/email inputs in new-entry form */51315131 .peach-identity-suggestions {51325132 display: flex;51335133 flex-direction: column;5134- gap: 2px;5134+ gap: 1px;51355135 margin-top: 4px;51365136 }51375137 5138+.peach-identity-filter-row {5139+ display: flex;5140+ gap: 4px;5141+ padding: 2px 0 5px;5142+}5143+5144+.peach-identity-filter-pill {5145+ padding: 2px 7px;5146+ border-radius: 999px;5147+ border: 1px solid var(--peach-t3);5148+ background: transparent;5149+ color: var(--peach-t2);5150+ font-family: "IBM Plex Mono", monospace;5151+ font-size: 0.6rem;5152+ cursor: pointer;5153+ transition: border-color 80ms, background 80ms, color 80ms;5154+}5155+5156+.peach-identity-filter-pill.is-on {5157+ border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);5158+ background: color-mix(in srgb, var(--peach-accent) 10%, transparent);5159+ color: var(--peach-accent);5160+}5161+51385162 .peach-identity-suggestion {51395163 display: flex;51405164 align-items: center;5141- justify-content: space-between;5165+ gap: 6px;51425166 width: 100%;5143- padding: 5px 8px;5144- border-radius: 6px;5167+ padding: 4px 6px;5168+ border-radius: 5px;51455169 border: none;51465170 background: transparent;51475171 cursor: pointer;@@ -5164,12 +5188,30 @@51645188 min-width: 0;51655189 }51665190 5191+.peach-identity-suggestion-type {5192+ font-family: "IBM Plex Mono", monospace;5193+ font-size: 0.55rem;5194+ font-weight: 600;5195+ flex-shrink: 0;5196+ padding: 1px 4px;5197+ border-radius: 3px;5198+}5199+5200+.peach-identity-suggestion-type--email {5201+ color: color-mix(in srgb, var(--peach-accent) 70%, transparent);5202+ background: color-mix(in srgb, var(--peach-accent) 10%, transparent);5203+}5204+5205+.peach-identity-suggestion-type--username {5206+ color: color-mix(in srgb, var(--peach-t1) 60%, transparent);5207+ background: color-mix(in srgb, var(--peach-t3) 40%, transparent);5208+}5209+51675210 .peach-identity-suggestion-meta {51685211 font-family: "IBM Plex Mono", monospace;51695212 font-size: 0.6rem;51705213 color: var(--peach-t3);51715214 flex-shrink: 0;5172- margin-left: 8px;51735215 }51745216 51755217 /* Tag filter strip */src/popup/views/Add.tsx+94 −31
@@ -31,24 +31,41 @@3131 const [showPassword, setShowPassword] = createSignal(false);3232 const [showTotp, setShowTotp] = createSignal(!!(props.entry?.totp));3333 const [showNotes, setShowNotes] = createSignal(!!(props.entry?.notes));34- const [showIdentitySuggestions, setShowIdentitySuggestions] = createSignal(false);34+ const [email, setEmail] = createSignal((props.entry as VaultEntry & { email?: string })?.email ?? "");35+ const [activeIdentityField, setActiveIdentityField] = createSignal<"username" | "email" | null>(null);36+ const [identityFilter, setIdentityFilter] = createSignal<"all" | "username" | "email">("all");3537 36- const identitySuggestions = createMemo(() => {37- const input = username().toLowerCase();38- const counts = new Map<string, number>();38+ const allIdentities = createMemo(() => {39+ const emailMap = new Map<string, number>();40+ const usernameMap = new Map<string, number>();3941 for (const entry of props.entries ?? []) {4042 const uname = entry.username?.trim() ?? "";41- const email = (entry as VaultEntry & { email?: string }).email?.trim() ?? "";42- if (uname) counts.set(uname, (counts.get(uname) ?? 0) + 1);43- if (email && email !== uname) counts.set(email, (counts.get(email) ?? 0) + 1);43+ const em = (entry as VaultEntry & { email?: string }).email?.trim() ?? "";44+ if (uname) usernameMap.set(uname, (usernameMap.get(uname) ?? 0) + 1);45+ if (em) emailMap.set(em, (emailMap.get(em) ?? 0) + 1);4446 }45- return Array.from(counts.entries())46- .filter(([val]) => !input || val.toLowerCase().includes(input))47- .sort((a, b) => b[1] - a[1])48- .slice(0, 6)49- .map(([value, count]) => ({ value, count }));47+ const result: Array<{ value: string; count: number; type: "email" | "username" }> = [];48+ const seen = new Set<string>();49+ for (const v of new Set([...emailMap.keys(), ...usernameMap.keys()])) {50+ if (seen.has(v)) continue;51+ seen.add(v);52+ const ec = emailMap.get(v) ?? 0;53+ const uc = usernameMap.get(v) ?? 0;54+ result.push({ value: v, count: Math.max(ec, uc), type: ec >= uc ? "email" : "username" });55+ }56+ return result.sort((a, b) => b.count - a.count);5057 });5158 59+ const identitySuggestions = createMemo(() => {60+ const field = activeIdentityField();61+ if (!field) return [];62+ const input = (field === "username" ? username() : email()).toLowerCase();63+ const filter = identityFilter();64+ return allIdentities()65+ .filter(s => (filter === "all" || s.type === filter) && (!input || s.value.toLowerCase().includes(input)))66+ .slice(0, 8);67+ });68+5269 const isNoteEntry = () => props.entry?.type === "note";5370 const isCardEntry = () => props.entry?.type === "card";5471 const [selectedType, setSelectedType] = createSignal<"login" | "note" | "card">(@@ -184,6 +201,7 @@184201 185202 if (selectedType() === "login") {186203 payload.username = username().trim();204+ payload.email = email().trim() || undefined;187205 payload.password = password();188206 payload.url = url().trim();189207 payload.totp = getNormalizedTotp();@@ -308,32 +326,49 @@308326 <div class="peach-field-value-row">309327 <input310328 class="peach-field-value peach-field-value--edit"311- placeholder="username or email"329+ placeholder="johndoe"312330 autocomplete="off"313331 value={username()}314- onInput={(e) => { setUsername((e.currentTarget as HTMLInputElement).value); setShowIdentitySuggestions(true); }}315- onFocus={() => setShowIdentitySuggestions(true)}316- onBlur={() => setTimeout(() => setShowIdentitySuggestions(false), 150)}332+ onInput={(e) => setUsername((e.currentTarget as HTMLInputElement).value)}333+ onFocus={() => setActiveIdentityField("username")}334+ onBlur={() => setTimeout(() => setActiveIdentityField(null), 150)}317335 />318336 </div>319- <Show when={showIdentitySuggestions() && identitySuggestions().length > 0}>320- <div class="peach-identity-suggestions">321- <For each={identitySuggestions()}>322- {(s) => (323- <button324- class="peach-identity-suggestion"325- type="button"326- onMouseDown={() => { setUsername(s.value); setShowIdentitySuggestions(false); }}327- >328- <span class="peach-identity-suggestion-value">{s.value}</span>329- <span class="peach-identity-suggestion-meta">{s.count}×</span>330- </button>331- )}332- </For>333- </div>337+ <Show when={activeIdentityField() === "username" && identitySuggestions().length > 0}>338+ <IdentitySuggestList339+ suggestions={identitySuggestions()}340+ filter={identityFilter()}341+ onFilter={setIdentityFilter}342+ onPick={(v) => { setUsername(v); setActiveIdentityField(null); }}343+ />334344 </Show>335345 </div>336346 347+ {/* Email */}348+ <div class="peach-field-card">349+ <span class="peach-label">email</span>350+ <div class="peach-field-value-row">351+ <input352+ class="peach-field-value peach-field-value--edit"353+ type="email"354+ placeholder="[email protected]"355+ autocomplete="off"356+ value={email()}357+ onInput={(e) => setEmail((e.currentTarget as HTMLInputElement).value)}358+ onFocus={() => setActiveIdentityField("email")}359+ onBlur={() => setTimeout(() => setActiveIdentityField(null), 150)}360+ />361+ </div>362+ <Show when={activeIdentityField() === "email" && identitySuggestions().length > 0}>363+ <IdentitySuggestList364+ suggestions={identitySuggestions()}365+ filter={identityFilter()}366+ onFilter={setIdentityFilter}367+ onPick={(v) => { setEmail(v); setActiveIdentityField(null); }}368+ />369+ </Show>370+ </div>371+337372 {/* Password */}338373 <div class="peach-field-card">339374 <span class="peach-label">password</span>@@ -584,6 +619,34 @@584619 </Show>585620 586621 </div>622+ </div>623+ );624+}625+626+interface IdentitySuggestListProps {627+ suggestions: Array<{ value: string; count: number; type: "email" | "username" }>;628+ filter: "all" | "username" | "email";629+ onFilter: (f: "all" | "username" | "email") => void;630+ onPick: (value: string) => void;631+}632+633+function IdentitySuggestList(props: IdentitySuggestListProps) {634+ return (635+ <div class="peach-identity-suggestions">636+ <div class="peach-identity-filter-row">637+ <button class={`peach-identity-filter-pill ${props.filter === "all" ? "is-on" : ""}`} type="button" onMouseDown={() => props.onFilter("all")}>all</button>638+ <button class={`peach-identity-filter-pill ${props.filter === "username" ? "is-on" : ""}`} type="button" onMouseDown={() => props.onFilter("username")}>user</button>639+ <button class={`peach-identity-filter-pill ${props.filter === "email" ? "is-on" : ""}`} type="button" onMouseDown={() => props.onFilter("email")}>email</button>640+ </div>641+ <For each={props.suggestions}>642+ {(s) => (643+ <button class="peach-identity-suggestion" type="button" onMouseDown={() => props.onPick(s.value)}>644+ <span class="peach-identity-suggestion-value">{s.value}</span>645+ <span class={`peach-identity-suggestion-type peach-identity-suggestion-type--${s.type}`}>{s.type === "email" ? "@" : "u"}</span>646+ <span class="peach-identity-suggestion-meta">{s.count}×</span>647+ </button>648+ )}649+ </For>587650 </div>588651 );589652 }