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
↓ peach-browser-f110a866.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1src/content/inject.ts+55 −46src/popup/views/Add.tsx+3 −15manifest.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": {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",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");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 )}