24cc275f410f

add password history reveal/copy/restore drawer to main popup edit mode with styling

time
2026-06-06 19:27 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
password history
parent
7e401ad4a9df
2 files src/popup/styles/popup.css+65 −0src/popup/views/Entry.tsx+104 −11
src/popup/styles/popup.css+65 −0
@@ -2359,6 +2359,71 @@23592359   color: var(--peach-accent);23602360 }23612361 2362+/* Holds the generate + history toggle links on one row under the editor. */2363+.peach-password-edit-links {2364+  display: flex;2365+  align-items: center;2366+  gap: 14px;2367+  flex-wrap: wrap;2368+}2369+2370+/* ── Past-password history list ── */2371+.peach-password-history {2372+  display: grid;2373+  gap: 4px;2374+  margin-top: 2px;2375+  max-height: 168px;2376+  overflow-y: auto;2377+  scrollbar-width: thin;2378+}2379+.peach-history-row {2380+  display: flex;2381+  align-items: center;2382+  justify-content: space-between;2383+  gap: 8px;2384+  padding: 5px 7px;2385+  border-radius: 7px;2386+  background: color-mix(in srgb, var(--peach-accent) 5%, transparent);2387+  border: 1px solid color-mix(in srgb, var(--peach-accent) 9%, transparent);2388+}2389+.peach-history-main {2390+  display: flex;2391+  flex-direction: column;2392+  gap: 1px;2393+  min-width: 0;2394+  flex: 1;2395+}2396+.peach-history-pw {2397+  font-family: "IBM Plex Mono", monospace;2398+  font-size: 0.6875rem;2399+  color: var(--peach-t1);2400+  white-space: nowrap;2401+  overflow: hidden;2402+  text-overflow: ellipsis;2403+}2404+.peach-history-pw .peach-password-mask {2405+  letter-spacing: 0.08em;2406+  color: var(--peach-t2);2407+}2408+.peach-history-age {2409+  font-family: "IBM Plex Mono", monospace;2410+  font-size: 0.5625rem;2411+  color: var(--peach-t3, var(--peach-t2));2412+  letter-spacing: 0.03em;2413+}2414+.peach-history-actions {2415+  display: flex;2416+  align-items: center;2417+  gap: 2px;2418+  flex-shrink: 0;2419+}2420+.peach-history-use {2421+  font-family: "IBM Plex Mono", monospace;2422+  font-size: 0.5625rem;2423+  letter-spacing: 0.05em;2424+  padding: 3px 7px;2425+}2426+23622427 /* ── New entry body (Add.tsx — mirrors .peach-stage) ─────────────────── */23632428 .peach-entry-body {23642429   flex: 1;

view file

src/popup/views/Entry.tsx+104 −11
@@ -13,6 +13,22 @@1313 1414 type EditableField = "title" | "url" | "username" | "email" | "password" | "notes" | "totp" | null;1515 16+// Compact "time since" label for the password-history list. Coarse on purpose —17+// the user wants "roughly when", not a precise duration.18+function timeSince(when: number): string {19+  const secs = Math.max(0, Math.floor((Date.now() - when) / 1000));20+  if (secs < 60) return "just now";21+  const mins = Math.floor(secs / 60);22+  if (mins < 60) return `${mins}m ago`;23+  const hours = Math.floor(mins / 60);24+  if (hours < 24) return `${hours}h ago`;25+  const days = Math.floor(hours / 24);26+  if (days < 30) return `${days}d ago`;27+  const months = Math.floor(days / 30);28+  if (months < 12) return `${months}mo ago`;29+  return `${Math.floor(months / 12)}y ago`;30+}31+1632 interface EntryProps {1733   entry: VaultEntry | null;1834   /**@@ -83,6 +99,8 @@8399   const [saving, setSaving] = createSignal(false);84100   const [showPassword, setShowPassword] = createSignal(false);85101   const [showGenerator, setShowGenerator] = createSignal(false);102+  const [showHistory, setShowHistory] = createSignal(false);103+  const [revealedHistory, setRevealedHistory] = createSignal<Set<number>>(new Set());86104   const [copied, setCopied] = createSignal<string | null>(null);87105   // Local draft for the additional URLs list, edited only while the URL field88106   // is in inline-edit mode. Saved alongside `url` when the user confirms.@@ -252,6 +270,8 @@252270     setDraft(value);253271     setIdentityHelperSuppressed(false);254272     setShowGenerator(false);273+    setShowHistory(false);274+    setRevealedHistory(new Set<number>());255275     setConfirmDelete(false);256276     if (field === "url") {257277       setAdditionalUrlsDraft([...(props.entry?.additionalUrls ?? [])]);@@ -305,6 +325,8 @@305325       triggerRing();306326       setEditingField(null);307327       setShowGenerator(false);328+      setShowHistory(false);329+      setRevealedHistory(new Set<number>());308330       await props.onFieldSaved();309331     } catch {310332       // silent — leave editing open@@ -351,6 +373,8 @@351373     setEditingField(null);352374     setDraft("");353375     setShowGenerator(false);376+    setShowHistory(false);377+    setRevealedHistory(new Set<number>());354378   }355379 356380   // Switch from one identity slot to the other.@@ -1139,23 +1163,92 @@11391163                           </button>11401164                         </div>11411165                         <InlineSaveCancelRow onSave={saveField} onCancel={cancelEdit} saving={saving()} />1142-                        <button1143-                          class="peach-generate-toggle-link"1144-                          type="button"1145-                          onClick={() => setShowGenerator(!showGenerator())}1146-                        >1147-                          <svg width="10" height="10" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">1148-                            <path d="M11 2.5A5.5 5.5 0 1 0 12.5 7" />1149-                            <path d="M9.5 2.5H11V1" />1150-                          </svg>1151-                          {showGenerator() ? "hide generator" : "generate"}1152-                        </button>1166+                        <div class="peach-password-edit-links">1167+                          <button1168+                            class="peach-generate-toggle-link"1169+                            type="button"1170+                            onClick={() => { setShowGenerator(!showGenerator()); if (!showGenerator()) setShowHistory(false); }}1171+                          >1172+                            <svg width="10" height="10" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">1173+                              <path d="M11 2.5A5.5 5.5 0 1 0 12.5 7" />1174+                              <path d="M9.5 2.5H11V1" />1175+                            </svg>1176+                            {showGenerator() ? "hide generator" : "generate"}1177+                          </button>1178+                          <Show when={(entry().passwordHistory?.length ?? 0) > 0}>1179+                            <button1180+                              class="peach-generate-toggle-link"1181+                              type="button"1182+                              onClick={() => { setShowHistory(!showHistory()); if (!showHistory()) setShowGenerator(false); }}1183+                            >1184+                              <svg width="10" height="10" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">1185+                                <path d="M6.5 1a5.5 5.5 0 1 0 5.5 5.5" />1186+                                <path d="M6.5 3.5V6.5L8.5 8" />1187+                                <path d="M1 1.5V4H3.5" />1188+                              </svg>1189+                              {showHistory() ? "hide history" : `history (${entry().passwordHistory!.length})`}1190+                            </button>1191+                          </Show>1192+                        </div>11531193                         <Show when={showGenerator()}>11541194                           <GeneratorDrawer11551195                             open={true}11561196                             compact={true}11571197                             onUse={(pw) => { setDraft(pw); setShowGenerator(false); }}11581198                           />1199+                        </Show>1200+                        <Show when={showHistory()}>1201+                          <div class="peach-password-history">1202+                            <For each={entry().passwordHistory ?? []}>1203+                              {(item, i) => (1204+                                <div class="peach-history-row">1205+                                  <div class="peach-history-main">1206+                                    <span class="peach-history-pw" classList={{ "is-revealed": revealedHistory().has(i()) }}>1207+                                      <Show1208+                                        when={revealedHistory().has(i())}1209+                                        fallback={<span class="peach-password-mask">••••••••••••</span>}1210+                                      >1211+                                        {item.password}1212+                                      </Show>1213+                                    </span>1214+                                    <span class="peach-history-age">{timeSince(item.changedAt)}</span>1215+                                  </div>1216+                                  <div class="peach-history-actions">1217+                                    <button1218+                                      class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1219+                                      type="button"1220+                                      title={revealedHistory().has(i()) ? "Hide" : "Reveal"}1221+                                      onClick={() => setRevealedHistory((prev) => {1222+                                        const next = new Set(prev);1223+                                        if (next.has(i())) next.delete(i()); else next.add(i());1224+                                        return next;1225+                                      })}1226+                                    >1227+                                      <Show when={revealedHistory().has(i())} fallback={<EyeOpenGlyph />}>1228+                                        <EyeClosedGlyph />1229+                                      </Show>1230+                                    </button>1231+                                    <button1232+                                      class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() === `history-${i()}` ? "is-copied" : ""}`}1233+                                      type="button"1234+                                      title="Copy"1235+                                      onClick={(e) => void copyField(item.password, `history-${i()}`, e.currentTarget as HTMLButtonElement)}1236+                                    >1237+                                      <CopyGlyph /><span class="peach-copy-sheen" />1238+                                    </button>1239+                                    <button1240+                                      class="peach-btn peach-btn--ghost peach-btn--sm peach-history-use"1241+                                      type="button"1242+                                      title="Use this password again"1243+                                      onClick={() => { setDraft(item.password); setShowPassword(true); setShowHistory(false); }}1244+                                    >1245+                                      use1246+                                    </button>1247+                                  </div>1248+                                </div>1249+                              )}1250+                            </For>1251+                          </div>11591252                         </Show>11601253                       </div>11611254                     </Show>

view file