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
↓ peach-browser-e3936c5a.tar.gzatlas at this save
files changed1 file
src/content/inject.ts+141 −2src/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;