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
↓ peach-browser-fc0e2449.tar.gzatlas at this save
files changed1 file
src/content/inject.ts+156 −66src/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) {