fc0e2449c05b

rebuild inline popup visual foundation: token-based dark/light themes via data-theme, new entrance/exit/slide animations, accent glow + border-accent vars on host, exit-animated dismiss

time
2026-04-26 18:08 UTC · 6 months ago
machine
koh2
parent
1c5fc17731cc
1 file src/content/inject.ts+156 −66
src/content/inject.ts+156 −66
@@ -14,11 +14,24 @@1414 import { detectPasswordPolicy } from "./policy";1515 1616 let accentHex = "#E8956D";17-chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string } } | undefined) => {18-  const name = resp?.data?.accentColor ?? "peach";17+let popupTheme: "dark" | "light" = "dark";18+chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string; theme?: string } } | undefined) => {19+  const data = resp?.data;20+  const name = data?.accentColor ?? "peach";1921   accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";22+  popupTheme = data?.theme === "light" ? "light" : "dark";2023 });2124 25+// 8-digit hex alpha helpers for the accent-derived shadow tokens.26+// `#E8956D` + alpha `28` → `#E8956D28`. The browser parses the trailing27+// two hex digits as the alpha channel.28+function withAlpha(hex: string, alpha: string): string {29+  // Defensive: only 6-digit hex inputs are supported here. ACCENT_COLORS30+  // always exposes 6-digit hex, so this is a guard against future changes31+  // shipping shorthand colors that would silently break the var injection.32+  return /^#[0-9a-f]{6}$/i.test(hex) ? hex + alpha : hex;33+}34+2235 export interface SelectorEntry {2336   id: string;2437   title: string;@@ -269,33 +282,89 @@269282   return () => { stopObserving(); button.remove(); };270283 }271284 272-// ── Shared selector + new-entry styles ────────────────────────────285+// All theme-dependent values resolve through tokens scoped to286+// .peach-inline-popup[data-theme="dark|light"]. Accent variables287+// (--peach-accent, --peach-accent-dim, --peach-glow, --peach-border-accent)288+// are identical in both themes — only surface tokens differ. See289+// Projects.md §4.14: hardcoded hex inside this stylesheet breaks the290+// theme switch — only literal brand-peach in peachIconSvg may stay raw.273291 const SELECTOR_STYLES = `274292   :host { all: initial; }275293   *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }276-  .selector {277-    background: #0f0e0b;278-    border: 1px solid rgba(240,232,216,0.10);279-    border-radius: 13px;294+295+  .peach-inline-popup[data-theme="dark"] {296+    --peach-bg:           #1a1410;297+    --peach-bg-card:      #221c16;298+    --peach-text:         #f5f0ea;299+    --peach-text-dim:     #8a7a6a;300+    --peach-text-muted:   #4a3a2a;301+    --peach-border:       rgba(255,255,255,0.07);302+    --peach-input-bg:     rgba(255,255,255,0.05);303+    --peach-input-border: rgba(255,255,255,0.08);304+  }305+  .peach-inline-popup[data-theme="light"] {306+    --peach-bg:           #fdf8f4;307+    --peach-bg-card:      #f5ede4;308+    --peach-text:         #1c1310;309+    --peach-text-dim:     #6b5a4a;310+    --peach-text-muted:   #a89080;311+    --peach-border:       rgba(0,0,0,0.08);312+    --peach-input-bg:     rgba(0,0,0,0.04);313+    --peach-input-border: rgba(0,0,0,0.10);314+  }315+316+  .peach-inline-popup {317+    background: var(--peach-bg);318+    border: 1px solid var(--peach-border-accent, var(--peach-border));319+    border-radius: 14px;280320     overflow: hidden;281-    box-shadow: 0 4px 16px rgba(0,0,0,0.45), 0 16px 48px rgba(0,0,0,0.7);321+    box-shadow:322+      0 8px 40px var(--peach-glow, transparent),323+      0 2px 12px rgba(0,0,0,0.5);282324     font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;283-    animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;325+    animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;284326   }285-  @keyframes sl-in {286-    from { opacity:0; transform:translateY(-6px) scale(0.97) }287-    to   { opacity:1; transform:none }327+  /* Light theme: shadow against a light page is too strong otherwise — drop both328+     the accent glow and the dark drop-shadow opacity to keep the popup elegant. */329+  .peach-inline-popup[data-theme="light"] {330+    box-shadow:331+      0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),332+      0 2px 12px rgba(0,0,0,0.12);288333   }334+  .peach-inline-popup.is-exiting {335+    animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;336+    pointer-events: none;337+  }289338 339+  @keyframes peach-enter {340+    0%   { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }341+    65%  { opacity: 1; filter: blur(0); }342+    100% { opacity: 1; transform: translateY(0) scale(1); }343+  }344+  @keyframes peach-exit {345+    0%   { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }346+    100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }347+  }348+  @keyframes peach-slide-in {349+    from { opacity: 0; transform: translateX(12px); }350+    to   { opacity: 1; transform: translateX(0); }351+  }352+  @keyframes peach-slide-back {353+    from { opacity: 0; transform: translateX(-12px); }354+    to   { opacity: 1; transform: translateX(0); }355+  }356+  .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }357+  .peach-content[data-dir="back"]    { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }358+290359   /* Domain header */291360   .domain-header {292361     padding: 9px 13px 7px;293-    border-bottom: 1px solid rgba(240,232,216,0.055);362+    border-bottom: 1px solid var(--peach-border);294363     display: flex; align-items: center; justify-content: space-between;295364   }296365   .domain-eyebrow {297366     font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;298-    color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;367+    color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;299368   }300369   .domain-name {301370     font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);@@ -305,12 +374,12 @@305374   /* Search */306375   .search-wrap {307376     padding: 9px 12px 7px;308-    border-bottom: 1px solid rgba(240,232,216,0.04);377+    border-bottom: 1px solid var(--peach-border);309378   }310379   .search {311-    width: 100%; background: rgba(240,232,216,0.03);312-    border: 1px solid rgba(240,232,216,0.08);313-    border-radius: 7px; color: #F0E8D8; padding: 7px 10px;380+    width: 100%; background: var(--peach-input-bg);381+    border: 1px solid var(--peach-input-border);382+    border-radius: 7px; color: var(--peach-text); padding: 7px 10px;314383     font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;315384     transition: border-color 120ms, box-shadow 120ms;316385   }@@ -318,7 +387,7 @@318387     border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);319388     box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);320389   }321-  .search::placeholder { color: rgba(240,232,216,0.2); }390+  .search::placeholder { color: var(--peach-text-muted); }322391 323392   /* Results */324393   .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }@@ -327,20 +396,21 @@327396   /* Entry rows */328397   .entry {329398     display: flex; align-items: center; padding: 8px 12px;330-    cursor: pointer; border-bottom: 1px solid rgba(240,232,216,0.04);399+    cursor: pointer; border-bottom: 1px solid var(--peach-border);331400     transition: background 70ms; position: relative; outline: none;332401   }333402   .entry:last-child { border-bottom: none; }334-  .entry:hover { background: rgba(240,232,216,0.04); }403+  .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }335404   .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }336405   .entry.focused::before {337406     content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;338-    width: 2px; border-radius: 2px; background: color-mix(in srgb, var(--peach-accent) 55%, transparent);407+    width: 2px; border-radius: 2px;408+    background: color-mix(in srgb, var(--peach-accent) 55%, transparent);339409   }340410 341411   .entry-icon {342412     width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;343-    background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.07);413+    background: var(--peach-input-bg); border: 1px solid var(--peach-border);344414     display: flex; align-items: center; justify-content: center;345415     overflow: hidden; margin-right: 10px;346416   }@@ -353,13 +423,13 @@353423 354424   .entry-info { flex: 1; min-width: 0; }355425   .entry-title {356-    font-size: 12.5px; color: #F0E8D8;426+    font-size: 12.5px; color: var(--peach-text);357427     white-space: nowrap; overflow: hidden; text-overflow: ellipsis;358428     line-height: 1.3;359429   }360430   .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }361431   .entry-sub {362-    font-size: 10px; color: rgba(240,232,216,0.32);432+    font-size: 10px; color: var(--peach-text-dim);363433     font-family: "IBM Plex Mono", monospace;364434     white-space: nowrap; overflow: hidden; text-overflow: ellipsis;365435     margin-top: 1px;@@ -368,52 +438,48 @@368438   .totp-badge {369439     font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;370440     border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);371-    color: color-mix(in srgb, var(--peach-accent) 75%, transparent); font-family: "IBM Plex Mono", monospace;372-    flex-shrink: 0; margin-left: 8px;441+    color: color-mix(in srgb, var(--peach-accent) 75%, transparent);442+    font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;373443   }374444 375-  .divider { height: 1px; background: rgba(240,232,216,0.05); margin: 2px 0; }445+  .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }376446 377447   .empty-hint {378-    padding: 14px 13px; font-size: 11px; color: rgba(240,232,216,0.28);448+    padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);379449     font-family: "IBM Plex Mono", monospace;380450   }381451 382-  /* ── Create form ─────────────────────────────────────── */452+  /* Create form */383453   .create-form-wrap {384-    animation: cf-in 180ms cubic-bezier(0.16,1,0.3,1) both;454+    animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;385455   }386-  @keyframes cf-in {387-    from { opacity:0; transform:translateX(8px) }388-    to { opacity:1; transform:none }389-  }390456   .create-head {391457     display: flex; align-items: center; gap: 8px;392-    padding: 10px 12px; border-bottom: 1px solid rgba(240,232,216,0.055);458+    padding: 10px 12px; border-bottom: 1px solid var(--peach-border);393459   }394460   .back-btn {395461     background: transparent; border: none; cursor: pointer;396-    color: rgba(240,232,216,0.38); padding: 3px; border-radius: 5px;462+    color: var(--peach-text-dim); padding: 3px; border-radius: 5px;397463     display: flex; align-items: center; line-height: 0;398464     transition: color 100ms, background 100ms;399465   }400-  .back-btn:hover { color: rgba(240,232,216,0.75); background: rgba(240,232,216,0.05); }401-  .create-label { font-size: 12px; color: rgba(240,232,216,0.65); font-weight: 500; flex: 1; }466+  .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }467+  .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }402468   .create-body { padding: 12px; display: grid; gap: 9px; }403469   .f-group { display: grid; gap: 3px; }404470   .f-label {405471     font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;406-    color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;472+    color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;407473   }408474   .f-input {409-    background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);410-    border-radius: 7px; color: #F0E8D8; padding: 7px 10px;475+    background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);476+    border-radius: 7px; color: var(--peach-text); padding: 7px 10px;411477     font-size: 11px; font-family: "IBM Plex Mono", monospace;412478     outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;413479   }414480   .f-input:focus { border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent); }415-  .f-input::placeholder { color: rgba(240,232,216,0.18); }416-  .f-input.url-f { font-size: 10px; color: rgba(240,232,216,0.5); letter-spacing: 0.01em; }481+  .f-input::placeholder { color: var(--peach-text-muted); }482+  .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }417483   .f-row { display: flex; gap: 6px; }418484   .f-row .f-input { flex: 1; min-width: 0; }419485   .gen-btn {@@ -430,16 +496,20 @@430496     width: 100%; padding: 8px; border-radius: 8px; border: none;431497     background: var(--peach-accent); color: #fff; font-size: 12px;432498     font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;433-    transition: background 120ms; letter-spacing: 0.01em;499+    transition: background 120ms, box-shadow 120ms, transform 80ms;500+    letter-spacing: 0.01em;434501   }435-  .save-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }502+  .save-btn:hover {503+    background: color-mix(in srgb, var(--peach-accent) 82%, #000);504+    box-shadow: 0 4px 20px var(--peach-glow, transparent);505+  }436506   .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }437507   .url-hint {438508     font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);439509     font-family: "IBM Plex Mono", monospace; font-style: italic;440510   }441511 442-  /* ── Success ─────────────────────────────────────────── */512+  /* Success */443513   .success-wrap {444514     display: flex; flex-direction: column; align-items: center;445515     padding: 24px 16px 20px; gap: 8px;@@ -451,46 +521,50 @@451521     background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);452522     display: flex; align-items: center; justify-content: center;453523   }454-  .success-text { font-size: 12.5px; color: #F0E8D8; font-weight: 500; }455-  .success-sub { font-size: 10px; color: rgba(240,232,216,0.3); font-family:"IBM Plex Mono",monospace; }524+  .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }525+  .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }456526 457-  /* ── Locked state ────────────────────────────────────── */527+  /* Locked state */458528   .locked-wrap {459529     display: flex; flex-direction: column; align-items: stretch;460530     padding: 14px 13px 13px; gap: 9px;461-    animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;462531   }463-  .locked-head {464-    display: flex; align-items: center; gap: 8px; padding-bottom: 2px;465-  }532+  .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }466533   .locked-icon {467534     width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;468-    background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.08);535+    background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);469536     display: flex; align-items: center; justify-content: center;470537   }471-  .locked-title {472-    font-size: 11.5px; color: rgba(240,232,216,0.55); font-weight: 500;473-  }538+  .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }474539   .locked-input {475-    width: 100%; background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);476-    border-radius: 7px; color: #F0E8D8; padding: 7px 10px;540+    width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);541+    border-radius: 7px; color: var(--peach-text); padding: 7px 10px;477542     font-size: 11px; font-family: "IBM Plex Mono", monospace;478543     outline: none; transition: border-color 120ms, box-shadow 120ms;479544   }480545   .locked-input:focus { border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent); }481-  .locked-input::placeholder { color: rgba(240,232,216,0.2); }546+  .locked-input::placeholder { color: var(--peach-text-muted); }482547   .locked-btn {483548     width: 100%; padding: 8px; border-radius: 8px; border: none;484549     background: var(--peach-accent); color: #fff; font-size: 12px;485550     font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;486-    transition: background 120ms; letter-spacing: 0.01em;551+    transition: background 120ms;552+    letter-spacing: 0.01em;553+    box-shadow: 0 2px 16px var(--peach-glow, transparent);554+    animation: peach-pulse-glow 2.4s ease-in-out infinite;487555   }488556   .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }489-  .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; }557+  .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }490558   .locked-error {491559     font-size: 10px; color: rgba(220,80,60,0.85);492560     font-family: "IBM Plex Mono", monospace;493561   }562+563+  @keyframes peach-pulse-glow {564+    0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }565+    50%      { box-shadow: 0 2px 28px var(--peach-glow, transparent),566+                            0 0 48px var(--peach-glow, transparent); }567+  }494568 `;495569 496570 export function showAutofillSelector(@@ -502,6 +576,9 @@502576   const host = document.createElement("div");503577   host.style.cssText = "position: absolute; z-index: 2147483647;";504578   host.style.setProperty("--peach-accent", accentHex);579+  host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));580+  host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));581+  host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));505582   const shadow = host.attachShadow({ mode: "closed" });506583 507584   const style = shadow.appendChild(document.createElement("style"));@@ -510,10 +587,13 @@510587   const rect = anchorEl.getBoundingClientRect();511588   host.style.top = `${rect.bottom + window.scrollY + 5}px`;512589   host.style.left = `${rect.left + window.scrollX}px`;513-  host.style.width = `${Math.max(rect.width, 260)}px`;590+  // Spec §1.1: 320px width. The Math.max() floor preserves usability on591+  // wide single-input fields (e.g. centered email input on signup pages).592+  host.style.width = `${Math.max(rect.width, 320)}px`;514593 515594   const container = shadow.appendChild(document.createElement("div"));516-  container.className = "selector";595+  container.className = "peach-inline-popup";596+  container.setAttribute("data-theme", popupTheme);517597 518598   const searchableEntries = options.searchableEntries ?? [];519599   const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();@@ -997,9 +1077,17 @@9971077     renderList();9981078   }9991079 1080+  let exitScheduled = false;10001081   function dismiss(userAction = false) {1082+    // Idempotent: dismiss() can be called from multiple paths (Escape,1083+    // outside click, entry select, fill success, host page nav). The exit1084+    // animation must only play once and host.remove() must run exactly once.1085+    if (exitScheduled) return;1086+    exitScheduled = true;1087+    // Detach listeners synchronously so nothing can re-trigger during the1088+    // 130ms exit window (a focusin firing here would race against the1089+    // dismiss-lock work in Pass 2).10011090     stopObserving();1002-    host.remove();10031091     document.removeEventListener("click", outsideClick, { capture: true });10041092     document.removeEventListener("keydown", handleKeyDown);10051093     if (captureKeydownRef) {@@ -1007,6 +1095,8 @@10071095       captureKeydownRef = null;10081096     }10091097     if (userAction) options.onDismiss?.();1098+    container.classList.add("is-exiting");1099+    setTimeout(() => host.remove(), 130);10101100   }10111101 10121102   function outsideClick(ev: MouseEvent) {

view file