123456789101112import { ACCENT_COLORS, type PasswordPolicy } from "../shared/types";13import { generateSmartPassword } from "../shared/generate";14import { detectPasswordPolicy } from "./policy";1516let accentHex = "#E8956D";17let popupTheme: "dark" | "light" = "dark";18chrome.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";21 accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";22 popupTheme = data?.theme === "light" ? "light" : "dark";23});2425262728function withAlpha(hex: string, alpha: string): string {29 30 31 32 return /^#[0-9a-f]{6}$/i.test(hex) ? hex + alpha : hex;33}3435export interface SelectorEntry {36 id: string;37 title: string;38 username?: string;39 url?: string;40 hasTotp?: boolean;41}4243interface SelectorOptions {44 domain?: string;45 searchableEntries?: SelectorEntry[];46 47 isSignup?: boolean;48 4950515253 onFillFields?: (username: string, password: string) => void;54 5556575859 locked?: boolean;60 61626364 onUnlocked?: () => void;65 6667686970 onDismiss?: () => void;71}7273function getDomainFromUrl(url: string): string {74 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }75}7677function keySvgStr(): string {78 return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none" stroke="rgba(240,232,216,0.35)" stroke-width="0.95" stroke-linecap="round"><rect x="2" y="5.5" width="8" height="6" rx="1.5"/><path d="M4 5.5V4.25A2 2 0 0 1 6 2.25A2 2 0 0 1 8 4.25V5.5"/></svg>`;79}8081function plusSvgStr(): string {82 return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><path d="M6 1.5v9M1.5 6h9"/></svg>`;83}8485function peachIconSvg(): string {86 87 88 89 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 64 64" fill="none" aria-hidden="true"><path d="M32 54C16 54 8 44 8 31C8 18 17 10 27 10C28 7 30 5.5 32 5.5C34 5.5 36 7 37 10C47 10 56 18 56 31C56 44 48 54 32 54Z" fill="#D96830"/><path d="M37 8.5C40.5 3.5 48 5 46 10.5C42 11 37 8.5 37 8.5Z" fill="#7B9060"/></svg>`;90}9192function genSvgStr(): string {93 return `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round"><path d="M9.5 2A4.5 4.5 0 1 0 10.5 5.5"/><path d="M8.5 2H9.5V1"/></svg>`;94}9596function checkSvgStr(): string {97 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="#7B9060" stroke-width="1.5" stroke-linecap="round"><path d="M4 10.5L8 14.5L16 6.5"/></svg>`;98}99100function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {101 const { hostname, origin } = location;102 const parts = hostname.split(".");103 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",104 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",105 "oidc", "federation", "saml", "cas", "ldap"];106 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());107 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;108 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);109 return { origin, title, loginSubdomain };110}111112113114115116117export function showRememberSiteToast(118 pageDomain: string,119 entryId: string,120): void {121 const host = document.createElement("div");122 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";123 host.style.setProperty("--peach-accent", accentHex);124 const shadow = host.attachShadow({ mode: "closed" });125126 const style = document.createElement("style");127 style.textContent = `128 .bar {129 display:flex;align-items:center;gap:10px;padding:10px 14px;130 background:#131109;border:1px solid rgba(240,232,216,0.09);131 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);132 font-family:Inter,sans-serif;white-space:nowrap;133 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;134 }135 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }136 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }137 .yes {138 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;139 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;140 }141 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }142 .no {143 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;144 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;145 }146 `;147 shadow.appendChild(style);148149 const bar = document.createElement("div"); bar.className = "bar";150 const txt = document.createElement("span"); txt.className = "txt";151 txt.textContent = `Remember ${pageDomain} for this entry?`;152 const yesBtn = document.createElement("button"); yesBtn.className = "yes";153 yesBtn.textContent = "Remember";154 yesBtn.addEventListener("click", async () => {155 await chrome.runtime.sendMessage({156 type: "ADD_ADDITIONAL_URL",157 payload: { id: entryId, url: location.origin },158 });159 host.remove();160 });161 const noBtn = document.createElement("button"); noBtn.className = "no";162 noBtn.textContent = "Not now";163 noBtn.addEventListener("click", () => host.remove());164 bar.append(txt, yesBtn, noBtn);165 shadow.appendChild(bar);166 document.body.appendChild(host);167 setTimeout(() => host.remove(), 9000);168}169170function generatePasswordForSite(policy: PasswordPolicy = {}): string {171 return generateSmartPassword(policy);172}173174export function observeAnchorPosition(175 anchorEl: HTMLElement,176 onUpdate: () => void177): () => void {178 let rafId = 0;179 let burstUntil = 0;180181 const runUpdate = () => {182 rafId = 0;183 onUpdate();184 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);185 };186 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };187 const scheduleBurst = (ms = 220) => {188 burstUntil = Math.max(burstUntil, performance.now() + ms);189 scheduleUpdate();190 };191192 const resizeObserver = new ResizeObserver(() => scheduleBurst());193 resizeObserver.observe(anchorEl);194 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);195196 const mutationObserver = new MutationObserver(() => scheduleBurst());197 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });198 if (anchorEl.parentElement) {199 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });200 }201202 const onScroll = () => scheduleUpdate();203 window.addEventListener("scroll", onScroll, true);204 window.addEventListener("resize", onScroll);205 window.visualViewport?.addEventListener("resize", onScroll);206 window.visualViewport?.addEventListener("scroll", onScroll);207 anchorEl.addEventListener("focus", () => scheduleBurst());208 anchorEl.addEventListener("input", () => scheduleBurst());209210 scheduleBurst(260);211212 return () => {213 if (rafId) cancelAnimationFrame(rafId);214 resizeObserver.disconnect();215 mutationObserver.disconnect();216 window.removeEventListener("scroll", onScroll, true);217 window.removeEventListener("resize", onScroll);218 window.visualViewport?.removeEventListener("resize", onScroll);219 window.visualViewport?.removeEventListener("scroll", onScroll);220 };221}222223export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {224 const button = document.createElement("button");225 button.type = "button";226 button.setAttribute("aria-label", "Peach autofill");227 button.title = "Fill with Peach";228 button.innerHTML = peachIconSvg();229 230 231 232 233 button.style.cssText = `234 position: fixed; z-index: 2147483646;235 width: 26px; height: 26px; cursor: pointer;236 pointer-events: auto; border: none; padding: 3px;237 display: inline-flex; align-items: center; justify-content: center;238 background: transparent; border-radius: 999px;239 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;240 opacity: 0;241 `;242 button.addEventListener("mouseenter", () => {243 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;244 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;245 button.style.transform = "scale(1.08)";246 });247 button.addEventListener("mouseleave", () => {248 button.style.background = "transparent";249 button.style.boxShadow = "none";250 button.style.transform = "none";251 });252 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });253 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });254255 document.body.appendChild(button);256257 function updatePosition() {258 const rect = field.getBoundingClientRect();259 if (rect.width === 0 || rect.height === 0) {260 button.style.opacity = "0"; button.style.pointerEvents = "none";261 return;262 }263 const sz = 26;264 const inset = Math.max(5, Math.min(10, rect.height * 0.22));265 const top = rect.top + rect.height / 2 - sz / 2;266 267 268 const minLeft = rect.left + 4;269 const left = Math.max(minLeft, rect.right - sz - inset);270 271 if (rect.width < 40) {272 button.style.opacity = "0"; button.style.pointerEvents = "none";273 return;274 }275 button.style.top = `${top}px`;276 button.style.left = `${left}px`;277 button.style.opacity = "1"; button.style.pointerEvents = "auto";278 }279 updatePosition();280 requestAnimationFrame(() => { button.style.opacity = "1"; });281 const stopObserving = observeAnchorPosition(field, updatePosition);282 return () => { stopObserving(); button.remove(); };283}284285286287288289290291const SELECTOR_STYLES = `292 :host { all: initial; }293 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }294295 .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 }315316 .peach-inline-popup {317 background: var(--peach-bg);318 border: 1px solid var(--peach-border-accent, var(--peach-border));319 border-radius: 14px;320 overflow: hidden;321 box-shadow:322 0 8px 40px var(--peach-glow, transparent),323 0 2px 12px rgba(0,0,0,0.5);324 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;325 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;326 }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);333 }334 .peach-inline-popup.is-exiting {335 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;336 pointer-events: none;337 }338339 @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; }358359 /* Domain header */360 .domain-header {361 padding: 9px 13px 7px;362 border-bottom: 1px solid var(--peach-border);363 display: flex; align-items: center; justify-content: space-between;364 }365 .domain-eyebrow {366 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;367 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;368 }369 .domain-name {370 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);371 font-family: "IBM Plex Mono", monospace;372 }373374 /* Search */375 .search-wrap {376 padding: 9px 12px 7px;377 border-bottom: 1px solid var(--peach-border);378 }379 .search {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;383 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;384 transition: border-color 120ms, box-shadow 120ms;385 }386 .search:focus {387 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);388 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);389 }390 .search::placeholder { color: var(--peach-text-muted); }391392 /* Results */393 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }394 .results::-webkit-scrollbar { display: none; }395396 /* Entry rows */397 .entry {398 display: flex; align-items: center; padding: 8px 12px;399 cursor: pointer; border-bottom: 1px solid var(--peach-border);400 transition: background 70ms; position: relative; outline: none;401 }402 .entry:last-child { border-bottom: none; }403 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }404 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }405 .entry.focused::before {406 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;407 width: 2px; border-radius: 2px;408 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);409 }410411 .entry-icon {412 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;413 background: var(--peach-input-bg); border: 1px solid var(--peach-border);414 display: flex; align-items: center; justify-content: center;415 overflow: hidden; margin-right: 10px;416 }417 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }418 .entry-icon.is-new {419 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);420 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);421 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);422 }423424 .entry-info { flex: 1; min-width: 0; }425 .entry-title {426 font-size: 12.5px; color: var(--peach-text);427 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;428 line-height: 1.3;429 }430 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }431 .entry-sub {432 font-size: 10px; color: var(--peach-text-dim);433 font-family: "IBM Plex Mono", monospace;434 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;435 margin-top: 1px;436 }437438 .totp-badge {439 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;440 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);441 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);442 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;443 }444445 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }446447 .empty-hint {448 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);449 font-family: "IBM Plex Mono", monospace;450 }451452 /* Create form */453 .create-form-wrap {454 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;455 }456 .create-head {457 display: flex; align-items: center; gap: 8px;458 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);459 }460 .back-btn {461 background: transparent; border: none; cursor: pointer;462 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;463 display: flex; align-items: center; line-height: 0;464 transition: color 100ms, background 100ms;465 }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; }468 .create-body { padding: 12px; display: grid; gap: 9px; }469 .f-group { display: grid; gap: 3px; }470 .f-label {471 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;472 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;473 }474 .f-input {475 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);476 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;477 font-size: 11px; font-family: "IBM Plex Mono", monospace;478 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;479 }480 .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); }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; }483 .f-row { display: flex; gap: 6px; }484 .f-row .f-input { flex: 1; min-width: 0; }485 .gen-btn {486 padding: 0 10px; border-radius: 7px; border: none;487 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);488 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);489 font-size: 10px; font-family: "IBM Plex Mono", monospace;490 cursor: pointer; white-space: nowrap; flex-shrink: 0;491 display: flex; align-items: center; gap: 4px;492 transition: background 100ms;493 }494 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }495 .save-btn {496 width: 100%; padding: 8px; border-radius: 8px; border: none;497 background: var(--peach-accent); color: #fff; font-size: 12px;498 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;499 transition: background 120ms, box-shadow 120ms, transform 80ms;500 letter-spacing: 0.01em;501 }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 }506 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }507 .url-hint {508 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);509 font-family: "IBM Plex Mono", monospace; font-style: italic;510 }511512 /* Success */513 .success-wrap {514 display: flex; flex-direction: column; align-items: center;515 padding: 24px 16px 20px; gap: 8px;516 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;517 }518 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }519 .success-ring {520 width: 40px; height: 40px; border-radius: 50%;521 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);522 display: flex; align-items: center; justify-content: center;523 }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; }526527 /* Silver platter (State 1 — single match) */528 .silver-platter {529 padding: 14px 14px 12px;530 display: flex; flex-direction: column; gap: 6px;531 }532 .sp-title {533 font-size: 14px; font-weight: 500; color: var(--peach-text);534 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;535 line-height: 1.25;536 }537 .sp-username {538 font-size: 12px; color: var(--peach-text-dim);539 font-family: "IBM Plex Mono", monospace;540 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;541 margin-bottom: 6px;542 }543 .sp-fill-btn {544 width: 100%; padding: 11px; border-radius: 9px; border: none;545 background: var(--peach-accent); color: #fff;546 font-size: 13px; font-weight: 500; cursor: pointer;547 font-family: Inter, sans-serif; letter-spacing: 0.01em;548 display: flex; align-items: center; justify-content: center; min-height: 38px;549 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;550 }551 .sp-fill-btn:hover {552 filter: brightness(1.1);553 box-shadow: 0 4px 20px var(--peach-glow, transparent);554 transform: translateY(-1px);555 }556 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }557 .sp-fill-btn.filled {558 background: #16a34a; transform: none; filter: none;559 box-shadow: 0 4px 16px rgba(22,163,74,0.35);560 cursor: default;561 }562 .sp-fill-btn:disabled { cursor: default; }563 .sp-actions {564 display: flex; gap: 4px; justify-content: space-between; align-items: center;565 margin-top: 4px;566 }567 .sp-action-btn {568 flex: 1; height: 28px; border: none; border-radius: 6px;569 background: transparent; color: var(--peach-text-dim);570 font-size: 12px; cursor: pointer;571 transition: background 120ms ease, color 120ms ease;572 font-family: Inter, sans-serif; letter-spacing: 0.04em;573 display: inline-flex; align-items: center; justify-content: center; gap: 4px;574 }575 .sp-action-btn:hover:not(:disabled) {576 background: color-mix(in srgb, var(--peach-text) 5%, transparent);577 color: var(--peach-text);578 }579 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }580 .sp-action-btn.flashed {581 color: var(--peach-accent);582 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);583 }584585 /* Card silver platter (State 7) — reuses .sp-* primary action classes */586 .card-platter {587 padding: 12px 14px 10px;588 display: flex; flex-direction: column; gap: 4px;589 }590 .cp-network {591 font-size: 10px; font-weight: 600;592 color: var(--peach-accent);593 letter-spacing: 0.08em; text-transform: uppercase;594 font-family: Inter, sans-serif;595 }596 .cp-nickname {597 font-size: 14px; font-weight: 500; color: var(--peach-text);598 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;599 line-height: 1.2;600 }601 .cp-last4 {602 font-size: 12px; color: var(--peach-text-dim);603 font-family: "IBM Plex Mono", monospace;604 letter-spacing: 0.04em;605 margin-top: 2px;606 }607 .cp-meta {608 font-size: 11px; color: var(--peach-text-dim);609 font-family: "IBM Plex Mono", monospace;610 margin-top: 2px;611 display: flex; gap: 10px; align-items: center;612 }613 .cp-meta-sep { opacity: 0.5; }614 .cp-fill-wrap { margin-top: 10px; }615616 /* Locked state */617 .locked-wrap {618 display: flex; flex-direction: column; align-items: stretch;619 padding: 14px 13px 13px; gap: 9px;620 }621 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }622 .locked-icon {623 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;624 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);625 display: flex; align-items: center; justify-content: center;626 }627 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }628 .locked-input {629 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);630 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;631 font-size: 11px; font-family: "IBM Plex Mono", monospace;632 outline: none; transition: border-color 120ms, box-shadow 120ms;633 }634 .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); }635 .locked-input::placeholder { color: var(--peach-text-muted); }636 .locked-btn {637 width: 100%; padding: 8px; border-radius: 8px; border: none;638 background: var(--peach-accent); color: #fff; font-size: 12px;639 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;640 transition: background 120ms;641 letter-spacing: 0.01em;642 box-shadow: 0 2px 16px var(--peach-glow, transparent);643 animation: peach-pulse-glow 2.4s ease-in-out infinite;644 }645 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }646 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }647 .locked-error {648 font-size: 10px; color: rgba(220,80,60,0.85);649 font-family: "IBM Plex Mono", monospace;650 }651652 @keyframes peach-pulse-glow {653 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }654 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),655 0 0 48px var(--peach-glow, transparent); }656 }657`;658659export function showAutofillSelector(660 anchorEl: HTMLInputElement,661 entries: SelectorEntry[],662 onSelect: (id: string) => void,663 options: SelectorOptions = {}664): () => void {665 const host = document.createElement("div");666 host.style.cssText = "position: absolute; z-index: 2147483647;";667 host.style.setProperty("--peach-accent", accentHex);668 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));669 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));670 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));671 const shadow = host.attachShadow({ mode: "closed" });672673 const style = shadow.appendChild(document.createElement("style"));674 style.textContent = SELECTOR_STYLES;675676 const rect = anchorEl.getBoundingClientRect();677 host.style.top = `${rect.bottom + window.scrollY + 5}px`;678 host.style.left = `${rect.left + window.scrollX}px`;679 680 681 host.style.width = `${Math.max(rect.width, 320)}px`;682683 const container = shadow.appendChild(document.createElement("div"));684 container.className = "peach-inline-popup";685 container.setAttribute("data-theme", popupTheme);686687 const searchableEntries = options.searchableEntries ?? [];688 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();689690 let query = "";691 let focusedIndex = -1;692 let currentMode: "list" | "create" | "success" | "silver-platter" = "list";693 let searchInput: HTMLInputElement | null = null;694695 696 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";697 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");698 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";699700 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {701 const seen = new Set<string>();702 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });703 }704705 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {706 const q = value.trim().toLowerCase();707 const deduped = uniqueEntries(source);708 if (!q) return deduped.slice(0, 8);709 return deduped.filter(e =>710 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)711 ).slice(0, 12);712 }713714 function activeEntries(): SelectorEntry[] {715 716 if (query.trim()) {717 const merged = uniqueEntries([...entries, ...searchableEntries]);718 return filterEntries(merged, query);719 }720 721 if (entries.length > 0) return entries;722 return filterEntries(searchableEntries, "").slice(0, 8);723 }724725 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {726 const icon = document.createElement("div");727 icon.className = `entry-icon${isNew ? " is-new" : ""}`;728 if (isNew) {729 icon.innerHTML = plusSvgStr();730 } else {731 732 733 734 void url;735 icon.innerHTML = keySvgStr();736 }737 return icon;738 }739740 function renderList() {741 container.innerHTML = "";742 focusedIndex = -1;743 searchInput = null;744745 const current = activeEntries();746747 748 const domHdr = document.createElement("div");749 domHdr.className = "domain-header";750 const eyebrow = document.createElement("div");751 eyebrow.className = "domain-eyebrow";752 eyebrow.textContent = options.isSignup753 ? "Sign up as"754 : entries.length > 0 ? "Saved for this site" : "No saved login";755 const domName = document.createElement("div");756 domName.className = "domain-name";757 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");758 domHdr.append(eyebrow, domName);759 container.appendChild(domHdr);760761 762 const sw = document.createElement("div"); sw.className = "search-wrap";763 searchInput = document.createElement("input");764 searchInput.className = "search";765 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";766 searchInput.value = query;767 searchInput.addEventListener("input", () => {768 query = searchInput?.value ?? "";769 renderList();770 searchInput?.focus();771 searchInput?.setSelectionRange(query.length, query.length);772 });773 sw.appendChild(searchInput);774 container.appendChild(sw);775776 777 const results = document.createElement("div");778 results.className = "results";779 container.appendChild(results);780781 if (current.length === 0 && entries.length > 0) {782 783 }784785 for (let i = 0; i < current.length; i++) {786 const e = current[i]!;787 const entryEl = document.createElement("div");788 entryEl.className = "entry";789 entryEl.dataset.id = e.id;790 entryEl.dataset.index = String(i);791 entryEl.setAttribute("tabindex", "-1");792793 entryEl.appendChild(makeEntryIcon(e.url));794795 const info = document.createElement("div"); info.className = "entry-info";796 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;797 const subEl = document.createElement("div"); subEl.className = "entry-sub";798 const domain = e.url ? getDomainFromUrl(e.url) : "";799 subEl.textContent = domain || e.username || "";800 info.append(titleEl, subEl);801 entryEl.appendChild(info);802803 if (e.hasTotp) {804 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";805 entryEl.appendChild(badge);806 }807808 entryEl.addEventListener("click", () => {809 if (options.isSignup && options.onFillFields) {810 811 options.onFillFields(e.username ?? "", "");812 dismiss();813 } else {814 onSelect(e.id);815 dismiss();816 }817 });818 results.appendChild(entryEl);819 }820821 822 if (current.length > 0) {823 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));824 }825826 827 const newEl = document.createElement("div");828 newEl.className = "entry";829 newEl.setAttribute("tabindex", "-1");830 newEl.appendChild(makeEntryIcon(undefined, true));831832 const newInfo = document.createElement("div"); newInfo.className = "entry-info";833 const newTitle = document.createElement("div");834 newTitle.className = "entry-title is-new";835 newTitle.textContent = `New entry for ${sTitle}`;836 const newSub = document.createElement("div");837 newSub.className = "entry-sub";838 newSub.textContent = sOrigin;839 newInfo.append(newTitle, newSub);840 newEl.appendChild(newInfo);841 newEl.addEventListener("click", showCreateForm);842 results.appendChild(newEl);843844 845 if (searchInput) {846 requestAnimationFrame(() => searchInput?.focus());847 }848 }849850 function setFocused(idx: number) {851 const all = container.querySelectorAll<HTMLElement>(".entry");852 all.forEach((el, i) => el.classList.toggle("focused", i === idx));853 focusedIndex = idx;854 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });855 }856857 async function showCreateForm() {858 currentMode = "create";859 container.innerHTML = "";860861 862 863 864 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;865 if (!statusRes?.unlocked) {866 renderLocked();867 return;868 }869870 871 872 const sitePolicy = detectPasswordPolicy();873874 const wrap = document.createElement("div");875 wrap.className = "create-form-wrap";876877 878 const head = document.createElement("div"); head.className = "create-head";879 const backBtn = document.createElement("button"); backBtn.className = "back-btn";880 backBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 8 14" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><path d="M6 1L1.5 7L6 13"/></svg>`;881 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });882 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";883 head.append(backBtn, lbl);884885 886 const body = document.createElement("div"); body.className = "create-body";887888 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {889 const g = document.createElement("div"); g.className = "f-group";890 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;891 const inp = document.createElement("input"); inp.className = "f-input";892 g.append(l, inp); return { g, inp };893 }894895 896 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";897 body.appendChild(tg.g);898899 900 const ug = fGroup("website");901 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";902 if (loginSubdomain) {903 const h = document.createElement("div"); h.className = "url-hint";904 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);905 }906 body.appendChild(ug.g);907908 909 const xg = fGroup("username / email");910 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";911 xg.inp.setAttribute("autocomplete", "off");912 body.appendChild(xg.g);913914 915 const pg = fGroup("password");916 const pwInp = document.createElement("input"); pwInp.className = "f-input";917 pwInp.type = "text"; pwInp.value = prePassword || generatePasswordForSite(sitePolicy);918 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';919 pwInp.style.letterSpacing = "0.04em";920 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");921 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";922 genBtn.innerHTML = genSvgStr() + " regen";923 genBtn.addEventListener("click", () => { pwInp.value = generatePasswordForSite(sitePolicy); });924 const pwRow = document.createElement("div"); pwRow.className = "f-row";925 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);926 body.appendChild(pg.g);927928 929 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";930 saveBtn.textContent = "Save entry";931 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";932 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }933 saveBtn.addEventListener("click", async () => {934 const title = tg.inp.value.trim();935 const pw = pwInp.value.trim();936 const username = xg.inp.value.trim();937 if (!title) {938 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();939 showFormError("Entry needs a title");940 return;941 }942 if (!pw) {943 pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus();944 showFormError("Entry needs a password");945 return;946 }947 errEl.style.display = "none";948 saveBtn.disabled = true; saveBtn.textContent = "Saving…";949 try {950 const res = await chrome.runtime.sendMessage({951 type: "ADD_ENTRY",952 payload: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" },953 });954 if ((res as { ok?: boolean }).ok) {955 956 957 958 959 960 961 options.onFillFields?.(username, pw);962 showSuccess(title);963 } else {964 const errMsg = (res as { error?: string }).error === "vault locked"965 ? "Vault locked — open Peach to save"966 : "Could not save entry";967 showFormError(errMsg);968 saveBtn.disabled = false; saveBtn.textContent = "Save entry";969 }970 } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }971 });972 body.appendChild(saveBtn);973 body.appendChild(errEl);974975 wrap.append(head, body);976 container.appendChild(wrap);977 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });978 }979980 function showSuccess(title: string) {981 currentMode = "success";982 container.innerHTML = "";983 const wrap = document.createElement("div"); wrap.className = "success-wrap";984 const ring = document.createElement("div"); ring.className = "success-ring";985 ring.innerHTML = checkSvgStr();986 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;987 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;988 wrap.append(ring, t, s);989 container.appendChild(wrap);990 setTimeout(() => dismiss(), 1600);991 }992993 function flashActionBtn(btn: HTMLButtonElement) {994 btn.classList.add("flashed");995 setTimeout(() => btn.classList.remove("flashed"), 700);996 }997998 function renderSilverPlatter(entry: SelectorEntry) {999 container.innerHTML = "";1000 searchInput = null;1001 focusedIndex = -1;1002 currentMode = "silver-platter";10031004 const domHdr = document.createElement("div");1005 domHdr.className = "domain-header";1006 const eyebrow = document.createElement("div");1007 eyebrow.className = "domain-eyebrow";1008 eyebrow.textContent = "Saved for this site";1009 const domName = document.createElement("div");1010 domName.className = "domain-name";1011 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1012 domHdr.append(eyebrow, domName);1013 container.appendChild(domHdr);10141015 const platter = document.createElement("div");1016 platter.className = "silver-platter";10171018 const titleEl = document.createElement("div");1019 titleEl.className = "sp-title";1020 titleEl.textContent = entry.title;1021 platter.appendChild(titleEl);10221023 if (entry.username) {1024 const userEl = document.createElement("div");1025 userEl.className = "sp-username";1026 userEl.textContent = entry.username;1027 platter.appendChild(userEl);1028 }10291030 const fillBtn = document.createElement("button");1031 fillBtn.className = "sp-fill-btn";1032 fillBtn.type = "button";1033 fillBtn.textContent = "Fill credentials";1034 fillBtn.addEventListener("click", () => {1035 if (fillBtn.disabled) return;1036 fillBtn.disabled = true;1037 fillBtn.classList.add("filled");1038 fillBtn.innerHTML = checkSvgStr();1039 1040 1041 1042 1043 onSelect(entry.id);1044 setTimeout(() => dismiss(), 400);1045 });1046 platter.appendChild(fillBtn);10471048 const actions = document.createElement("div");1049 actions.className = "sp-actions";10501051 const copyU = document.createElement("button");1052 copyU.className = "sp-action-btn";1053 copyU.type = "button";1054 copyU.title = "Copy username";1055 copyU.textContent = "copy ⓤ";1056 copyU.disabled = !entry.username;1057 copyU.addEventListener("click", async (e) => {1058 e.stopPropagation();1059 if (!entry.username) return;1060 let alreadyWritten = false;1061 try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1062 1063 1064 1065 await chrome.runtime.sendMessage({1066 type: "COPY",1067 payload: { text: entry.username, label: "Username", alreadyWritten },1068 }).catch(() => {});1069 flashActionBtn(copyU);1070 });1071 actions.appendChild(copyU);10721073 const copyP = document.createElement("button");1074 copyP.className = "sp-action-btn";1075 copyP.type = "button";1076 copyP.title = "Copy password";1077 copyP.textContent = "copy ⓟ";1078 copyP.addEventListener("click", async (e) => {1079 e.stopPropagation();1080 const resp = await chrome.runtime.sendMessage({1081 type: "GET_ENTRY_FOR_FILL",1082 payload: { id: entry.id },1083 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;1084 if (!resp?.ok || !resp.data?.password) return;1085 let alreadyWritten = false;1086 try { await navigator.clipboard.writeText(resp.data.password); alreadyWritten = true; } catch {}1087 await chrome.runtime.sendMessage({1088 type: "COPY",1089 payload: { text: resp.data.password, label: "Password", alreadyWritten },1090 }).catch(() => {});1091 flashActionBtn(copyP);1092 });1093 actions.appendChild(copyP);10941095 const closeBtn = document.createElement("button");1096 closeBtn.className = "sp-action-btn";1097 closeBtn.type = "button";1098 closeBtn.title = "Dismiss";1099 closeBtn.textContent = "✕";1100 closeBtn.addEventListener("click", (e) => {1101 e.stopPropagation();1102 dismiss(true);1103 });1104 actions.appendChild(closeBtn);11051106 platter.appendChild(actions);1107 container.appendChild(platter);1108 }11091110 function renderLocked() {1111 container.innerHTML = "";11121113 1114 const domHdr = document.createElement("div");1115 domHdr.className = "domain-header";1116 const eyebrow = document.createElement("div");1117 eyebrow.className = "domain-eyebrow";1118 eyebrow.textContent = "vault locked";1119 const domName = document.createElement("div");1120 domName.className = "domain-name";1121 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1122 domHdr.append(eyebrow, domName);1123 container.appendChild(domHdr);11241125 const wrap = document.createElement("div");1126 wrap.className = "locked-wrap";11271128 1129 const head = document.createElement("div"); head.className = "locked-head";1130 const icon = document.createElement("div"); icon.className = "locked-icon";1131 icon.innerHTML = keySvgStr();1132 const title = document.createElement("div"); title.className = "locked-title";1133 title.textContent = "unlock to autofill";1134 head.append(icon, title);11351136 1137 const pwInp = document.createElement("input");1138 pwInp.className = "locked-input";1139 pwInp.type = "password";1140 pwInp.placeholder = "master password";1141 pwInp.setAttribute("autocomplete", "current-password");11421143 1144 const errEl = document.createElement("div");1145 errEl.className = "locked-error";1146 errEl.style.display = "none";11471148 1149 const unlockBtn = document.createElement("button");1150 unlockBtn.className = "locked-btn";1151 unlockBtn.type = "button";1152 unlockBtn.textContent = "unlock";11531154 async function tryUnlock() {1155 const pw = pwInp.value.trim();1156 if (!pw) { pwInp.focus(); return; }1157 unlockBtn.disabled = true;1158 unlockBtn.textContent = "unlocking…";1159 errEl.style.display = "none";1160 try {1161 const res = await chrome.runtime.sendMessage({1162 type: "UNLOCK",1163 payload: { password: pw },1164 });1165 if ((res as { ok?: boolean }).ok) {1166 dismiss();1167 options.onUnlocked?.();1168 } else {1169 errEl.textContent = "incorrect password";1170 errEl.style.display = "block";1171 pwInp.value = "";1172 pwInp.focus();1173 unlockBtn.disabled = false;1174 unlockBtn.textContent = "unlock";1175 }1176 } catch {1177 errEl.textContent = "could not reach Peach";1178 errEl.style.display = "block";1179 unlockBtn.disabled = false;1180 unlockBtn.textContent = "unlock";1181 }1182 }11831184 pwInp.addEventListener("keydown", (e) => {1185 e.stopPropagation();1186 if (e.key === "Enter") void tryUnlock();1187 });1188 pwInp.addEventListener("keyup", (e) => e.stopPropagation());1189 pwInp.addEventListener("keypress", (e) => e.stopPropagation());1190 unlockBtn.addEventListener("click", () => void tryUnlock());11911192 wrap.append(head, pwInp, errEl, unlockBtn);1193 container.appendChild(wrap);11941195 1196 1197 1198 1199 1200 1201 const captureKeydown = (e: KeyboardEvent) => {1202 if (!host.isConnected) {1203 document.removeEventListener("keydown", captureKeydown, true);1204 return;1205 }1206 1207 if (e.ctrlKey || e.metaKey || e.altKey || e.key === "Tab") return;1208 1209 e.stopImmediatePropagation();1210 if (e.key === "Escape") { dismiss(true); return; }1211 if (e.key === "Enter") { void tryUnlock(); return; }1212 1213 1214 if (document.activeElement !== host) {1215 pwInp.focus({ preventScroll: true });1216 }1217 };1218 captureKeydownRef = captureKeydown;1219 document.addEventListener("keydown", captureKeydown, true);12201221 1222 1223 pwInp.focus({ preventScroll: true });1224 requestAnimationFrame(() => { if (host.isConnected) pwInp.focus({ preventScroll: true }); });1225 }12261227 function handleKeyDown(e: KeyboardEvent) {1228 if (options.locked) {1229 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1230 return;1231 }1232 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter") {1233 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1234 return;1235 }1236 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");1237 const count = allEntryEls.length;1238 if (document.activeElement === searchInput) {1239 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }1240 if (count === 0) return;1241 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;1242 }1243 if (count === 0) return;1244 switch (e.key) {1245 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;1246 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;1247 case "Enter": {1248 if (focusedIndex >= 0) {1249 const el = allEntryEls[focusedIndex] as HTMLElement;1250 const id = el.dataset.id;1251 if (id) { onSelect(id); dismiss(); }1252 else if (el.classList.contains("is-new") || !id) {1253 1254 showCreateForm();1255 }1256 }1257 break;1258 }1259 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;1260 }1261 }12621263 document.addEventListener("keydown", handleKeyDown);12641265 document.body.appendChild(host);12661267 function updatePosition() {1268 const r = anchorEl.getBoundingClientRect();1269 host.style.top = `${r.bottom + window.scrollY + 5}px`;1270 host.style.left = `${r.left + window.scrollX}px`;1271 host.style.width = `${Math.max(r.width, 260)}px`;1272 }1273 1274 1275 1276 1277 1278 1279 1280 let captureKeydownRef: ((e: KeyboardEvent) => void) | null = null;1281 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);12821283 if (options.locked) {1284 renderLocked();1285 } else if (entries.length === 1 && !options.isSignup) {1286 1287 1288 1289 1290 renderSilverPlatter(entries[0]!);1291 } else {1292 renderList();1293 }12941295 let exitScheduled = false;1296 function dismiss(userAction = false) {1297 1298 1299 1300 if (exitScheduled) return;1301 exitScheduled = true;1302 1303 1304 1305 stopObserving();1306 document.removeEventListener("click", outsideClick, { capture: true });1307 document.removeEventListener("keydown", handleKeyDown);1308 if (captureKeydownRef) {1309 document.removeEventListener("keydown", captureKeydownRef, true);1310 captureKeydownRef = null;1311 }1312 if (userAction) options.onDismiss?.();1313 container.classList.add("is-exiting");1314 setTimeout(() => host.remove(), 130);1315 }13161317 function outsideClick(ev: MouseEvent) {1318 const path = ev.composedPath();1319 1320 1321 1322 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);1323 }1324 1325 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);13261327 return dismiss;1328}13291330export interface CardSelectorEntry {1331 id: string;1332 title: string;1333 cardholderName?: string;1334 last4?: string;1335 expMonth?: string;1336 expYear?: string;1337 network?: import("../shared/types").CardNetwork;1338}13391340interface CardSelectorOptions {1341 domain?: string;1342 1343 onFill: (entry: import("../shared/types").VaultEntry) => void;1344 1345 onDismiss?: () => void;1346}134713481349135013511352135313541355export function showCardSelector(1356 anchorEl: HTMLInputElement,1357 cards: CardSelectorEntry[],1358 options: CardSelectorOptions1359): () => void {1360 if (cards.length === 0) return () => {};1361 const card = cards[0]!;13621363 const host = document.createElement("div");1364 host.style.cssText = "position: absolute; z-index: 2147483647;";1365 host.style.setProperty("--peach-accent", accentHex);1366 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));1367 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));1368 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1369 const shadow = host.attachShadow({ mode: "closed" });13701371 const style = shadow.appendChild(document.createElement("style"));1372 style.textContent = SELECTOR_STYLES;13731374 const rect = anchorEl.getBoundingClientRect();1375 host.style.top = `${rect.bottom + window.scrollY + 5}px`;1376 host.style.left = `${rect.left + window.scrollX}px`;1377 host.style.width = `${Math.max(rect.width, 320)}px`;13781379 const container = shadow.appendChild(document.createElement("div"));1380 container.className = "peach-inline-popup";1381 container.setAttribute("data-theme", popupTheme);13821383 1384 const domHdr = document.createElement("div");1385 domHdr.className = "domain-header";1386 const eyebrow = document.createElement("div");1387 eyebrow.className = "domain-eyebrow";1388 eyebrow.textContent = "Pay with Peach";1389 const domName = document.createElement("div");1390 domName.className = "domain-name";1391 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1392 domHdr.append(eyebrow, domName);1393 container.appendChild(domHdr);13941395 const platter = document.createElement("div");1396 platter.className = "card-platter";13971398 if (card.network && card.network !== "unknown") {1399 const net = document.createElement("div");1400 net.className = "cp-network";1401 net.textContent = card.network;1402 platter.appendChild(net);1403 }14041405 const nick = document.createElement("div");1406 nick.className = "cp-nickname";1407 nick.textContent = card.title;1408 platter.appendChild(nick);14091410 if (card.last4) {1411 const l4 = document.createElement("div");1412 l4.className = "cp-last4";1413 l4.textContent = `•••• ${card.last4}`;1414 platter.appendChild(l4);1415 }14161417 const meta = document.createElement("div");1418 meta.className = "cp-meta";1419 if (card.cardholderName) {1420 const name = document.createElement("span");1421 name.textContent = card.cardholderName;1422 meta.appendChild(name);1423 }1424 if (card.expMonth && card.expYear) {1425 if (card.cardholderName) {1426 const sep = document.createElement("span");1427 sep.className = "cp-meta-sep";1428 sep.textContent = "·";1429 meta.appendChild(sep);1430 }1431 const exp = document.createElement("span");1432 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;1433 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;1434 meta.appendChild(exp);1435 }1436 if (meta.childElementCount > 0) platter.appendChild(meta);14371438 const fillWrap = document.createElement("div");1439 fillWrap.className = "cp-fill-wrap";1440 const fillBtn = document.createElement("button");1441 fillBtn.className = "sp-fill-btn";1442 fillBtn.type = "button";1443 fillBtn.textContent = "Fill card details";1444 fillBtn.addEventListener("click", async () => {1445 if (fillBtn.disabled) return;1446 fillBtn.disabled = true;1447 fillBtn.classList.add("filled");1448 fillBtn.innerHTML = checkSvgStr();1449 const resp = await chrome.runtime.sendMessage({1450 type: "GET_ENTRY_FOR_FILL",1451 payload: { id: card.id },1452 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;1453 if (resp?.ok && resp.data) options.onFill(resp.data);1454 setTimeout(() => dismiss(), 400);1455 });1456 fillWrap.appendChild(fillBtn);1457 platter.appendChild(fillWrap);14581459 1460 const actions = document.createElement("div");1461 actions.className = "sp-actions";14621463 function flashBtn(b: HTMLButtonElement) {1464 b.classList.add("flashed");1465 setTimeout(() => b.classList.remove("flashed"), 700);1466 }14671468 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {1469 const resp = await chrome.runtime.sendMessage({1470 type: "GET_ENTRY_FOR_FILL",1471 payload: { id: card.id },1472 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;1473 return resp?.ok && resp.data ? resp.data : null;1474 }14751476 const copyNum = document.createElement("button");1477 copyNum.className = "sp-action-btn";1478 copyNum.type = "button";1479 copyNum.title = "Copy card number";1480 copyNum.textContent = "copy #";1481 copyNum.addEventListener("click", async (e) => {1482 e.stopPropagation();1483 const full = await fetchFullCard();1484 if (!full?.cardNumber) return;1485 let alreadyWritten = false;1486 try { await navigator.clipboard.writeText(full.cardNumber); alreadyWritten = true; } catch {}1487 await chrome.runtime.sendMessage({1488 type: "COPY",1489 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },1490 }).catch(() => {});1491 flashBtn(copyNum);1492 });1493 actions.appendChild(copyNum);14941495 const copyCvc = document.createElement("button");1496 copyCvc.className = "sp-action-btn";1497 copyCvc.type = "button";1498 copyCvc.title = "Copy CVC";1499 copyCvc.textContent = "copy CVC";1500 copyCvc.addEventListener("click", async (e) => {1501 e.stopPropagation();1502 const full = await fetchFullCard();1503 if (!full?.cvc) return;1504 let alreadyWritten = false;1505 try { await navigator.clipboard.writeText(full.cvc); alreadyWritten = true; } catch {}1506 await chrome.runtime.sendMessage({1507 type: "COPY",1508 payload: { text: full.cvc, label: "CVC", alreadyWritten },1509 }).catch(() => {});1510 flashBtn(copyCvc);1511 });1512 actions.appendChild(copyCvc);15131514 const closeBtn = document.createElement("button");1515 closeBtn.className = "sp-action-btn";1516 closeBtn.type = "button";1517 closeBtn.title = "Dismiss";1518 closeBtn.textContent = "✕";1519 closeBtn.addEventListener("click", (e) => {1520 e.stopPropagation();1521 dismiss(true);1522 });1523 actions.appendChild(closeBtn);15241525 platter.appendChild(actions);1526 container.appendChild(platter);15271528 document.body.appendChild(host);1529 function updatePosition() {1530 const r = anchorEl.getBoundingClientRect();1531 host.style.top = `${r.bottom + window.scrollY + 5}px`;1532 host.style.left = `${r.left + window.scrollX}px`;1533 host.style.width = `${Math.max(r.width, 320)}px`;1534 }1535 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);15361537 function onKeyDown(e: KeyboardEvent) {1538 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1539 }1540 document.addEventListener("keydown", onKeyDown);15411542 let exitScheduled = false;1543 function dismiss(userAction = false) {1544 if (exitScheduled) return;1545 exitScheduled = true;1546 stopObserving();1547 document.removeEventListener("click", outsideClick, { capture: true });1548 document.removeEventListener("keydown", onKeyDown);1549 if (userAction) options.onDismiss?.();1550 container.classList.add("is-exiting");1551 setTimeout(() => host.remove(), 130);1552 }15531554 function outsideClick(ev: MouseEvent) {1555 const path = ev.composedPath();1556 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);1557 }1558 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);15591560 return dismiss;1561}