f110a866ba42

auto-filter by field type, keep collapse toggle, smooth height animation on inline popup (v2.38.16)

time
2026-05-12 17:07 UTC · 5 months ago
machine
koh2
parent
8f57c111acab
4 files manifest.firefox.json+1 −1manifest.json+1 −1src/content/inject.ts+55 −46src/popup/views/Add.tsx+3 −15
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.38.15",4+  "version": "2.38.16",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.15",4+  "version": "2.38.16",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

src/content/inject.ts+55 −46
@@ -536,6 +536,7 @@536536       0 2px 12px rgba(0,0,0,0.5);537537     font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;538538     animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;539+    transition: height 220ms cubic-bezier(0.16,1,0.3,1);539540   }540541   /* Light theme: shadow against a light page is too strong otherwise — drop both541542      the accent glow and the dark drop-shadow opacity to keep the popup elegant. */@@ -1249,6 +1250,37 @@12491250   // setInterval. Add to this list — don't fan out the dismiss function.12501251   const cleanupHooks: Array<() => void> = [];12511252 1253+  // Smooth height transitions whenever content changes height.1254+  // ResizeObserver fires on natural height changes; we freeze the old height,1255+  // then animate to the new one so the surface never jumps.1256+  {1257+    let prevH = 0;1258+    let blocked = false;1259+    const heightRo = new ResizeObserver(([entry]) => {1260+      if (blocked) return;1261+      const h = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;1262+      if (prevH > 0 && Math.abs(h - prevH) > 1) {1263+        blocked = true;1264+        container.style.height = `${prevH}px`;1265+        requestAnimationFrame(() => requestAnimationFrame(() => {1266+          container.style.height = `${h}px`;1267+          prevH = h;1268+          const onEnd = (e: TransitionEvent) => {1269+            if (e.propertyName !== "height") return;1270+            container.style.height = "";1271+            blocked = false;1272+            container.removeEventListener("transitionend", onEnd);1273+          };1274+          container.addEventListener("transitionend", onEnd);1275+        }));1276+      } else {1277+        prevH = h;1278+      }1279+    });1280+    heightRo.observe(container);1281+    cleanupHooks.push(() => heightRo.disconnect());1282+  }1283+12521284   // Pre-capture the anchor field value for username prefill12531285   const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";12541286   const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");@@ -1611,9 +1643,9 @@16111643     if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });16121644   }16131645 1614-  function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[]): void {1615-    if (!identities.length) return;1616-    let activeFilter: "all" | "username" | "email" = "all";1646+  function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[], fieldType: "username" | "email"): void {1647+    const typed = identities.filter(s => s.type === fieldType);1648+    if (!typed.length) return;16171649     let collapsed = false;16181650     let suppressNextFocus = false;16191651 @@ -1621,34 +1653,11 @@16211653     list.className = "identity-suggest-list";16221654     list.style.display = "none";16231655 1624-    function pick(value: string) {1625-      inp.value = value;1626-      list.style.display = "none";1627-      suppressNextFocus = true;1628-      inp.focus();1629-    }1630-1631-    // Filter pill row + collapse toggle1632-    const filterRow = list.appendChild(document.createElement("div"));1633-    filterRow.className = "identity-filter-row";1634-    function makePill(label: string, value: "all" | "username" | "email") {1635-      const b = filterRow.appendChild(document.createElement("button"));1636-      b.type = "button"; b.className = "identity-filter-pill"; b.textContent = label;1637-      b.addEventListener("mousedown", (e) => {1638-        e.preventDefault();1639-        activeFilter = value;1640-        filterRow.querySelectorAll(".identity-filter-pill").forEach(p => p.classList.remove("is-on"));1641-        b.classList.add("is-on");1642-        render();1643-      });1644-      if (value === "all") b.classList.add("is-on");1645-      return b;1646-    }1647-    makePill("all", "all");1648-    makePill("user", "username");1649-    makePill("email", "email");1650-1651-    const collapseBtn = filterRow.appendChild(document.createElement("button"));1656+    // Collapse toggle — sits alone now that filter pills are gone1657+    const collapseRow = list.appendChild(document.createElement("div"));1658+    collapseRow.className = "identity-filter-row";1659+    collapseRow.style.justifyContent = "flex-end";1660+    const collapseBtn = collapseRow.appendChild(document.createElement("button"));16521661     collapseBtn.type = "button";16531662     collapseBtn.className = "identity-collapse-btn";16541663     collapseBtn.innerHTML = `<svg width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1L5 5L9 1"/></svg>`;@@ -1664,23 +1673,23 @@16641673     const rows = rowsWrapper.appendChild(document.createElement("div"));16651674     rows.className = "identity-rows-inner";16661675 1676+    function pick(value: string) {1677+      inp.value = value;1678+      list.style.display = "none";1679+      suppressNextFocus = true;1680+      inp.focus();1681+    }1682+16671683     function render() {16681684       const q = inp.value.toLowerCase();1669-      const filtered = identities1670-        .filter(s => (activeFilter === "all" || s.type === activeFilter) && (!q || s.value.toLowerCase().includes(q)))1671-        .slice(0, 8);1685+      const filtered = typed.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 8);16721686       rows.innerHTML = "";1673-      // Keep the list visible when the filter produces no results — hide only1674-      // when there are genuinely no identities to show (handled by the caller).1675-      if (!filtered.length) return;1687+      if (!filtered.length) { list.style.display = "none"; return; }16761688       for (const s of filtered) {16771689         const row = rows.appendChild(document.createElement("button"));16781690         row.type = "button"; row.className = "identity-suggest-row";16791691         const val = row.appendChild(document.createElement("span"));16801692         val.className = "identity-suggest-val"; val.textContent = s.value;1681-        const badge = row.appendChild(document.createElement("span"));1682-        badge.className = `identity-suggest-type identity-suggest-type--${s.type}`;1683-        badge.textContent = s.type === "email" ? "@" : "u";16841693         const meta = row.appendChild(document.createElement("span"));16851694         meta.className = "identity-suggest-meta"; meta.textContent = `${s.useCount}×`;16861695         row.addEventListener("mousedown", (e) => { e.preventDefault(); pick(s.value); });@@ -1692,7 +1701,7 @@16921701             e.preventDefault();16931702             const prev = row.previousElementSibling as HTMLElement | null;16941703             if (prev) prev.focus();1695-            else inp.focus(); // back to input, list stays visible via focus handler1704+            else inp.focus();16961705           } else if (e.key === "Enter" || e.key === " ") {16971706             e.preventDefault();16981707             pick(s.value);@@ -1729,7 +1738,7 @@17291738     });17301739     inp.addEventListener("blur", (e) => {17311740       const rt = (e as FocusEvent).relatedTarget as Node | null;1732-      if (rt && list.contains(rt)) return; // focus moved into suggestion list, keep open1741+      if (rt && list.contains(rt)) return;17331742       setTimeout(() => { list.style.display = "none"; }, 150);17341743     });17351744   }@@ -1800,7 +1809,7 @@18001809     xg.inp.value = preUsername && !preUsername.includes("@") ? preUsername : "";18011810     xg.inp.placeholder = "johndoe";18021811     xg.inp.setAttribute("autocomplete", "off");1803-    if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities);1812+    if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities, "username");18041813     body.appendChild(xg.g);18051814 18061815     // Email — pre-fill from anchor if it looks like an email@@ -1809,7 +1818,7 @@18091818     eg.inp.value = preUsername && preUsername.includes("@") ? preUsername : "";18101819     eg.inp.placeholder = "[email protected]";18111820     eg.inp.setAttribute("autocomplete", "off");1812-    if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities);1821+    if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities, "email");18131822     body.appendChild(eg.g);18141823 18151824     // Password — full smart generator with charset pills + length slider +@@ -2220,9 +2229,9 @@22202229     urlInput.classList.add("url-f");22212230     const usernameLabel = (entryRes.data.username ?? "").includes("@") ? "email" : "username";22222231     const { g: usernameGroup, inp: usernameInput } = field(usernameLabel, entryRes.data.username ?? "");2223-    if (editIdentities.length) attachIdentitySuggestions(usernameGroup, usernameInput, editIdentities);2232+    if (editIdentities.length) attachIdentitySuggestions(usernameGroup, usernameInput, editIdentities, "username");22242233     const { g: emailGroup, inp: emailInput } = field("email", entryRes.data.email ?? "");2225-    if (editIdentities.length) attachIdentitySuggestions(emailGroup, emailInput, editIdentities);2234+    if (editIdentities.length) attachIdentitySuggestions(emailGroup, emailInput, editIdentities, "email");22262235 22272236     // Password row with reveal toggle22282237     const pwGroup = document.createElement("div");

view file

src/popup/views/Add.tsx+3 −15
@@ -34,7 +34,6 @@3434   const [email, setEmail] = createSignal((props.entry as VaultEntry & { email?: string })?.email ?? "");3535   const [activeIdentityField, setActiveIdentityField] = createSignal<"username" | "email" | null>(null);3636   const [currentTabUrl, setCurrentTabUrl] = createSignal("");37-  const [identityFilter, setIdentityFilter] = createSignal<"all" | "username" | "email">("all");3837 3938   // Mutable refs for keyboard navigation between inputs and suggestion lists.4039   // Not signals — focus state doesn't need to be reactive.@@ -74,9 +73,8 @@7473     const field = activeIdentityField();7574     if (!field) return [];7675     const input = (field === "username" ? username() : email()).toLowerCase();77-    const filter = identityFilter();7876     return allIdentities()79-      .filter(s => (filter === "all" || s.type === filter) && (!input || s.value.toLowerCase().includes(input)))77+      .filter(s => s.type === field && (!input || s.value.toLowerCase().includes(input)))8078       .slice(0, 8);8179   });8280 @@ -392,8 +390,6 @@392390                   listRef={(el) => (usernameSuggestEl = el)}393391                   inputRef={() => usernameInputEl}394392                   suggestions={identitySuggestions()}395-                  filter={identityFilter()}396-                  onFilter={setIdentityFilter}397393                   onPick={(v) => { setUsername(v); setActiveIdentityField(null); suppressIdentityOpen = true; usernameInputEl?.focus(); }}398394                   onClose={() => { setActiveIdentityField(null); suppressIdentityOpen = true; usernameInputEl?.focus(); }}399395                 />@@ -434,8 +430,6 @@434430                   listRef={(el) => (emailSuggestEl = el)}435431                   inputRef={() => emailInputEl}436432                   suggestions={identitySuggestions()}437-                  filter={identityFilter()}438-                  onFilter={setIdentityFilter}439433                   onPick={(v) => { setEmail(v); setActiveIdentityField(null); suppressIdentityOpen = true; emailInputEl?.focus(); }}440434                   onClose={() => { setActiveIdentityField(null); suppressIdentityOpen = true; emailInputEl?.focus(); }}441435                 />@@ -698,8 +692,6 @@698692 699693 interface IdentitySuggestListProps {700694   suggestions: Array<{ value: string; count: number; type: "email" | "username" }>;701-  filter: "all" | "username" | "email";702-  onFilter: (f: "all" | "username" | "email") => void;703695   onPick: (value: string) => void;704696   onClose?: () => void;705697   listRef?: (el: HTMLDivElement) => void;@@ -723,7 +715,7 @@723715       e.preventDefault();724716       const prev = row.previousElementSibling as HTMLElement | null;725717       if (prev) prev.focus();726-      else props.inputRef?.()?.focus(); // back to input, list stays open718+      else props.inputRef?.()?.focus();727719     } else if (e.key === "Enter" || e.key === " ") {728720       e.preventDefault();729721       props.onPick(value);@@ -735,10 +727,7 @@735727 736728   return (737729     <div class="peach-identity-suggestions" ref={props.listRef}>738-      <div class="peach-identity-filter-row">739-        <button class={`peach-identity-filter-pill ${props.filter === "all" ? "is-on" : ""}`} type="button" onMouseDown={(e) => { e.preventDefault(); props.onFilter("all"); }}>all</button>740-        <button class={`peach-identity-filter-pill ${props.filter === "username" ? "is-on" : ""}`} type="button" onMouseDown={(e) => { e.preventDefault(); props.onFilter("username"); }}>user</button>741-        <button class={`peach-identity-filter-pill ${props.filter === "email" ? "is-on" : ""}`} type="button" onMouseDown={(e) => { e.preventDefault(); props.onFilter("email"); }}>email</button>730+      <div class="peach-identity-filter-row" style="justify-content:flex-end">742731         <button743732           class={`peach-identity-collapse-btn${collapsed() ? " is-collapsed" : ""}`}744733           type="button"@@ -759,7 +748,6 @@759748                 onKeyDown={(e) => handleRowKeyDown(e, s.value)}760749               >761750                 <span class="peach-identity-suggestion-value">{s.value}</span>762-                <span class={`peach-identity-suggestion-type peach-identity-suggestion-type--${s.type}`}>{s.type === "email" ? "@" : "u"}</span>763751                 <span class="peach-identity-suggestion-meta">{s.count}×</span>764752               </button>765753             )}

view file