d08bf4e3dd8a
add email field to entries; smart email/username label; +Add Email button; inline popup label fix
- time
- 2026-05-05 15:59 UTC · 5 months ago
- machine
- koh2
- parent
- 3640600d7a5c
↓ peach-browser-d08bf4e3.tar.gzatlas at this save
files changed6 files
src/background/messages.ts+1 −0src/background/vault.ts+4 −0src/content/inject.ts+60 −11src/popup/styles/popup.css+9 −0src/popup/views/Entry.tsx+60 −3src/shared/types.ts+1 −0src/background/messages.ts+1 −0
@@ -390,6 +390,7 @@390390 id: e.id,391391 title: e.title,392392 username: e.username,393+ email: e.email,393394 url: e.url,394395 hasTotp: Boolean(e.totp),395396 hasPasskey: e.passkey !== undefined,src/background/vault.ts+4 −0
@@ -1523,6 +1523,10 @@15231523 nextPatch.username = patch.username?.trim() ?? "";15241524 }15251525 1526+ if ("email" in patch) {1527+ nextPatch.email = patch.email?.trim() || undefined;1528+ }1529+15261530 if ("password" in patch) {15271531 const password = patch.password ?? "";15281532 // Allow clearing a password (e.g. on passkey-only entries)src/content/inject.ts+60 −11
@@ -37,6 +37,7 @@3737 id: string;3838 title: string;3939 username?: string;40+ email?: string;4041 url?: string;4142 hasTotp?: boolean;4243 /** True when the entry also has a passkey credential attached. */@@ -753,12 +754,20 @@753754 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;754755 line-height: 1.25;755756 }757+ .sp-identity-row {758+ display: flex; align-items: baseline; gap: 5px; margin-bottom: 3px;759+ }760+ .sp-identity-label {761+ font-size: 9px; font-weight: 600; letter-spacing: 0.05em;762+ text-transform: uppercase; color: var(--peach-text-muted); flex-shrink: 0;763+ }756764 .sp-username {757765 font-size: 12px; color: var(--peach-text-dim);758766 font-family: "IBM Plex Mono", monospace;759767 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;760- margin-bottom: 6px;768+ margin-bottom: 0;761769 }770+ .sp-identity-row + .sp-fill-btn, .sp-username + .sp-fill-btn { margin-top: 6px; }762771 .sp-fill-btn {763772 width: 100%; padding: 11px; border-radius: 9px; border: none;764773 background: var(--peach-accent); color: #fff;@@ -1681,10 +1690,29 @@16811690 titleEl.textContent = entry.title;16821691 platter.appendChild(titleEl);16831692 1693+ if (entry.email) {1694+ const emailEl = document.createElement("div");1695+ emailEl.className = "sp-identity-row";1696+ const emailLbl = document.createElement("span");1697+ emailLbl.className = "sp-identity-label";1698+ emailLbl.textContent = "email";1699+ const emailVal = document.createElement("span");1700+ emailVal.className = "sp-username";1701+ emailVal.textContent = entry.email;1702+ emailEl.append(emailLbl, emailVal);1703+ platter.appendChild(emailEl);1704+ }1705+16841706 if (entry.username) {16851707 const userEl = document.createElement("div");1686- userEl.className = "sp-username";1687- userEl.textContent = entry.username;1708+ userEl.className = "sp-identity-row";1709+ const userLbl = document.createElement("span");1710+ userLbl.className = "sp-identity-label";1711+ userLbl.textContent = entry.username.includes("@") ? "email" : "username";1712+ const userVal = document.createElement("span");1713+ userVal.className = "sp-username";1714+ userVal.textContent = entry.username;1715+ userEl.append(userLbl, userVal);16881716 platter.appendChild(userEl);16891717 }16901718 @@ -1709,23 +1737,40 @@17091737 const actions = document.createElement("div");17101738 actions.className = "sp-actions";17111739 1740+ if (entry.email) {1741+ const copyEmail = document.createElement("button");1742+ copyEmail.className = "sp-action-btn";1743+ copyEmail.type = "button";1744+ copyEmail.title = "Copy email";1745+ copyEmail.textContent = "Copy email";1746+ copyEmail.addEventListener("click", async (e) => {1747+ e.stopPropagation();1748+ let alreadyWritten = false;1749+ try { await navigator.clipboard.writeText(entry.email!); alreadyWritten = true; } catch {}1750+ await chrome.runtime.sendMessage({1751+ type: "COPY",1752+ payload: { text: entry.email, label: "Email", alreadyWritten },1753+ }).catch(() => {});1754+ flashActionBtn(copyEmail);1755+ });1756+ actions.appendChild(copyEmail);1757+ }1758+17121759 const copyU = document.createElement("button");17131760 copyU.className = "sp-action-btn";17141761 copyU.type = "button";1715- copyU.title = "Copy username";1716- copyU.textContent = "Copy username";1762+ const copyULabel = entry.username?.includes("@") ? "Copy email" : "Copy username";1763+ copyU.title = copyULabel;1764+ copyU.textContent = copyULabel;17171765 copyU.disabled = !entry.username;17181766 copyU.addEventListener("click", async (e) => {17191767 e.stopPropagation();17201768 if (!entry.username) return;17211769 let alreadyWritten = false;17221770 try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1723- // Mirror the popup's copy path so the COPY message can clear the1724- // clipboard via the configured timeout. alreadyWritten lets the SW1725- // skip a redundant write when the page-level write succeeded.17261771 await chrome.runtime.sendMessage({17271772 type: "COPY",1728- payload: { text: entry.username, label: "Username", alreadyWritten },1773+ payload: { text: entry.username, label: entry.username.includes("@") ? "Email" : "Username", alreadyWritten },17291774 }).catch(() => {});17301775 flashActionBtn(copyU);17311776 });@@ -1902,7 +1947,7 @@19021947 const res = (await chrome.runtime.sendMessage({19031948 type: "GET_ENTRY_FOR_FILL",19041949 payload: { id: summary.id },1905- }).catch(() => null)) as { ok: boolean; data?: { title?: string; username?: string; password?: string; url?: string; totp?: string; notes?: string } } | null;1950+ }).catch(() => null)) as { ok: boolean; data?: { title?: string; username?: string; email?: string; password?: string; url?: string; totp?: string; notes?: string } } | null;19061951 19071952 if (!res || !res.ok || !res.data) {19081953 loading.textContent = "couldn't load this entry. try again from the popup.";@@ -1929,7 +1974,9 @@19291974 const titleInput = field("title", res.data.title ?? "");19301975 const urlInput = field("url", res.data.url ?? "");19311976 urlInput.classList.add("url-f");1932- const usernameInput = field("username", res.data.username ?? "");1977+ const usernameLabel = (res.data.username ?? "").includes("@") ? "email" : "username";1978+ const usernameInput = field(usernameLabel, res.data.username ?? "");1979+ const emailInput = field("email", res.data.email ?? "");19331980 19341981 // Password row with reveal toggle19351982 const pwGroup = document.createElement("div");@@ -1998,6 +2045,7 @@19982045 title: titleInput.value.trim(),19992046 url: urlInput.value.trim(),20002047 username: usernameInput.value.trim(),2048+ email: emailInput.value.trim() || undefined,20012049 password: pwInput.value,20022050 totp: totpInput.value.trim(),20032051 notes: notesInput.value,@@ -2022,6 +2070,7 @@20222070 id: summary.id,20232071 title: patch.title as string,20242072 username: patch.username as string,2073+ email: patch.email as string | undefined,20252074 url: patch.url as string,20262075 hasTotp: Boolean(patch.totp),20272076 hasPasskey: summary.hasPasskey,src/popup/styles/popup.css+9 −0
@@ -385,6 +385,15 @@385385 text-transform: uppercase; letter-spacing: 0.2em;386386 font-size: 0.5rem; color: var(--peach-t2);387387 }388+.peach-add-field-btn {389+ align-self: flex-start;390+ background: none; border: none; padding: 2px 0;391+ font-family: Inter, system-ui, sans-serif;392+ font-size: 0.6875rem; color: var(--peach-accent);393+ cursor: pointer; opacity: 0.75;394+ letter-spacing: 0.02em;395+}396+.peach-add-field-btn:hover { opacity: 1; }388397 .peach-field-value-row {389398 display: flex; justify-content: space-between;390399 align-items: center; gap: 8px;src/popup/views/Entry.tsx+60 −3
@@ -10,7 +10,7 @@1010 import { GeneratorDrawer } from "../components/GeneratorDrawer";1111 import { NoteEditorInline, Toolbar } from "./NoteEditor";1212 13-type EditableField = "title" | "url" | "username" | "password" | "notes" | "totp" | null;13+type EditableField = "title" | "url" | "username" | "email" | "password" | "notes" | "totp" | null;1414 1515 interface EntryProps {1616 entry: VaultEntry | null;@@ -269,6 +269,8 @@269269 return entry.url ?? "";270270 case "username":271271 return entry.username ?? "";272+ case "email":273+ return entry.email ?? "";272274 case "password":273275 return entry.password ?? "";274276 case "notes":@@ -769,9 +771,9 @@769771 <Show when={entry().type !== "note" && entry().type !== "card" && !isPasskeyOnly(entry())}>770772 <section class="peach-fields" style="position:relative;z-index:1;">771773 772- {/* Username */}774+ {/* Username / Email */}773775 <div class="peach-field-card" classList={{ "is-editing": editingField() === "username" }}>774- <span class="peach-label">username</span>776+ <span class="peach-label">{entry().username?.includes("@") ? "email" : "username"}</span>775777 <Show776778 when={editingField() === "username"}777779 fallback={@@ -812,6 +814,61 @@812814 </div>813815 </Show>814816 </div>817+818+ {/* Email (separate from username — for sites that require both) */}819+ <Show when={entry().email || editingField() === "email"}>820+ <div class="peach-field-card" classList={{ "is-editing": editingField() === "email" }}>821+ <span class="peach-label">email</span>822+ <Show823+ when={editingField() === "email"}824+ fallback={825+ <div class="peach-field-value-row">826+ <span827+ class="peach-field-value peach-inline-clickable"828+ title="Click to edit"829+ onClick={() => startEdit("email", entry().email ?? "")}830+ >831+ {entry().email}832+ </span>833+ <button834+ class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() === "email" ? "is-copied" : ""}`}835+ onClick={(e) => void copyField(entry().email!, "email", e.currentTarget as HTMLButtonElement)}836+ aria-label="Copy email"837+ >838+ <CopyGlyph /><span class="peach-copy-sheen" />839+ </button>840+ </div>841+ }842+ >843+ <div844+ class="peach-inline-field-wrap"845+ onFocusOut={(event) => handleEditorFocusOut("email", event)}846+ >847+ <input848+ class="peach-inline-input"849+ value={draft()}850+ onInput={(e) => setDraft(e.currentTarget.value)}851+ onKeyDown={onEditKeyDown}852+ ref={autoFocus}853+ disabled={saving()}854+ autocomplete="email"855+ />856+ <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />857+ </div>858+ </Show>859+ </div>860+ </Show>861+862+ {/* +Add Email button — only shown when no email is set */}863+ <Show when={!entry().email && editingField() !== "email"}>864+ <button865+ class="peach-add-field-btn"866+ type="button"867+ onClick={() => startEdit("email", "")}868+ >869+ + add email870+ </button>871+ </Show>815872 816873 {/* Password */}817874 <div class="peach-field-card" classList={{ "is-editing": editingField() === "password" }}>src/shared/types.ts+1 −0
@@ -66,6 +66,7 @@6666 type?: "login" | "note" | "passkey" | "card";6767 title: string;6868 username?: string; // only for login entries69+ email?: string; // separate email address — for sites that require both a username and an email6970 password?: string; // only for login entries7071 url?: string; // only for login entries7172 /**