d5f614d9f6b0
filter passkey-only + notes from inline autofill selector, add passkey badge to entries with both password and passkey, move tags section above TOTP so it stays discoverable when 2FA is present, bump v2.11.1
- time
- 2026-04-28 22:25 UTC · 6 months ago
- machine
- koh2
- parent
- ca652c13429f
↓ peach-browser-d5f614d9.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.ts+14 −5src/content/inject.ts+14 −0src/popup/views/Entry.tsx+61 −63manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.11.0",4+ "version": "2.11.1",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.11.0",4+ "version": "2.11.1",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.11.0",3+ "version": "2.11.1",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/messages.ts+14 −5
@@ -265,11 +265,18 @@265265 })),266266 };267267 }268- const matches = includeAll269- ? vault270- .getEntries()271- .filter((entry) => entry.type !== "note" && entry.type !== "passkey" && entry.type !== "card")272- : vault.queryDomain(domain);268+ // The inline selector fills username+password into a form. Passkey-269+ // only entries have no password to fill; sign-in for those goes270+ // through the WebAuthn path instead. Filter them out everywhere so271+ // the user doesn't see four "GitHub" rows that mostly do nothing.272+ // Notes and cards never belonged here either.273+ const matches = (274+ includeAll275+ ? vault276+ .getEntries()277+ .filter((entry) => entry.type !== "note" && entry.type !== "card")278+ : vault.queryDomain(domain)279+ ).filter((entry) => !isPasskeyOnly(entry));273280 return {274281 ok: true,275282 data: matches.map((e) =>@@ -277,6 +284,7 @@277284 ? {278285 id: e.id,279286 title: e.title,287+ hasPasskey: e.passkey !== undefined,280288 }281289 : {282290 id: e.id,@@ -284,6 +292,7 @@284292 username: e.username,285293 url: e.url,286294 hasTotp: Boolean(e.totp),295+ hasPasskey: e.passkey !== undefined,287296 }288297 ),289298 };src/content/inject.ts+14 −0
@@ -39,6 +39,8 @@3939 username?: string;4040 url?: string;4141 hasTotp?: boolean;42+ /** True when the entry also has a passkey credential attached. */43+ hasPasskey?: boolean;4244 }4345 4446 interface SelectorOptions {@@ -447,6 +449,14 @@447449 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);448450 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;449451 }452+ .passkey-badge {453+ font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;454+ border-radius: 4px;455+ background: color-mix(in srgb, var(--peach-accent) 14%, transparent);456+ color: color-mix(in srgb, var(--peach-accent) 85%, transparent);457+ border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);458+ font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;459+ }450460 451461 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }452462 @@ -1152,6 +1162,10 @@11521162 11531163 if (e.hasTotp) {11541164 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";1165+ entryEl.appendChild(badge);1166+ }1167+ if (e.hasPasskey) {1168+ const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";11551169 entryEl.appendChild(badge);11561170 }11571171 src/popup/views/Entry.tsx+61 −63
@@ -918,6 +918,67 @@918918 </Show>919919 </Show>920920 921+ {/* ── Tags editor / add-tags link (between notes and TOTP) ─── */}922+ <Show923+ when={editingTags()}924+ fallback={925+ <Show when={!entry().tags || entry().tags!.length === 0}>926+ <button927+ class="peach-add-field-link peach-entry-add-link"928+ onClick={startEditTags}929+ >930+ + tags931+ </button>932+ </Show>933+ }934+ >935+ <div936+ class="peach-inline-tag-editor"937+ onFocusOut={(e) => {938+ const next = e.relatedTarget;939+ if (next instanceof Node && e.currentTarget.contains(next)) return;940+ void saveTags();941+ }}942+ >943+ <span class="peach-label">tags</span>944+ <div class="peach-tag-input-wrap">945+ <For each={tagsDraft()}>946+ {(tag) => (947+ <span class="peach-tag-chip">948+ {tag}949+ <button950+ class="peach-tag-chip-remove"951+ onMouseDown={(e) => e.preventDefault()}952+ onClick={() => setTagsDraft(tagsDraft().filter(t => t !== tag))}953+ aria-label={`Remove ${tag}`}954+ >×</button>955+ </span>956+ )}957+ </For>958+ <input959+ class="peach-tag-input"960+ placeholder={tagsDraft().length === 0 ? "type and press Enter…" : ""}961+ value={tagInput()}962+ onInput={(e) => setTagInput(e.currentTarget.value)}963+ onKeyDown={(e) => {964+ if (e.key === "Enter" || e.key === ",") {965+ e.preventDefault();966+ addTag(tagInput());967+ } else if (e.key === "Backspace" && !tagInput() && tagsDraft().length > 0) {968+ setTagsDraft(tagsDraft().slice(0, -1));969+ } else if (e.key === "Escape") {970+ e.preventDefault();971+ setEditingTags(false);972+ setTagInput("");973+ }974+ }}975+ ref={(el) => requestAnimationFrame(() => el?.focus())}976+ style="flex:1;min-width:80px;background:transparent;border:none;outline:none;color:var(--t1);font-size:12px;padding:2px;"977+ />978+ </div>979+ </div>980+ </Show>981+921982 {/* ── TOTP ─────────────────────────────────────────── */}922983 <Show when={entry().type === "login" && !isPasskeyOnly(entry())}>923984 <Show@@ -1071,69 +1132,6 @@10711132 </section>10721133 );10731134 }}1074- </Show>1075-1076- {/* ── Tags editor / add-tags link ──────────────────── */}1077- <Show when={true}>1078- <Show1079- when={editingTags()}1080- fallback={1081- <Show when={!entry().tags || entry().tags!.length === 0}>1082- <button1083- class="peach-add-field-link peach-entry-add-link"1084- onClick={startEditTags}1085- >1086- + tags1087- </button>1088- </Show>1089- }1090- >1091- <div1092- class="peach-inline-tag-editor"1093- onFocusOut={(e) => {1094- const next = e.relatedTarget;1095- if (next instanceof Node && e.currentTarget.contains(next)) return;1096- void saveTags();1097- }}1098- >1099- <span class="peach-label">tags</span>1100- <div class="peach-tag-input-wrap">1101- <For each={tagsDraft()}>1102- {(tag) => (1103- <span class="peach-tag-chip">1104- {tag}1105- <button1106- class="peach-tag-chip-remove"1107- onMouseDown={(e) => e.preventDefault()}1108- onClick={() => setTagsDraft(tagsDraft().filter(t => t !== tag))}1109- aria-label={`Remove ${tag}`}1110- >×</button>1111- </span>1112- )}1113- </For>1114- <input1115- class="peach-tag-input"1116- placeholder={tagsDraft().length === 0 ? "type and press Enter…" : ""}1117- value={tagInput()}1118- onInput={(e) => setTagInput(e.currentTarget.value)}1119- onKeyDown={(e) => {1120- if (e.key === "Enter" || e.key === ",") {1121- e.preventDefault();1122- addTag(tagInput());1123- } else if (e.key === "Backspace" && !tagInput() && tagsDraft().length > 0) {1124- setTagsDraft(tagsDraft().slice(0, -1));1125- } else if (e.key === "Escape") {1126- e.preventDefault();1127- setEditingTags(false);1128- setTagInput("");1129- }1130- }}1131- ref={(el) => requestAnimationFrame(() => el?.focus())}1132- style="flex:1;min-width:80px;background:transparent;border:none;outline:none;color:var(--t1);font-size:12px;padding:2px;"1133- />1134- </div>1135- </div>1136- </Show>11371135 </Show>11381136 11391137 {/* ── Bottom spacer ─────────────────────────────────── */}