e3936c5a967c

add password history reveal/copy/restore to inline edit form with styling and timeSince helper

time
2026-06-06 19:29 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
password history
parent
24cc275f410f
1 file src/content/inject.ts+141 −2
src/content/inject.ts+141 −2
@@ -9,7 +9,7 @@99  * to fill come back across the boundary.1010  */1111 12-import { ACCENT_COLORS, type PasswordPolicy, type SavedIdentity } from "../shared/types";12+import { ACCENT_COLORS, type PasswordPolicy, type PasswordHistoryItem, type SavedIdentity } from "../shared/types";1313 import { generatePassword } from "../shared/generate";1414 import { detectPasswordPolicy } from "./policy";1515 import {@@ -20,6 +20,22 @@2020   type IdentityKind,2121 } from "./identity-helper";2222 23+// Coarse "time since" label for the inline password-history list. Mirrors the24+// main popup's helper — "roughly when", not a precise duration.25+function timeSince(when: number): string {26+  const secs = Math.max(0, Math.floor((Date.now() - when) / 1000));27+  if (secs < 60) return "just now";28+  const mins = Math.floor(secs / 60);29+  if (mins < 60) return `${mins}m ago`;30+  const hours = Math.floor(mins / 60);31+  if (hours < 24) return `${hours}h ago`;32+  const days = Math.floor(hours / 24);33+  if (days < 30) return `${days}d ago`;34+  const months = Math.floor(days / 30);35+  if (months < 12) return `${months}mo ago`;36+  return `${Math.floor(months / 12)}y ago`;37+}38+2339 let accentHex = "#E8956D";2440 let popupTheme: "dark" | "light" = "dark";2541 chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string; theme?: string } } | undefined) => {@@ -777,6 +793,36 @@777793     cursor: pointer; text-align: left; transition: color 100ms;778794   }779795   .gen-toggle-link:hover { color: var(--peach-accent); }796+  .pw-history {797+    display: grid; gap: 4px; margin-top: 4px;798+    max-height: 150px; overflow-y: auto; scrollbar-width: thin;799+  }800+  .pw-history-row {801+    display: flex; align-items: center; justify-content: space-between; gap: 6px;802+    padding: 5px 7px; border-radius: 7px;803+    background: color-mix(in srgb, var(--peach-accent) 6%, transparent);804+    border: 1px solid color-mix(in srgb, var(--peach-accent) 10%, transparent);805+  }806+  .pw-history-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }807+  .pw-history-pw {808+    font-family: "IBM Plex Mono", monospace; font-size: 11px;809+    color: var(--peach-t1, #eee); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;810+  }811+  .pw-history-age {812+    font-family: "IBM Plex Mono", monospace; font-size: 9px;813+    color: color-mix(in srgb, var(--peach-accent) 55%, transparent); letter-spacing: 0.03em;814+  }815+  .pw-history-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }816+  .pw-history-btn {817+    background: none; border: none; padding: 2px 5px; border-radius: 5px;818+    color: color-mix(in srgb, var(--peach-accent) 65%, transparent);819+    font-size: 9px; font-family: "IBM Plex Mono", monospace; cursor: pointer;820+    transition: color 100ms, background 100ms;821+  }822+  .pw-history-btn:hover {823+    color: var(--peach-accent);824+    background: color-mix(in srgb, var(--peach-accent) 12%, transparent);825+  }780826   .save-btn {781827     width: 100%; padding: 8px; border-radius: 8px; border: none;782828     background: var(--peach-accent); color: #fff; font-size: 12px;@@ -2787,7 +2833,7 @@27872833       }).catch(() => null),27882834       chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null),27892835     ]);2790-    const entryRes = res as { ok: boolean; data?: { title?: string; username?: string; email?: string; password?: string; url?: string; totp?: string; notes?: string } } | null;2836+    const entryRes = res as { ok: boolean; data?: { title?: string; username?: string; email?: string; password?: string; url?: string; totp?: string; notes?: string; passwordHistory?: PasswordHistoryItem[] } } | null;27912837     const editIdentities: SavedIdentity[] = (idRes as { ok?: boolean; data?: SavedIdentity[] } | null)?.data ?? [];27922838 27932839     if (!entryRes || !entryRes.ok || !entryRes.data) {@@ -2858,6 +2904,99 @@28582904       genToggleBtn.textContent = editGenVisible ? "☰ hide generator" : "⚙ generate";28592905     });28602906     pwGroup.append(pwLbl, pwRow, genToggleBtn, editSmartGen.root);2907+2908+    // Past-password history: reveal, copy, or restore a previously-used value.2909+    // Rows are masked by default; "use" loads the old value into the editor so2910+    // the standard save flow records the swap (no separate write path).2911+    const history = entryRes.data.passwordHistory ?? [];2912+    if (history.length > 0) {2913+      const histList = document.createElement("div");2914+      histList.className = "pw-history";2915+      histList.style.display = "none";2916+      let histVisible = false;2917+2918+      const histToggle = document.createElement("button");2919+      histToggle.className = "gen-toggle-link";2920+      histToggle.type = "button";2921+      histToggle.textContent = `↺ history (${history.length})`;2922+      histToggle.addEventListener("click", () => {2923+        histVisible = !histVisible;2924+        histList.style.display = histVisible ? "" : "none";2925+        histToggle.textContent = histVisible ? "↺ hide history" : `↺ history (${history.length})`;2926+      });2927+2928+      for (const item of history) {2929+        const row = document.createElement("div");2930+        row.className = "pw-history-row";2931+        const main = document.createElement("div");2932+        main.className = "pw-history-main";2933+        const pwText = document.createElement("div");2934+        pwText.className = "pw-history-pw";2935+        let revealed = false;2936+        const mask = "••••••••••••";2937+        pwText.textContent = mask;2938+        const age = document.createElement("div");2939+        age.className = "pw-history-age";2940+        age.textContent = timeSince(item.changedAt);2941+        main.append(pwText, age);2942+2943+        const acts = document.createElement("div");2944+        acts.className = "pw-history-actions";2945+        const revealBtn2 = document.createElement("button");2946+        revealBtn2.className = "pw-history-btn";2947+        revealBtn2.type = "button";2948+        revealBtn2.textContent = "show";2949+        revealBtn2.addEventListener("click", () => {2950+          revealed = !revealed;2951+          pwText.textContent = revealed ? item.password : mask;2952+          revealBtn2.textContent = revealed ? "hide" : "show";2953+        });2954+        const copyBtn2 = document.createElement("button");2955+        copyBtn2.className = "pw-history-btn";2956+        copyBtn2.type = "button";2957+        copyBtn2.textContent = "copy";2958+        copyBtn2.addEventListener("click", async () => {2959+          let alreadyWritten = false;2960+          try {2961+            await navigator.clipboard.writeText(item.password);2962+            alreadyWritten = true;2963+          } catch (err) {2964+            console.warn("[Peach] clipboard write denied:", err);2965+          }2966+          await chrome.runtime.sendMessage({2967+            type: "COPY",2968+            payload: { text: item.password, label: "Password", alreadyWritten },2969+          }).catch(() => null);2970+          copyBtn2.textContent = "copied";2971+          setTimeout(() => { if (copyBtn2.isConnected) copyBtn2.textContent = "copy"; }, 1200);2972+        });2973+        const useBtn = document.createElement("button");2974+        useBtn.className = "pw-history-btn";2975+        useBtn.type = "button";2976+        useBtn.textContent = "use";2977+        useBtn.title = "Use this password again";2978+        useBtn.addEventListener("click", () => {2979+          pwInput.value = item.password;2980+          pwInput.type = "text";2981+          revealBtn.textContent = "hide";2982+          if (editGenVisible) {2983+            editGenVisible = false;2984+            pwRow.style.display = "";2985+            editSmartGen.root.style.display = "none";2986+            genToggleBtn.textContent = "⚙ generate";2987+          }2988+          histVisible = false;2989+          histList.style.display = "none";2990+          histToggle.textContent = `↺ history (${history.length})`;2991+        });2992+        acts.append(revealBtn2, copyBtn2, useBtn);2993+        row.append(main, acts);2994+        histList.appendChild(row);2995+      }2996+2997+      pwGroup.append(histToggle, histList);2998+    }2999+28613000     body.appendChild(pwGroup);28623001 28633002     const totpInput = field("2fa secret", entryRes.data.totp ?? "").inp;

view file