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
6 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 −31
manifest.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": {

view file

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

view file

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

view file

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 +

view file

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

view file

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 }

view file