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
6 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 −0
src/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,

view file

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)

view file

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,

view file

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;

view file

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

view file

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

view file