123456789101112import { ACCENT_COLORS, type PasswordPolicy, type SavedIdentity } from "../shared/types";13import { generateSmartPassword } from "../shared/generate";14import { generateTOTP } from "../crypto/totp";15import { detectPasswordPolicy } from "./policy";1617let accentHex = "#E8956D";18let popupTheme: "dark" | "light" = "dark";19chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string; theme?: string } } | undefined) => {20 const data = resp?.data;21 const name = data?.accentColor ?? "peach";22 accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";23 popupTheme = data?.theme === "light" ? "light" : "dark";24});2526272829function withAlpha(hex: string, alpha: string): string {30 31 32 33 return /^#[0-9a-f]{6}$/i.test(hex) ? hex + alpha : hex;34}3536export interface SelectorEntry {37 id: string;38 title: string;39 username?: string;40 url?: string;41 hasTotp?: boolean;42 43 hasPasskey?: boolean;44}4546interface SelectorOptions {47 domain?: string;48 searchableEntries?: SelectorEntry[];49 50 isSignup?: boolean;51 52535455 savedIdentities?: SavedIdentity[];56 5758596061 onFillFields?: (username: string, password: string) => void;62 6364656667 locked?: boolean;68 69707172 onUnlocked?: () => void;73 7475767778 onDismiss?: () => void;79}8081function getDomainFromUrl(url: string): string {82 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }83}8485function keySvgStr(): string {86 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>`;87}8889function plusSvgStr(): string {90 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>`;91}9293function peachIconSvg(): string {94 95 96 97 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>`;98}99100function genSvgStr(): string {101 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>`;102}103104function checkSvgStr(): string {105 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>`;106}107108function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {109 const { hostname, origin } = location;110 const parts = hostname.split(".");111 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",112 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",113 "oidc", "federation", "saml", "cas", "ldap"];114 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());115 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;116 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);117 return { origin, title, loginSubdomain };118}119120121122123124125export function showRememberSiteToast(126 pageDomain: string,127 entryId: string,128): void {129 const host = document.createElement("div");130 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";131 host.style.setProperty("--peach-accent", accentHex);132 const shadow = host.attachShadow({ mode: "closed" });133134 const style = document.createElement("style");135 style.textContent = `136 .bar {137 display:flex;align-items:center;gap:10px;padding:10px 14px;138 background:#131109;border:1px solid rgba(240,232,216,0.09);139 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);140 font-family:Inter,sans-serif;white-space:nowrap;141 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;142 }143 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }144 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }145 .yes {146 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;147 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;148 }149 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }150 .no {151 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;152 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;153 }154 `;155 shadow.appendChild(style);156157 const bar = document.createElement("div"); bar.className = "bar";158 const txt = document.createElement("span"); txt.className = "txt";159 txt.textContent = `Remember ${pageDomain} for this entry?`;160 const yesBtn = document.createElement("button"); yesBtn.className = "yes";161 yesBtn.textContent = "Remember";162 yesBtn.addEventListener("click", async () => {163 await chrome.runtime.sendMessage({164 type: "ADD_ADDITIONAL_URL",165 payload: { id: entryId, url: location.origin },166 });167 host.remove();168 });169 const noBtn = document.createElement("button"); noBtn.className = "no";170 noBtn.textContent = "Not now";171 noBtn.addEventListener("click", () => host.remove());172 bar.append(txt, yesBtn, noBtn);173 shadow.appendChild(bar);174 document.body.appendChild(host);175 setTimeout(() => host.remove(), 9000);176}177178function generatePasswordForSite(policy: PasswordPolicy = {}): string {179 return generateSmartPassword(policy);180}181182export function observeAnchorPosition(183 anchorEl: HTMLElement,184 onUpdate: () => void185): () => void {186 let rafId = 0;187 let burstUntil = 0;188189 const runUpdate = () => {190 rafId = 0;191 onUpdate();192 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);193 };194 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };195 const scheduleBurst = (ms = 220) => {196 burstUntil = Math.max(burstUntil, performance.now() + ms);197 scheduleUpdate();198 };199200 const resizeObserver = new ResizeObserver(() => scheduleBurst());201 resizeObserver.observe(anchorEl);202 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);203204 const mutationObserver = new MutationObserver(() => scheduleBurst());205 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });206 if (anchorEl.parentElement) {207 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });208 }209210 const onScroll = () => scheduleUpdate();211 window.addEventListener("scroll", onScroll, true);212 window.addEventListener("resize", onScroll);213 window.visualViewport?.addEventListener("resize", onScroll);214 window.visualViewport?.addEventListener("scroll", onScroll);215 anchorEl.addEventListener("focus", () => scheduleBurst());216 anchorEl.addEventListener("input", () => scheduleBurst());217218 scheduleBurst(260);219220 return () => {221 if (rafId) cancelAnimationFrame(rafId);222 resizeObserver.disconnect();223 mutationObserver.disconnect();224 window.removeEventListener("scroll", onScroll, true);225 window.removeEventListener("resize", onScroll);226 window.visualViewport?.removeEventListener("resize", onScroll);227 window.visualViewport?.removeEventListener("scroll", onScroll);228 };229}230231export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {232 const button = document.createElement("button");233 button.type = "button";234 button.setAttribute("aria-label", "Peach autofill");235 button.title = "Fill with Peach";236 button.innerHTML = peachIconSvg();237 238 239 240 241 button.style.cssText = `242 position: fixed; z-index: 2147483646;243 width: 26px; height: 26px; cursor: pointer;244 pointer-events: auto; border: none; padding: 3px;245 display: inline-flex; align-items: center; justify-content: center;246 background: transparent; border-radius: 999px;247 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;248 opacity: 0;249 `;250 button.addEventListener("mouseenter", () => {251 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;252 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;253 button.style.transform = "scale(1.08)";254 });255 button.addEventListener("mouseleave", () => {256 button.style.background = "transparent";257 button.style.boxShadow = "none";258 button.style.transform = "none";259 });260 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });261 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });262263 document.body.appendChild(button);264265 function updatePosition() {266 const rect = field.getBoundingClientRect();267 if (rect.width === 0 || rect.height === 0) {268 button.style.opacity = "0"; button.style.pointerEvents = "none";269 return;270 }271 const sz = 26;272 const inset = Math.max(5, Math.min(10, rect.height * 0.22));273 const top = rect.top + rect.height / 2 - sz / 2;274 275 276 const minLeft = rect.left + 4;277 const left = Math.max(minLeft, rect.right - sz - inset);278 279 if (rect.width < 40) {280 button.style.opacity = "0"; button.style.pointerEvents = "none";281 return;282 }283 button.style.top = `${top}px`;284 button.style.left = `${left}px`;285 button.style.opacity = "1"; button.style.pointerEvents = "auto";286 }287 updatePosition();288 requestAnimationFrame(() => { button.style.opacity = "1"; });289 const stopObserving = observeAnchorPosition(field, updatePosition);290 return () => { stopObserving(); button.remove(); };291}292293294295296297298299const SELECTOR_STYLES = `300 :host { all: initial; }301 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }302303 .peach-inline-popup[data-theme="dark"] {304 --peach-bg: #1a1410;305 --peach-bg-card: #221c16;306 --peach-text: #f5f0ea;307 --peach-text-dim: #8a7a6a;308 --peach-text-muted: #4a3a2a;309 --peach-border: rgba(255,255,255,0.07);310 --peach-input-bg: rgba(255,255,255,0.05);311 --peach-input-border: rgba(255,255,255,0.08);312 }313 .peach-inline-popup[data-theme="light"] {314 --peach-bg: #fdf8f4;315 --peach-bg-card: #f5ede4;316 --peach-text: #1c1310;317 --peach-text-dim: #6b5a4a;318 --peach-text-muted: #a89080;319 --peach-border: rgba(0,0,0,0.08);320 --peach-input-bg: rgba(0,0,0,0.04);321 --peach-input-border: rgba(0,0,0,0.10);322 }323324 .peach-inline-popup {325 background: var(--peach-bg);326 border: 1px solid var(--peach-border-accent, var(--peach-border));327 border-radius: 14px;328 overflow: hidden;329 box-shadow:330 0 8px 40px var(--peach-glow, transparent),331 0 2px 12px rgba(0,0,0,0.5);332 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;333 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;334 }335 /* Light theme: shadow against a light page is too strong otherwise — drop both336 the accent glow and the dark drop-shadow opacity to keep the popup elegant. */337 .peach-inline-popup[data-theme="light"] {338 box-shadow:339 0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),340 0 2px 12px rgba(0,0,0,0.12);341 }342 .peach-inline-popup.is-exiting {343 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;344 pointer-events: none;345 }346347 @keyframes peach-enter {348 0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }349 65% { opacity: 1; filter: blur(0); }350 100% { opacity: 1; transform: translateY(0) scale(1); }351 }352 @keyframes peach-exit {353 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }354 100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }355 }356 @keyframes peach-slide-in {357 from { opacity: 0; transform: translateX(12px); }358 to { opacity: 1; transform: translateX(0); }359 }360 @keyframes peach-slide-back {361 from { opacity: 0; transform: translateX(-12px); }362 to { opacity: 1; transform: translateX(0); }363 }364 .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }365 .peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }366367 /* Domain header */368 .domain-header {369 padding: 9px 13px 7px;370 border-bottom: 1px solid var(--peach-border);371 display: flex; align-items: center; justify-content: space-between;372 }373 .domain-eyebrow {374 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;375 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;376 }377 .domain-name {378 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);379 font-family: "IBM Plex Mono", monospace;380 }381382 /* Search */383 .search-wrap {384 padding: 9px 12px 7px;385 border-bottom: 1px solid var(--peach-border);386 }387 .search {388 width: 100%; background: var(--peach-input-bg);389 border: 1px solid var(--peach-input-border);390 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;391 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;392 transition: border-color 120ms, box-shadow 120ms;393 }394 .search:focus {395 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);396 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);397 }398 .search::placeholder { color: var(--peach-text-muted); }399400 /* Results */401 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }402 .results::-webkit-scrollbar { display: none; }403404 /* Entry rows */405 .entry {406 display: flex; align-items: center; padding: 8px 12px;407 cursor: pointer; border-bottom: 1px solid var(--peach-border);408 transition: background 70ms; position: relative; outline: none;409 }410 .entry:last-child { border-bottom: none; }411 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }412 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }413 .entry.focused::before {414 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;415 width: 2px; border-radius: 2px;416 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);417 }418419 .entry-icon {420 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;421 background: var(--peach-input-bg); border: 1px solid var(--peach-border);422 display: flex; align-items: center; justify-content: center;423 overflow: hidden; margin-right: 10px;424 }425 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }426 .entry-icon.is-new {427 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);428 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);429 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);430 }431432 .entry-info { flex: 1; min-width: 0; }433 .entry-title {434 font-size: 12.5px; color: var(--peach-text);435 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;436 line-height: 1.3;437 }438 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }439 .entry-sub {440 font-size: 10px; color: var(--peach-text-dim);441 font-family: "IBM Plex Mono", monospace;442 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;443 margin-top: 1px;444 }445446 .totp-badge {447 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;448 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);449 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);450 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;451 }452 .passkey-badge {453 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;454 border-radius: 4px;455 background: color-mix(in srgb, var(--peach-accent) 14%, transparent);456 color: color-mix(in srgb, var(--peach-accent) 85%, transparent);457 border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);458 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;459 }460461 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }462463 .empty-hint {464 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);465 font-family: "IBM Plex Mono", monospace;466 }467468 /* Create form */469 .create-form-wrap {470 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;471 }472 .create-head {473 display: flex; align-items: center; gap: 8px;474 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);475 }476 .back-btn {477 background: transparent; border: none; cursor: pointer;478 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;479 display: flex; align-items: center; line-height: 0;480 transition: color 100ms, background 100ms;481 }482 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }483 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }484 .create-body { padding: 12px; display: grid; gap: 9px; }485 .f-group { display: grid; gap: 3px; }486 .f-label {487 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;488 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;489 }490 .f-input {491 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);492 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;493 font-size: 11px; font-family: "IBM Plex Mono", monospace;494 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;495 }496 .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); }497 .f-input::placeholder { color: var(--peach-text-muted); }498 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }499 .f-row { display: flex; gap: 6px; }500 .f-row .f-input { flex: 1; min-width: 0; }501 .gen-btn {502 padding: 0 10px; border-radius: 7px; border: none;503 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);504 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);505 font-size: 10px; font-family: "IBM Plex Mono", monospace;506 cursor: pointer; white-space: nowrap; flex-shrink: 0;507 display: flex; align-items: center; gap: 4px;508 transition: background 100ms;509 }510 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }511 .save-btn {512 width: 100%; padding: 8px; border-radius: 8px; border: none;513 background: var(--peach-accent); color: #fff; font-size: 12px;514 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;515 transition: background 120ms, box-shadow 120ms, transform 80ms;516 letter-spacing: 0.01em;517 }518 .save-btn:hover {519 background: color-mix(in srgb, var(--peach-accent) 82%, #000);520 box-shadow: 0 4px 20px var(--peach-glow, transparent);521 }522 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }523 .url-hint {524 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);525 font-family: "IBM Plex Mono", monospace; font-style: italic;526 }527528 /* Success */529 .success-wrap {530 display: flex; flex-direction: column; align-items: center;531 padding: 24px 16px 20px; gap: 8px;532 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;533 }534 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }535 .success-ring {536 width: 40px; height: 40px; border-radius: 50%;537 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);538 display: flex; align-items: center; justify-content: center;539 }540 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }541 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }542543 /* Silver platter (State 1 — single match) */544 .silver-platter {545 padding: 14px 14px 12px;546 display: flex; flex-direction: column; gap: 6px;547 }548 .sp-title {549 font-size: 14px; font-weight: 500; color: var(--peach-text);550 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;551 line-height: 1.25;552 }553 .sp-username {554 font-size: 12px; color: var(--peach-text-dim);555 font-family: "IBM Plex Mono", monospace;556 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;557 margin-bottom: 6px;558 }559 .sp-fill-btn {560 width: 100%; padding: 11px; border-radius: 9px; border: none;561 background: var(--peach-accent); color: #fff;562 font-size: 13px; font-weight: 500; cursor: pointer;563 font-family: Inter, sans-serif; letter-spacing: 0.01em;564 display: flex; align-items: center; justify-content: center; min-height: 38px;565 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;566 }567 .sp-fill-btn:hover {568 filter: brightness(1.1);569 box-shadow: 0 4px 20px var(--peach-glow, transparent);570 transform: translateY(-1px);571 }572 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }573 .sp-fill-btn.filled {574 background: #16a34a; transform: none; filter: none;575 box-shadow: 0 4px 16px rgba(22,163,74,0.35);576 cursor: default;577 }578 .sp-fill-btn:disabled { cursor: default; }579 .sp-actions {580 display: flex; gap: 4px; justify-content: space-between; align-items: center;581 margin-top: 4px;582 }583 .sp-action-btn {584 flex: 1; height: 28px; border: none; border-radius: 6px;585 background: transparent; color: var(--peach-text-dim);586 font-size: 12px; cursor: pointer;587 transition: background 120ms ease, color 120ms ease;588 font-family: Inter, sans-serif; letter-spacing: 0.04em;589 display: inline-flex; align-items: center; justify-content: center; gap: 4px;590 }591 .sp-action-btn:hover:not(:disabled) {592 background: color-mix(in srgb, var(--peach-text) 5%, transparent);593 color: var(--peach-text);594 }595 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }596 .sp-action-btn.flashed {597 color: var(--peach-accent);598 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);599 }600601 /* TOTP tray — shown below the silver-platter secondary actions */602 .sp-tray {603 margin-top: 8px;604 padding: 10px 12px;605 border-radius: 9px;606 background: var(--peach-input-bg);607 border: 1px solid var(--peach-input-border);608 display: flex; align-items: center; gap: 12px;609 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;610 }611 .sp-tray-label {612 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;613 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;614 flex-shrink: 0;615 }616 .sp-totp-code {617 font-size: 18px; font-weight: 500;618 color: var(--peach-text);619 font-family: "IBM Plex Mono", monospace;620 letter-spacing: 0.12em;621 flex: 1;622 text-align: center;623 }624 .sp-totp-ring {625 width: 22px; height: 22px; flex-shrink: 0;626 }627 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }628 .sp-totp-ring .ring-fill {629 stroke: var(--peach-accent);630 transition: stroke-dashoffset 1s linear;631 transform: rotate(-90deg);632 transform-origin: center;633 }634 .sp-totp-copy {635 background: transparent; border: none; cursor: pointer;636 padding: 4px 8px; border-radius: 5px;637 color: var(--peach-text-dim); font-size: 11px;638 font-family: Inter, sans-serif;639 transition: background 120ms, color 120ms;640 }641 .sp-totp-copy:hover {642 background: color-mix(in srgb, var(--peach-text) 5%, transparent);643 color: var(--peach-text);644 }645646 /* Card silver platter (State 7) — reuses .sp-* primary action classes */647 .card-platter {648 padding: 12px 14px 10px;649 display: flex; flex-direction: column; gap: 4px;650 }651 .cp-network {652 font-size: 10px; font-weight: 600;653 color: var(--peach-accent);654 letter-spacing: 0.08em; text-transform: uppercase;655 font-family: Inter, sans-serif;656 }657 .cp-nickname {658 font-size: 14px; font-weight: 500; color: var(--peach-text);659 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;660 line-height: 1.2;661 }662 .cp-last4 {663 font-size: 12px; color: var(--peach-text-dim);664 font-family: "IBM Plex Mono", monospace;665 letter-spacing: 0.04em;666 margin-top: 2px;667 }668 .cp-meta {669 font-size: 11px; color: var(--peach-text-dim);670 font-family: "IBM Plex Mono", monospace;671 margin-top: 2px;672 display: flex; gap: 10px; align-items: center;673 }674 .cp-meta-sep { opacity: 0.5; }675 .cp-fill-wrap { margin-top: 10px; }676677 /* Locked state */678 .locked-wrap {679 display: flex; flex-direction: column; align-items: stretch;680 padding: 14px 13px 13px; gap: 9px;681 }682 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }683 .locked-icon {684 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;685 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);686 display: flex; align-items: center; justify-content: center;687 }688 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }689 .locked-input {690 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);691 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;692 font-size: 11px; font-family: "IBM Plex Mono", monospace;693 outline: none; transition: border-color 120ms, box-shadow 120ms;694 }695 .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); }696 .locked-input::placeholder { color: var(--peach-text-muted); }697 .locked-btn {698 width: 100%; padding: 8px; border-radius: 8px; border: none;699 background: var(--peach-accent); color: #fff; font-size: 12px;700 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;701 transition: background 120ms;702 letter-spacing: 0.01em;703 box-shadow: 0 2px 16px var(--peach-glow, transparent);704 animation: peach-pulse-glow 2.4s ease-in-out infinite;705 }706 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }707 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }708 .locked-error {709 font-size: 10px; color: rgba(220,80,60,0.85);710 font-family: "IBM Plex Mono", monospace;711 }712713 @keyframes peach-pulse-glow {714 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }715 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),716 0 0 48px var(--peach-glow, transparent); }717 }718719 /* ── State 10: identity helper ─────────────────────────────── */720 .identity-list { max-height: 192px; overflow-y: auto; padding: 4px 0; }721 .peach-identity-row {722 display: flex; align-items: center; gap: 10px;723 padding: 9px 12px; cursor: pointer; border-radius: 8px;724 transition: background 100ms ease;725 }726 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }727 .peach-identity-value {728 font-family: "IBM Plex Mono", monospace; font-size: 13px;729 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;730 }731 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }732 .identity-alt-row {733 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;734 border-radius: 8px; transition: background 100ms ease;735 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;736 }737 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }738 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }739 .identity-alt-input-wrap.open { display: flex; }740 .identity-alt-input {741 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;742 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);743 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;744 outline: none; transition: border-color 120ms;745 }746 .identity-alt-input:focus {747 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);748 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);749 }750 .identity-alt-confirm {751 height: 32px; padding: 0 12px; border-radius: 7px; border: none;752 background: var(--peach-accent); color: #fff; font-size: 12px;753 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;754 }755 /* ── Generator view (after identity picked) ────────────────── */756 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }757 .gen-confirmed {758 display: flex; align-items: center; gap: 8px; padding: 7px 10px;759 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);760 border-radius: 8px;761 }762 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }763 .gen-confirmed-val {764 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);765 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;766 }767 .gen-label {768 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;769 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;770 }771 .gen-pw-row { display: flex; gap: 6px; align-items: center; }772 .gen-pw-val {773 flex: 1; padding: 8px 10px; border-radius: 7px;774 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);775 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);776 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;777 }778 .gen-regen-btn {779 height: 34px; padding: 0 10px; border-radius: 7px; border: none;780 background: color-mix(in srgb, var(--peach-text) 8%, transparent);781 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;782 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;783 }784 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }785 .gen-fill-btn {786 padding: 10px; border-radius: 8px; border: none;787 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;788 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;789 transition: filter 120ms ease, transform 80ms ease;790 }791 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }792 .gen-fill-btn:active { transform: translateY(0); }793 /* ── Save offer (after password filled) ───────────────────── */794 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }795 .save-offer-title {796 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;797 }798 .save-offer-row {799 display: flex; align-items: center; gap: 8px; padding: 6px 8px;800 border-radius: 7px; background: var(--peach-input-bg);801 border: 1px solid var(--peach-input-border);802 }803 .save-offer-lbl {804 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;805 font-family: "IBM Plex Mono", monospace;806 }807 .save-offer-val {808 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);809 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;810 }811 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }812 .save-offer-save {813 flex: 1; padding: 9px; border-radius: 8px; border: none;814 background: var(--peach-accent); color: #fff; font-size: 13px;815 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;816 }817 .save-offer-save:hover { filter: brightness(1.08); }818 .save-offer-skip {819 flex: 1; padding: 9px; border-radius: 8px; border: none;820 background: color-mix(in srgb, var(--peach-text) 8%, transparent);821 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;822 font-family: Inter, sans-serif; transition: background 100ms ease;823 }824 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }825`;826827export function showAutofillSelector(828 anchorEl: HTMLInputElement,829 entries: SelectorEntry[],830 onSelect: (id: string) => void,831 options: SelectorOptions = {}832): () => void {833 const host = document.createElement("div");834 host.style.cssText = "position: absolute; z-index: 2147483647;";835 host.style.setProperty("--peach-accent", accentHex);836 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));837 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));838 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));839 const shadow = host.attachShadow({ mode: "closed" });840841 const style = shadow.appendChild(document.createElement("style"));842 style.textContent = SELECTOR_STYLES;843844 const rect = anchorEl.getBoundingClientRect();845 host.style.top = `${rect.bottom + window.scrollY + 5}px`;846 host.style.left = `${rect.left + window.scrollX}px`;847 848 849 host.style.width = `${Math.max(rect.width, 320)}px`;850851 const container = shadow.appendChild(document.createElement("div"));852 container.className = "peach-inline-popup";853 container.setAttribute("data-theme", popupTheme);854855 const searchableEntries = options.searchableEntries ?? [];856 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();857858 let query = "";859 let focusedIndex = -1;860 let currentMode: "list" | "create" | "success" | "silver-platter" = "list";861 let searchInput: HTMLInputElement | null = null;862 863 864 const cleanupHooks: Array<() => void> = [];865866 867 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";868 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");869 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";870871 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {872 const seen = new Set<string>();873 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });874 }875876 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {877 const q = value.trim().toLowerCase();878 const deduped = uniqueEntries(source);879 if (!q) return deduped.slice(0, 8);880 return deduped.filter(e =>881 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)882 ).slice(0, 12);883 }884885 function activeEntries(): SelectorEntry[] {886 887 if (query.trim()) {888 const merged = uniqueEntries([...entries, ...searchableEntries]);889 return filterEntries(merged, query);890 }891 892 if (entries.length > 0) return entries;893 return filterEntries(searchableEntries, "").slice(0, 8);894 }895896 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {897 const icon = document.createElement("div");898 icon.className = `entry-icon${isNew ? " is-new" : ""}`;899 if (isNew) {900 icon.innerHTML = plusSvgStr();901 } else {902 903 904 905 void url;906 icon.innerHTML = keySvgStr();907 }908 return icon;909 }910911 function renderIdentityHelper() {912 container.innerHTML = "";913 const identities = options.savedIdentities ?? [];914915 916 const hdr = container.appendChild(document.createElement("div"));917 hdr.className = "domain-header";918 const eyebrow = hdr.appendChild(document.createElement("div"));919 eyebrow.className = "domain-eyebrow";920 eyebrow.textContent = "NEW ACCOUNT";921 const domName = hdr.appendChild(document.createElement("div"));922 domName.className = "domain-name";923 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");924925 926 const list = container.appendChild(document.createElement("div"));927 list.className = "identity-list";928 for (const id of identities) {929 const row = list.appendChild(document.createElement("div"));930 row.className = "peach-identity-row";931 row.setAttribute("tabindex", "-1");932 const val = row.appendChild(document.createElement("div"));933 val.className = "peach-identity-value";934 val.textContent = id.value;935 const meta = row.appendChild(document.createElement("div"));936 meta.className = "peach-identity-meta";937 meta.textContent = `used ${id.useCount}×`;938 row.addEventListener("click", () => {939 options.onFillFields?.(id.value, "");940 renderGenerator(id.value);941 });942 }943944 945 if (identities.length > 0) {946 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));947 }948949 950 const altRow = container.appendChild(document.createElement("div"));951 altRow.className = "identity-alt-row";952 altRow.textContent = "+ Use a different email…";953954 const altInputWrap = container.appendChild(document.createElement("div"));955 altInputWrap.className = "identity-alt-input-wrap";956 const altInput = altInputWrap.appendChild(document.createElement("input"));957 altInput.className = "identity-alt-input";958 altInput.type = "email";959 altInput.placeholder = "[email protected]";960 altInput.setAttribute("autocomplete", "off");961 const altConfirm = altInputWrap.appendChild(document.createElement("button"));962 altConfirm.className = "identity-alt-confirm";963 altConfirm.type = "button";964 altConfirm.textContent = "✓";965966 function fillCustomEmail() {967 const v = altInput.value.trim();968 if (!v) return;969 options.onFillFields?.(v, "");970 renderGenerator(v);971 }972973 altRow.addEventListener("click", () => {974 altRow.style.display = "none";975 altInputWrap.classList.add("open");976 altInput.focus();977 });978 altConfirm.addEventListener("click", fillCustomEmail);979 altInput.addEventListener("keydown", (e) => {980 if (e.key === "Enter") { e.preventDefault(); fillCustomEmail(); }981 });982 }983984 function renderGenerator(filledEmail: string) {985 container.innerHTML = "";986 const sitePolicy = detectPasswordPolicy();987 let pw = generatePasswordForSite(sitePolicy);988989 const wrap = container.appendChild(document.createElement("div"));990 wrap.className = "gen-view";991992 993 const hdrRow = wrap.appendChild(document.createElement("div"));994 hdrRow.className = "domain-header";995 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));996 eyebrow2.className = "domain-eyebrow";997 eyebrow2.textContent = "NEW ACCOUNT";998 const domName2 = hdrRow.appendChild(document.createElement("div"));999 domName2.className = "domain-name";1000 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");10011002 1003 const conf = wrap.appendChild(document.createElement("div"));1004 conf.className = "gen-confirmed";1005 const confCheck = conf.appendChild(document.createElement("span"));1006 confCheck.className = "gen-confirmed-check";1007 confCheck.textContent = "✓";1008 const confVal = conf.appendChild(document.createElement("div"));1009 confVal.className = "gen-confirmed-val";1010 confVal.textContent = filledEmail;10111012 1013 const lbl = wrap.appendChild(document.createElement("div"));1014 lbl.className = "gen-label";1015 lbl.textContent = "Generate a password";10161017 1018 const pwRow = wrap.appendChild(document.createElement("div"));1019 pwRow.className = "gen-pw-row";1020 const pwVal = pwRow.appendChild(document.createElement("div"));1021 pwVal.className = "gen-pw-val";1022 pwVal.textContent = pw;1023 const regenBtn = pwRow.appendChild(document.createElement("button"));1024 regenBtn.className = "gen-regen-btn";1025 regenBtn.type = "button";1026 regenBtn.textContent = "⟳ regen";1027 regenBtn.addEventListener("click", () => {1028 pw = generatePasswordForSite(sitePolicy);1029 pwVal.textContent = pw;1030 });10311032 1033 const fillBtn = wrap.appendChild(document.createElement("button"));1034 fillBtn.className = "gen-fill-btn";1035 fillBtn.type = "button";1036 fillBtn.textContent = "Copy & fill";1037 fillBtn.addEventListener("click", () => {1038 options.onFillFields?.("", pw);1039 renderSaveOffer(filledEmail, pw);1040 });1041 }10421043 function renderSaveOffer(filledEmail: string, filledPassword: string) {1044 container.innerHTML = "";1045 const domain = options.domain ?? location.hostname.replace(/^www\./, "");10461047 const wrap = container.appendChild(document.createElement("div"));1048 wrap.className = "save-offer";10491050 const title = wrap.appendChild(document.createElement("div"));1051 title.className = "save-offer-title";1052 title.textContent = "Save this login to Peach?";10531054 function detailRow(label: string, value: string) {1055 const row = wrap.appendChild(document.createElement("div"));1056 row.className = "save-offer-row";1057 const lbl = row.appendChild(document.createElement("div"));1058 lbl.className = "save-offer-lbl";1059 lbl.textContent = label;1060 const val = row.appendChild(document.createElement("div"));1061 val.className = "save-offer-val";1062 val.textContent = value;1063 }10641065 detailRow("site", domain);1066 detailRow("email", filledEmail);1067 detailRow("password", "••••••••••••••••");10681069 const btns = wrap.appendChild(document.createElement("div"));1070 btns.className = "save-offer-btns";10711072 const saveBtn = btns.appendChild(document.createElement("button"));1073 saveBtn.className = "save-offer-save";1074 saveBtn.type = "button";1075 saveBtn.textContent = "Save";1076 saveBtn.addEventListener("click", async () => {1077 saveBtn.disabled = true;1078 saveBtn.textContent = "Saving…";1079 try {1080 const res = await chrome.runtime.sendMessage({1081 type: "ADD_ENTRY",1082 payload: { title: sTitle, url: sOrigin, username: filledEmail, password: filledPassword, type: "login" },1083 });1084 if ((res as { ok?: boolean }).ok) {1085 showSuccess(sTitle);1086 } else {1087 saveBtn.disabled = false;1088 saveBtn.textContent = "Save";1089 }1090 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }1091 });10921093 const skipBtn = btns.appendChild(document.createElement("button"));1094 skipBtn.className = "save-offer-skip";1095 skipBtn.type = "button";1096 skipBtn.textContent = "Not now";1097 skipBtn.addEventListener("click", () => { dismiss(true); });1098 }10991100 function renderList() {1101 container.innerHTML = "";1102 focusedIndex = -1;1103 searchInput = null;11041105 const current = activeEntries();11061107 1108 const domHdr = document.createElement("div");1109 domHdr.className = "domain-header";1110 const eyebrow = document.createElement("div");1111 eyebrow.className = "domain-eyebrow";1112 eyebrow.textContent = options.isSignup1113 ? "Sign up as"1114 : entries.length > 0 ? "Saved for this site" : "No saved login";1115 const domName = document.createElement("div");1116 domName.className = "domain-name";1117 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1118 domHdr.append(eyebrow, domName);1119 container.appendChild(domHdr);11201121 1122 const sw = document.createElement("div"); sw.className = "search-wrap";1123 searchInput = document.createElement("input");1124 searchInput.className = "search";1125 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";1126 searchInput.value = query;1127 searchInput.addEventListener("input", () => {1128 query = searchInput?.value ?? "";1129 renderList();1130 searchInput?.focus();1131 searchInput?.setSelectionRange(query.length, query.length);1132 });1133 sw.appendChild(searchInput);1134 container.appendChild(sw);11351136 1137 const results = document.createElement("div");1138 results.className = "results";1139 container.appendChild(results);11401141 if (current.length === 0 && entries.length > 0) {1142 1143 }11441145 for (let i = 0; i < current.length; i++) {1146 const e = current[i]!;1147 const entryEl = document.createElement("div");1148 entryEl.className = "entry";1149 entryEl.dataset.id = e.id;1150 entryEl.dataset.index = String(i);1151 entryEl.setAttribute("tabindex", "-1");11521153 entryEl.appendChild(makeEntryIcon(e.url));11541155 const info = document.createElement("div"); info.className = "entry-info";1156 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;1157 const subEl = document.createElement("div"); subEl.className = "entry-sub";1158 const domain = e.url ? getDomainFromUrl(e.url) : "";1159 subEl.textContent = domain || e.username || "";1160 info.append(titleEl, subEl);1161 entryEl.appendChild(info);11621163 if (e.hasTotp) {1164 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";1165 entryEl.appendChild(badge);1166 }1167 if (e.hasPasskey) {1168 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";1169 entryEl.appendChild(badge);1170 }11711172 entryEl.addEventListener("click", () => {1173 if (options.isSignup && options.onFillFields) {1174 1175 options.onFillFields(e.username ?? "", "");1176 dismiss();1177 } else {1178 onSelect(e.id);1179 dismiss();1180 }1181 });1182 results.appendChild(entryEl);1183 }11841185 1186 if (current.length > 0) {1187 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1188 }11891190 1191 const newEl = document.createElement("div");1192 newEl.className = "entry";1193 newEl.setAttribute("tabindex", "-1");1194 newEl.appendChild(makeEntryIcon(undefined, true));11951196 const newInfo = document.createElement("div"); newInfo.className = "entry-info";1197 const newTitle = document.createElement("div");1198 newTitle.className = "entry-title is-new";1199 newTitle.textContent = `New entry for ${sTitle}`;1200 const newSub = document.createElement("div");1201 newSub.className = "entry-sub";1202 newSub.textContent = sOrigin;1203 newInfo.append(newTitle, newSub);1204 newEl.appendChild(newInfo);1205 newEl.addEventListener("click", showCreateForm);1206 results.appendChild(newEl);12071208 1209 if (searchInput) {1210 requestAnimationFrame(() => searchInput?.focus());1211 }1212 }12131214 function setFocused(idx: number) {1215 const all = container.querySelectorAll<HTMLElement>(".entry");1216 all.forEach((el, i) => el.classList.toggle("focused", i === idx));1217 focusedIndex = idx;1218 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });1219 }12201221 async function showCreateForm() {1222 currentMode = "create";1223 container.innerHTML = "";12241225 1226 1227 1228 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;1229 if (!statusRes?.unlocked) {1230 renderLocked();1231 return;1232 }12331234 1235 1236 const sitePolicy = detectPasswordPolicy();12371238 const wrap = document.createElement("div");1239 wrap.className = "create-form-wrap";12401241 1242 const head = document.createElement("div"); head.className = "create-head";1243 const backBtn = document.createElement("button"); backBtn.className = "back-btn";1244 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>`;1245 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });1246 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";1247 head.append(backBtn, lbl);12481249 1250 const body = document.createElement("div"); body.className = "create-body";12511252 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {1253 const g = document.createElement("div"); g.className = "f-group";1254 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;1255 const inp = document.createElement("input"); inp.className = "f-input";1256 g.append(l, inp); return { g, inp };1257 }12581259 1260 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";1261 body.appendChild(tg.g);12621263 1264 const ug = fGroup("website");1265 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";1266 if (loginSubdomain) {1267 const h = document.createElement("div"); h.className = "url-hint";1268 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);1269 }1270 body.appendChild(ug.g);12711272 1273 const xg = fGroup("username / email");1274 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";1275 xg.inp.setAttribute("autocomplete", "off");1276 body.appendChild(xg.g);12771278 1279 const pg = fGroup("password");1280 const pwInp = document.createElement("input"); pwInp.className = "f-input";1281 pwInp.type = "text"; pwInp.value = prePassword || generatePasswordForSite(sitePolicy);1282 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';1283 pwInp.style.letterSpacing = "0.04em";1284 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");1285 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";1286 genBtn.innerHTML = genSvgStr() + " regen";1287 genBtn.addEventListener("click", () => { pwInp.value = generatePasswordForSite(sitePolicy); });1288 const pwRow = document.createElement("div"); pwRow.className = "f-row";1289 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);1290 body.appendChild(pg.g);12911292 1293 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";1294 saveBtn.textContent = "Save entry";1295 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";1296 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }1297 saveBtn.addEventListener("click", async () => {1298 const title = tg.inp.value.trim();1299 const pw = pwInp.value.trim();1300 const username = xg.inp.value.trim();1301 if (!title) {1302 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();1303 showFormError("Entry needs a title");1304 return;1305 }1306 if (!pw) {1307 pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus();1308 showFormError("Entry needs a password");1309 return;1310 }1311 errEl.style.display = "none";1312 saveBtn.disabled = true; saveBtn.textContent = "Saving…";1313 try {1314 const res = await chrome.runtime.sendMessage({1315 type: "ADD_ENTRY",1316 payload: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" },1317 });1318 if ((res as { ok?: boolean }).ok) {1319 1320 1321 1322 1323 1324 1325 options.onFillFields?.(username, pw);1326 showSuccess(title);1327 } else {1328 const errMsg = (res as { error?: string }).error === "vault locked"1329 ? "Vault locked — open Peach to save"1330 : "Could not save entry";1331 showFormError(errMsg);1332 saveBtn.disabled = false; saveBtn.textContent = "Save entry";1333 }1334 } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }1335 });1336 body.appendChild(saveBtn);1337 body.appendChild(errEl);13381339 wrap.append(head, body);1340 container.appendChild(wrap);1341 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });1342 }13431344 function showSuccess(title: string) {1345 currentMode = "success";1346 container.innerHTML = "";1347 const wrap = document.createElement("div"); wrap.className = "success-wrap";1348 const ring = document.createElement("div"); ring.className = "success-ring";1349 ring.innerHTML = checkSvgStr();1350 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;1351 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;1352 wrap.append(ring, t, s);1353 container.appendChild(wrap);1354 setTimeout(() => dismiss(), 1600);1355 }13561357 function flashActionBtn(btn: HTMLButtonElement) {1358 btn.classList.add("flashed");1359 setTimeout(() => btn.classList.remove("flashed"), 700);1360 }13611362 function renderSilverPlatter(entry: SelectorEntry) {1363 container.innerHTML = "";1364 searchInput = null;1365 focusedIndex = -1;1366 currentMode = "silver-platter";13671368 const domHdr = document.createElement("div");1369 domHdr.className = "domain-header";1370 const eyebrow = document.createElement("div");1371 eyebrow.className = "domain-eyebrow";1372 eyebrow.textContent = "Saved for this site";1373 const domName = document.createElement("div");1374 domName.className = "domain-name";1375 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1376 domHdr.append(eyebrow, domName);1377 container.appendChild(domHdr);13781379 const platter = document.createElement("div");1380 platter.className = "silver-platter";13811382 const titleEl = document.createElement("div");1383 titleEl.className = "sp-title";1384 titleEl.textContent = entry.title;1385 platter.appendChild(titleEl);13861387 if (entry.username) {1388 const userEl = document.createElement("div");1389 userEl.className = "sp-username";1390 userEl.textContent = entry.username;1391 platter.appendChild(userEl);1392 }13931394 const fillBtn = document.createElement("button");1395 fillBtn.className = "sp-fill-btn";1396 fillBtn.type = "button";1397 fillBtn.textContent = "Fill credentials";1398 fillBtn.addEventListener("click", () => {1399 if (fillBtn.disabled) return;1400 fillBtn.disabled = true;1401 fillBtn.classList.add("filled");1402 fillBtn.innerHTML = checkSvgStr();1403 1404 1405 1406 1407 onSelect(entry.id);1408 setTimeout(() => dismiss(), 400);1409 });1410 platter.appendChild(fillBtn);14111412 const actions = document.createElement("div");1413 actions.className = "sp-actions";14141415 const copyU = document.createElement("button");1416 copyU.className = "sp-action-btn";1417 copyU.type = "button";1418 copyU.title = "Copy username";1419 copyU.textContent = "Copy username";1420 copyU.disabled = !entry.username;1421 copyU.addEventListener("click", async (e) => {1422 e.stopPropagation();1423 if (!entry.username) return;1424 let alreadyWritten = false;1425 try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1426 1427 1428 1429 await chrome.runtime.sendMessage({1430 type: "COPY",1431 payload: { text: entry.username, label: "Username", alreadyWritten },1432 }).catch(() => {});1433 flashActionBtn(copyU);1434 });1435 actions.appendChild(copyU);14361437 const copyP = document.createElement("button");1438 copyP.className = "sp-action-btn";1439 copyP.type = "button";1440 copyP.title = "Copy password";1441 copyP.textContent = "Copy password";1442 copyP.addEventListener("click", async (e) => {1443 e.stopPropagation();1444 const resp = await chrome.runtime.sendMessage({1445 type: "GET_ENTRY_FOR_FILL",1446 payload: { id: entry.id },1447 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;1448 if (!resp?.ok || !resp.data?.password) return;1449 let alreadyWritten = false;1450 try { await navigator.clipboard.writeText(resp.data.password); alreadyWritten = true; } catch {}1451 await chrome.runtime.sendMessage({1452 type: "COPY",1453 payload: { text: resp.data.password, label: "Password", alreadyWritten },1454 }).catch(() => {});1455 flashActionBtn(copyP);1456 });1457 actions.appendChild(copyP);14581459 let trayEl: HTMLDivElement | null = null;1460 let trayInterval: ReturnType<typeof setInterval> | null = null;14611462 function closeTray() {1463 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }1464 trayEl?.remove();1465 trayEl = null;1466 }1467 1468 1469 cleanupHooks.push(closeTray);14701471 async function openTOTPTray() {1472 1473 1474 1475 const resp = await chrome.runtime.sendMessage({1476 type: "GET_ENTRY_FOR_FILL",1477 payload: { id: entry.id },1478 }).catch(() => null) as { ok: boolean; data?: { totp?: string } } | null;1479 const secret = resp?.ok ? resp.data?.totp : undefined;1480 if (!secret) return;14811482 trayEl = document.createElement("div");1483 trayEl.className = "sp-tray";14841485 const label = document.createElement("span");1486 label.className = "sp-tray-label";1487 label.textContent = "2FA";1488 trayEl.appendChild(label);14891490 const codeEl = document.createElement("div");1491 codeEl.className = "sp-totp-code";1492 trayEl.appendChild(codeEl);14931494 const ringNs = "http://www.w3.org/2000/svg";1495 const ring = document.createElementNS(ringNs, "svg");1496 ring.setAttribute("class", "sp-totp-ring");1497 ring.setAttribute("viewBox", "0 0 24 24");1498 const trackC = document.createElementNS(ringNs, "circle");1499 trackC.setAttribute("class", "ring-track");1500 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");1501 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");1502 const fillC = document.createElementNS(ringNs, "circle");1503 fillC.setAttribute("class", "ring-fill");1504 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");1505 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");1506 fillC.setAttribute("stroke-linecap", "round");1507 fillC.setAttribute("stroke-dasharray", "62.83");1508 ring.append(trackC, fillC);1509 trayEl.appendChild(ring);15101511 const copyBtn = document.createElement("button");1512 copyBtn.className = "sp-totp-copy";1513 copyBtn.type = "button";1514 copyBtn.textContent = "Copy";1515 trayEl.appendChild(copyBtn);15161517 function refresh() {1518 const { code, remaining } = generateTOTP(secret!);1519 1520 codeEl.textContent = code.length === 61521 ? `${code.slice(0, 3)} ${code.slice(3)}`1522 : code;1523 1524 1525 const fraction = (30 - remaining) / 30;1526 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));1527 }1528 refresh();1529 trayInterval = setInterval(refresh, 1000);15301531 copyBtn.addEventListener("click", async (e) => {1532 e.stopPropagation();1533 const { code } = generateTOTP(secret!);1534 let alreadyWritten = false;1535 try { await navigator.clipboard.writeText(code); alreadyWritten = true; } catch {}1536 await chrome.runtime.sendMessage({1537 type: "COPY",1538 payload: { text: code, label: "TOTP", alreadyWritten },1539 }).catch(() => {});1540 copyBtn.textContent = "Copied";1541 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);1542 });15431544 platter.appendChild(trayEl);1545 }15461547 if (entry.hasTotp) {1548 const moreBtn = document.createElement("button");1549 moreBtn.className = "sp-action-btn";1550 moreBtn.type = "button";1551 moreBtn.title = "Show 2FA code";1552 moreBtn.textContent = "···";1553 moreBtn.addEventListener("click", (e) => {1554 e.stopPropagation();1555 if (trayEl) {1556 closeTray();1557 moreBtn.classList.remove("flashed");1558 } else {1559 moreBtn.classList.add("flashed");1560 void openTOTPTray();1561 }1562 });1563 actions.appendChild(moreBtn);1564 }15651566 const closeBtn = document.createElement("button");1567 closeBtn.className = "sp-action-btn";1568 closeBtn.type = "button";1569 closeBtn.title = "Dismiss";1570 closeBtn.textContent = "✕";1571 closeBtn.addEventListener("click", (e) => {1572 e.stopPropagation();1573 closeTray();1574 dismiss(true);1575 });1576 actions.appendChild(closeBtn);15771578 platter.appendChild(actions);1579 container.appendChild(platter);1580 }15811582 function renderLocked() {1583 container.innerHTML = "";15841585 1586 const domHdr = document.createElement("div");1587 domHdr.className = "domain-header";1588 const eyebrow = document.createElement("div");1589 eyebrow.className = "domain-eyebrow";1590 eyebrow.textContent = "vault locked";1591 const domName = document.createElement("div");1592 domName.className = "domain-name";1593 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1594 domHdr.append(eyebrow, domName);1595 container.appendChild(domHdr);15961597 const wrap = document.createElement("div");1598 wrap.className = "locked-wrap";15991600 1601 const head = document.createElement("div"); head.className = "locked-head";1602 const icon = document.createElement("div"); icon.className = "locked-icon";1603 icon.innerHTML = keySvgStr();1604 const title = document.createElement("div"); title.className = "locked-title";1605 title.textContent = "unlock to autofill";1606 head.append(icon, title);16071608 1609 const pwInp = document.createElement("input");1610 pwInp.className = "locked-input";1611 pwInp.type = "password";1612 pwInp.placeholder = "master password";1613 pwInp.setAttribute("autocomplete", "current-password");16141615 1616 const errEl = document.createElement("div");1617 errEl.className = "locked-error";1618 errEl.style.display = "none";16191620 1621 const unlockBtn = document.createElement("button");1622 unlockBtn.className = "locked-btn";1623 unlockBtn.type = "button";1624 unlockBtn.textContent = "unlock";16251626 async function tryUnlock() {1627 const pw = pwInp.value.trim();1628 if (!pw) { pwInp.focus(); return; }1629 unlockBtn.disabled = true;1630 unlockBtn.textContent = "unlocking…";1631 errEl.style.display = "none";1632 try {1633 const res = await chrome.runtime.sendMessage({1634 type: "UNLOCK",1635 payload: { password: pw },1636 });1637 if ((res as { ok?: boolean }).ok) {1638 dismiss();1639 options.onUnlocked?.();1640 } else {1641 errEl.textContent = "incorrect password";1642 errEl.style.display = "block";1643 pwInp.value = "";1644 pwInp.focus();1645 unlockBtn.disabled = false;1646 unlockBtn.textContent = "unlock";1647 }1648 } catch {1649 errEl.textContent = "could not reach Peach";1650 errEl.style.display = "block";1651 unlockBtn.disabled = false;1652 unlockBtn.textContent = "unlock";1653 }1654 }16551656 pwInp.addEventListener("keydown", (e) => {1657 e.stopPropagation();1658 if (e.key === "Enter") void tryUnlock();1659 });1660 pwInp.addEventListener("keyup", (e) => e.stopPropagation());1661 pwInp.addEventListener("keypress", (e) => e.stopPropagation());1662 unlockBtn.addEventListener("click", () => void tryUnlock());16631664 wrap.append(head, pwInp, errEl, unlockBtn);1665 container.appendChild(wrap);16661667 1668 1669 1670 1671 1672 1673 const captureKeydown = (e: KeyboardEvent) => {1674 if (!host.isConnected) {1675 document.removeEventListener("keydown", captureKeydown, true);1676 return;1677 }1678 1679 if (e.ctrlKey || e.metaKey || e.altKey || e.key === "Tab") return;1680 1681 e.stopImmediatePropagation();1682 if (e.key === "Escape") { dismiss(true); return; }1683 if (e.key === "Enter") { void tryUnlock(); return; }1684 1685 1686 if (document.activeElement !== host) {1687 pwInp.focus({ preventScroll: true });1688 }1689 };1690 captureKeydownRef = captureKeydown;1691 document.addEventListener("keydown", captureKeydown, true);16921693 1694 1695 pwInp.focus({ preventScroll: true });1696 requestAnimationFrame(() => { if (host.isConnected) pwInp.focus({ preventScroll: true }); });1697 }16981699 function handleKeyDown(e: KeyboardEvent) {1700 if (options.locked) {1701 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1702 return;1703 }1704 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter") {1705 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1706 return;1707 }1708 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");1709 const count = allEntryEls.length;1710 if (document.activeElement === searchInput) {1711 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }1712 if (count === 0) return;1713 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;1714 }1715 if (count === 0) return;1716 switch (e.key) {1717 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;1718 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;1719 case "Enter": {1720 if (focusedIndex >= 0) {1721 const el = allEntryEls[focusedIndex] as HTMLElement;1722 const id = el.dataset.id;1723 if (id) { onSelect(id); dismiss(); }1724 else if (el.classList.contains("is-new") || !id) {1725 1726 showCreateForm();1727 }1728 }1729 break;1730 }1731 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;1732 }1733 }17341735 document.addEventListener("keydown", handleKeyDown);17361737 document.body.appendChild(host);17381739 function updatePosition() {1740 const r = anchorEl.getBoundingClientRect();1741 host.style.top = `${r.bottom + window.scrollY + 5}px`;1742 host.style.left = `${r.left + window.scrollX}px`;1743 host.style.width = `${Math.max(r.width, 260)}px`;1744 }1745 1746 1747 1748 1749 1750 1751 1752 let captureKeydownRef: ((e: KeyboardEvent) => void) | null = null;1753 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);17541755 if (options.locked) {1756 renderLocked();1757 } else if (options.isSignup && options.savedIdentities) {1758 1759 1760 renderIdentityHelper();1761 } else if (entries.length === 1 && !options.isSignup) {1762 1763 1764 renderSilverPlatter(entries[0]!);1765 } else {1766 renderList();1767 }17681769 let exitScheduled = false;1770 function dismiss(userAction = false) {1771 1772 1773 1774 if (exitScheduled) return;1775 exitScheduled = true;1776 1777 1778 1779 stopObserving();1780 document.removeEventListener("click", outsideClick, { capture: true });1781 document.removeEventListener("keydown", handleKeyDown);1782 if (captureKeydownRef) {1783 document.removeEventListener("keydown", captureKeydownRef, true);1784 captureKeydownRef = null;1785 }1786 for (const cleanup of cleanupHooks) {1787 try { cleanup(); } catch { }1788 }1789 if (userAction) options.onDismiss?.();1790 container.classList.add("is-exiting");1791 setTimeout(() => host.remove(), 130);1792 }17931794 function outsideClick(ev: MouseEvent) {1795 const path = ev.composedPath();1796 1797 1798 1799 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);1800 }1801 1802 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);18031804 return dismiss;1805}18061807export interface CardSelectorEntry {1808 id: string;1809 title: string;1810 cardholderName?: string;1811 last4?: string;1812 expMonth?: string;1813 expYear?: string;1814 network?: import("../shared/types").CardNetwork;1815}18161817interface CardSelectorOptions {1818 domain?: string;1819 1820 onFill: (entry: import("../shared/types").VaultEntry) => void;1821 1822 onDismiss?: () => void;1823}18241825182618271828182918301831export function showPasskeyProGate(): void {1832 const domain = location.hostname.replace(/^www\./, "");18331834 const host = document.createElement("div");1835 host.setAttribute("data-peach-passkey-gate", "1");1836 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";1837 const shadow = host.attachShadow({ mode: "closed" });18381839 const style = document.createElement("style");1840 style.textContent = `1841 :host { all: initial; }1842 .gate-wrap {1843 position: fixed;1844 top: 50%;1845 left: 50%;1846 transform: translate(-50%, -50%);1847 width: 280px;1848 pointer-events: auto;1849 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;1850 }1851 @keyframes g-in {1852 from { opacity:0; transform: translate(-50%,-52%); }1853 to { opacity:1; transform: translate(-50%,-50%); }1854 }1855 .gate-wrap.is-exiting {1856 animation: g-out 130ms ease-in forwards;1857 }1858 @keyframes g-out {1859 to { opacity:0; transform: translate(-50%,-52%); }1860 }1861 .gate {1862 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};1863 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};1864 border-radius: 12px;1865 box-shadow: 0 12px 48px rgba(0,0,0,0.75);1866 padding: 16px;1867 font-family: Inter, sans-serif;1868 }1869 .gate-header {1870 display: flex;1871 align-items: center;1872 gap: 7px;1873 margin-bottom: 12px;1874 padding-bottom: 10px;1875 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};1876 }1877 .gate-domain {1878 font-size: 12px;1879 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};1880 font-family: "IBM Plex Mono", monospace;1881 overflow: hidden;1882 text-overflow: ellipsis;1883 white-space: nowrap;1884 }1885 .gate-title {1886 font-size: 14px;1887 font-weight: 500;1888 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};1889 margin-bottom: 6px;1890 }1891 .gate-subtitle {1892 font-size: 12px;1893 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};1894 line-height: 1.5;1895 margin-bottom: 14px;1896 }1897 .gate-btn {1898 display: block;1899 width: 100%;1900 padding: 9px 0;1901 border-radius: 8px;1902 border: none;1903 cursor: pointer;1904 font-size: 13px;1905 font-family: Inter, sans-serif;1906 font-weight: 500;1907 transition: opacity 100ms;1908 margin-bottom: 7px;1909 text-align: center;1910 }1911 .gate-btn:last-child { margin-bottom: 0; }1912 .gate-btn--accent {1913 background: ${accentHex};1914 color: #fff;1915 }1916 .gate-btn--accent:hover { opacity: 0.88; }1917 .gate-btn--ghost {1918 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};1919 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};1920 }1921 .gate-btn--ghost:hover { opacity: 0.75; }1922 .backdrop {1923 position: fixed;1924 inset: 0;1925 pointer-events: auto;1926 }1927 `;1928 shadow.appendChild(style);19291930 const backdrop = document.createElement("div");1931 backdrop.className = "backdrop";1932 shadow.appendChild(backdrop);19331934 const wrap = document.createElement("div");1935 wrap.className = "gate-wrap";1936 shadow.appendChild(wrap);19371938 const gate = document.createElement("div");1939 gate.className = "gate";1940 wrap.appendChild(gate);19411942 1943 const header = document.createElement("div");1944 header.className = "gate-header";1945 const icon = document.createElement("span");1946 icon.innerHTML = peachIconSvg();1947 const domainEl = document.createElement("span");1948 domainEl.className = "gate-domain";1949 domainEl.textContent = domain;1950 header.append(icon, domainEl);1951 gate.appendChild(header);19521953 const title = document.createElement("div");1954 title.className = "gate-title";1955 title.textContent = "Passkeys are part of Peach Pro.";1956 gate.appendChild(title);19571958 const subtitle = document.createElement("div");1959 subtitle.className = "gate-subtitle";1960 subtitle.textContent = "Store and use passkeys across all your devices, securely.";1961 gate.appendChild(subtitle);19621963 const upgradeBtn = document.createElement("button");1964 upgradeBtn.className = "gate-btn gate-btn--accent";1965 upgradeBtn.type = "button";1966 upgradeBtn.textContent = "Upgrade to Pro";1967 gate.appendChild(upgradeBtn);19681969 const laterBtn = document.createElement("button");1970 laterBtn.className = "gate-btn gate-btn--ghost";1971 laterBtn.type = "button";1972 laterBtn.textContent = "Not right now";1973 gate.appendChild(laterBtn);19741975 function dismiss() {1976 wrap.classList.add("is-exiting");1977 setTimeout(() => host.remove(), 130);1978 }19791980 upgradeBtn.addEventListener("click", () => {1981 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});1982 dismiss();1983 });1984 laterBtn.addEventListener("click", dismiss);1985 backdrop.addEventListener("click", dismiss);19861987 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {1988 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }1989 });19901991 document.body.appendChild(host);1992}199319941995199619971998199920002001export function showPasskeyRegistrationOffer(args: {2002 rpId: string;2003 rpName?: string;2004 userName?: string;2005 userDisplayName?: string;2006}): void {2007 const domain = location.hostname.replace(/^www\./, "");2008 const displayName = args.rpName || domain;20092010 const host = document.createElement("div");2011 host.setAttribute("data-peach-passkey-offer", "1");2012 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";2013 const shadow = host.attachShadow({ mode: "closed" });20142015 const style = document.createElement("style");2016 style.textContent = `2017 :host { all: initial; }2018 .offer-wrap {2019 position: fixed;2020 top: 16px;2021 right: 16px;2022 width: 320px;2023 pointer-events: auto;2024 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;2025 }2026 @keyframes o-in {2027 from { opacity: 0; transform: translateY(-6px); }2028 to { opacity: 1; transform: translateY(0); }2029 }2030 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }2031 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }2032 .offer {2033 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2034 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2035 border-radius: 12px;2036 box-shadow: 0 12px 48px rgba(0,0,0,0.55);2037 padding: 14px 14px 12px;2038 font-family: Inter, sans-serif;2039 position: relative;2040 }2041 .offer-close {2042 position: absolute;2043 top: 8px;2044 right: 8px;2045 width: 18px;2046 height: 18px;2047 border: none;2048 background: transparent;2049 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};2050 cursor: pointer;2051 font-size: 14px;2052 line-height: 1;2053 padding: 0;2054 }2055 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }2056 .offer-header {2057 display: flex;2058 align-items: center;2059 gap: 7px;2060 margin-bottom: 8px;2061 padding-bottom: 8px;2062 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2063 }2064 .offer-domain {2065 font-size: 12px;2066 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2067 font-family: "IBM Plex Mono", monospace;2068 overflow: hidden;2069 text-overflow: ellipsis;2070 white-space: nowrap;2071 }2072 .offer-title {2073 font-size: 13px;2074 font-weight: 500;2075 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2076 margin-bottom: 4px;2077 }2078 .offer-subtitle {2079 font-size: 11px;2080 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};2081 line-height: 1.45;2082 margin-bottom: 12px;2083 }2084 .offer-btn {2085 display: block;2086 width: 100%;2087 padding: 8px 0;2088 border-radius: 7px;2089 border: none;2090 cursor: pointer;2091 font-size: 12px;2092 font-family: Inter, sans-serif;2093 font-weight: 500;2094 transition: opacity 100ms;2095 margin-bottom: 6px;2096 text-align: center;2097 }2098 .offer-btn:last-child { margin-bottom: 0; }2099 .offer-btn--accent { background: ${accentHex}; color: #fff; }2100 .offer-btn--accent:hover { opacity: 0.88; }2101 .offer-btn--ghost {2102 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2103 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};2104 }2105 .offer-btn--ghost:hover { opacity: 0.75; }2106 `;2107 shadow.appendChild(style);21082109 const wrap = document.createElement("div");2110 wrap.className = "offer-wrap";2111 shadow.appendChild(wrap);21122113 const offer = document.createElement("div");2114 offer.className = "offer";2115 wrap.appendChild(offer);21162117 const closeBtn = document.createElement("button");2118 closeBtn.className = "offer-close";2119 closeBtn.type = "button";2120 closeBtn.setAttribute("aria-label", "Dismiss");2121 closeBtn.textContent = "✕";2122 offer.appendChild(closeBtn);21232124 const header = document.createElement("div");2125 header.className = "offer-header";2126 const icon = document.createElement("span");2127 icon.innerHTML = peachIconSvg();2128 const domainEl = document.createElement("span");2129 domainEl.className = "offer-domain";2130 domainEl.textContent = domain;2131 header.append(icon, domainEl);2132 offer.appendChild(header);21332134 const title = document.createElement("div");2135 title.className = "offer-title";2136 title.textContent = "This site supports passkeys.";2137 offer.appendChild(title);21382139 const subtitle = document.createElement("div");2140 subtitle.className = "offer-subtitle";2141 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";2142 offer.appendChild(subtitle);21432144 const saveBtn = document.createElement("button");2145 saveBtn.className = "offer-btn offer-btn--accent";2146 saveBtn.type = "button";2147 saveBtn.textContent = "Save passkey to Peach";2148 offer.appendChild(saveBtn);21492150 const laterBtn = document.createElement("button");2151 laterBtn.className = "offer-btn offer-btn--ghost";2152 laterBtn.type = "button";2153 laterBtn.textContent = "Not now";2154 offer.appendChild(laterBtn);21552156 function dismiss(decline: boolean) {2157 if (decline) {2158 chrome.runtime.sendMessage({2159 type: "PASSKEY_OFFER_DECLINE",2160 payload: { domain },2161 }).catch(() => {});2162 }2163 wrap.classList.add("is-exiting");2164 setTimeout(() => host.remove(), 130);2165 }21662167 saveBtn.addEventListener("click", () => {2168 chrome.runtime.sendMessage({2169 type: "INITIATE_FIREFOX_PASSKEY_CREATE",2170 payload: {2171 rpId: args.rpId,2172 rpName: args.rpName ?? displayName,2173 userName: args.userName ?? "user",2174 userDisplayName: args.userDisplayName,2175 },2176 }).catch(() => {});2177 dismiss(false);2178 });2179 laterBtn.addEventListener("click", () => dismiss(true));2180 closeBtn.addEventListener("click", () => dismiss(true));21812182 document.body.appendChild(host);2183}2184218521862187218821892190219121922193export function showPasskeyAvailable(args: {2194 rpId: string;2195 userName: string;2196 fromBrowser?: "chrome" | "firefox";2197 currentBrowser?: "chrome" | "firefox" | "unknown";2198}): void {2199 const domain = location.hostname.replace(/^www\./, "");2200 const crossBrowser =2201 args.fromBrowser &&2202 args.currentBrowser &&2203 args.currentBrowser !== "unknown" &&2204 args.fromBrowser !== args.currentBrowser;22052206 const host = document.createElement("div");2207 host.setAttribute("data-peach-passkey-available", "1");2208 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";2209 const shadow = host.attachShadow({ mode: "closed" });22102211 const style = document.createElement("style");2212 style.textContent = `2213 :host { all: initial; }2214 .avail-wrap {2215 position: fixed;2216 top: 16px;2217 right: 16px;2218 width: 320px;2219 pointer-events: auto;2220 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;2221 }2222 @keyframes a-in {2223 from { opacity: 0; transform: translateY(-6px); }2224 to { opacity: 1; transform: translateY(0); }2225 }2226 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }2227 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }2228 .avail {2229 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2230 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2231 border-radius: 12px;2232 box-shadow: 0 12px 48px rgba(0,0,0,0.55);2233 padding: 14px 14px 12px;2234 font-family: Inter, sans-serif;2235 position: relative;2236 }2237 .avail-close {2238 position: absolute;2239 top: 8px;2240 right: 8px;2241 width: 18px;2242 height: 18px;2243 border: none;2244 background: transparent;2245 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};2246 cursor: pointer;2247 font-size: 14px;2248 line-height: 1;2249 padding: 0;2250 }2251 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }2252 .avail-header {2253 display: flex;2254 align-items: center;2255 gap: 7px;2256 margin-bottom: 8px;2257 padding-bottom: 8px;2258 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2259 }2260 .avail-domain {2261 font-size: 12px;2262 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2263 font-family: "IBM Plex Mono", monospace;2264 }2265 .avail-label {2266 font-size: 9px;2267 letter-spacing: 0.16em;2268 text-transform: uppercase;2269 color: ${accentHex};2270 margin-bottom: 4px;2271 }2272 .avail-user {2273 font-size: 13px;2274 font-weight: 500;2275 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2276 margin-bottom: 12px;2277 overflow: hidden;2278 text-overflow: ellipsis;2279 white-space: nowrap;2280 }2281 .avail-cross {2282 font-size: 11px;2283 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};2284 line-height: 1.45;2285 margin-bottom: 12px;2286 padding: 8px;2287 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};2288 border-radius: 6px;2289 }2290 .avail-btn {2291 display: block;2292 width: 100%;2293 padding: 8px 0;2294 border-radius: 7px;2295 border: none;2296 cursor: pointer;2297 font-size: 12px;2298 font-family: Inter, sans-serif;2299 font-weight: 500;2300 transition: opacity 100ms;2301 margin-bottom: 6px;2302 text-align: center;2303 }2304 .avail-btn:last-child { margin-bottom: 0; }2305 .avail-btn--accent { background: ${accentHex}; color: #fff; }2306 .avail-btn--accent:hover { opacity: 0.88; }2307 .avail-btn--ghost {2308 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2309 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};2310 }2311 .avail-btn--ghost:hover { opacity: 0.75; }2312 `;2313 shadow.appendChild(style);23142315 const wrap = document.createElement("div");2316 wrap.className = "avail-wrap";2317 shadow.appendChild(wrap);23182319 const avail = document.createElement("div");2320 avail.className = "avail";2321 wrap.appendChild(avail);23222323 const closeBtn = document.createElement("button");2324 closeBtn.className = "avail-close";2325 closeBtn.type = "button";2326 closeBtn.setAttribute("aria-label", "Dismiss");2327 closeBtn.textContent = "✕";2328 avail.appendChild(closeBtn);23292330 const header = document.createElement("div");2331 header.className = "avail-header";2332 const icon = document.createElement("span");2333 icon.innerHTML = peachIconSvg();2334 const domainEl = document.createElement("span");2335 domainEl.className = "avail-domain";2336 domainEl.textContent = domain;2337 header.append(icon, domainEl);2338 avail.appendChild(header);23392340 const label = document.createElement("div");2341 label.className = "avail-label";2342 label.textContent = "PASSKEY";2343 avail.appendChild(label);23442345 const user = document.createElement("div");2346 user.className = "avail-user";2347 user.textContent = args.userName;2348 avail.appendChild(user);23492350 if (crossBrowser) {2351 const cross = document.createElement("div");2352 cross.className = "avail-cross";2353 cross.textContent =2354 args.fromBrowser === "chrome"2355 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."2356 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";2357 avail.appendChild(cross);2358 }23592360 const signInBtn = document.createElement("button");2361 signInBtn.className = "avail-btn avail-btn--accent";2362 signInBtn.type = "button";2363 signInBtn.textContent = "Sign in with passkey";2364 avail.appendChild(signInBtn);23652366 const passwordBtn = document.createElement("button");2367 passwordBtn.className = "avail-btn avail-btn--ghost";2368 passwordBtn.type = "button";2369 passwordBtn.textContent = "Use password instead";2370 avail.appendChild(passwordBtn);23712372 function dismiss() {2373 wrap.classList.add("is-exiting");2374 setTimeout(() => host.remove(), 130);2375 }23762377 signInBtn.addEventListener("click", () => {2378 2379 2380 2381 2382 2383 dismiss();2384 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});2385 });2386 passwordBtn.addEventListener("click", dismiss);2387 closeBtn.addEventListener("click", dismiss);23882389 document.body.appendChild(host);2390}239123922393239423952396239723982399export function showCardSelector(2400 anchorEl: HTMLInputElement,2401 cards: CardSelectorEntry[],2402 options: CardSelectorOptions2403): () => void {2404 if (cards.length === 0) return () => {};2405 const card = cards[0]!;24062407 const host = document.createElement("div");2408 host.style.cssText = "position: absolute; z-index: 2147483647;";2409 host.style.setProperty("--peach-accent", accentHex);2410 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));2411 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));2412 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));2413 const shadow = host.attachShadow({ mode: "closed" });24142415 const style = shadow.appendChild(document.createElement("style"));2416 style.textContent = SELECTOR_STYLES;24172418 const rect = anchorEl.getBoundingClientRect();2419 host.style.top = `${rect.bottom + window.scrollY + 5}px`;2420 host.style.left = `${rect.left + window.scrollX}px`;2421 host.style.width = `${Math.max(rect.width, 320)}px`;24222423 const container = shadow.appendChild(document.createElement("div"));2424 container.className = "peach-inline-popup";2425 container.setAttribute("data-theme", popupTheme);24262427 2428 const domHdr = document.createElement("div");2429 domHdr.className = "domain-header";2430 const eyebrow = document.createElement("div");2431 eyebrow.className = "domain-eyebrow";2432 eyebrow.textContent = "Pay with Peach";2433 const domName = document.createElement("div");2434 domName.className = "domain-name";2435 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2436 domHdr.append(eyebrow, domName);2437 container.appendChild(domHdr);24382439 const platter = document.createElement("div");2440 platter.className = "card-platter";24412442 if (card.network && card.network !== "unknown") {2443 const net = document.createElement("div");2444 net.className = "cp-network";2445 net.textContent = card.network;2446 platter.appendChild(net);2447 }24482449 const nick = document.createElement("div");2450 nick.className = "cp-nickname";2451 nick.textContent = card.title;2452 platter.appendChild(nick);24532454 if (card.last4) {2455 const l4 = document.createElement("div");2456 l4.className = "cp-last4";2457 l4.textContent = `•••• ${card.last4}`;2458 platter.appendChild(l4);2459 }24602461 const meta = document.createElement("div");2462 meta.className = "cp-meta";2463 if (card.cardholderName) {2464 const name = document.createElement("span");2465 name.textContent = card.cardholderName;2466 meta.appendChild(name);2467 }2468 if (card.expMonth && card.expYear) {2469 if (card.cardholderName) {2470 const sep = document.createElement("span");2471 sep.className = "cp-meta-sep";2472 sep.textContent = "·";2473 meta.appendChild(sep);2474 }2475 const exp = document.createElement("span");2476 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;2477 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;2478 meta.appendChild(exp);2479 }2480 if (meta.childElementCount > 0) platter.appendChild(meta);24812482 const fillWrap = document.createElement("div");2483 fillWrap.className = "cp-fill-wrap";2484 const fillBtn = document.createElement("button");2485 fillBtn.className = "sp-fill-btn";2486 fillBtn.type = "button";2487 fillBtn.textContent = "Fill card details";2488 fillBtn.addEventListener("click", async () => {2489 if (fillBtn.disabled) return;2490 fillBtn.disabled = true;2491 fillBtn.classList.add("filled");2492 fillBtn.innerHTML = checkSvgStr();2493 const resp = await chrome.runtime.sendMessage({2494 type: "GET_ENTRY_FOR_FILL",2495 payload: { id: card.id },2496 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;2497 if (resp?.ok && resp.data) options.onFill(resp.data);2498 setTimeout(() => dismiss(), 400);2499 });2500 fillWrap.appendChild(fillBtn);2501 platter.appendChild(fillWrap);25022503 2504 const actions = document.createElement("div");2505 actions.className = "sp-actions";25062507 function flashBtn(b: HTMLButtonElement) {2508 b.classList.add("flashed");2509 setTimeout(() => b.classList.remove("flashed"), 700);2510 }25112512 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {2513 const resp = await chrome.runtime.sendMessage({2514 type: "GET_ENTRY_FOR_FILL",2515 payload: { id: card.id },2516 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;2517 return resp?.ok && resp.data ? resp.data : null;2518 }25192520 const copyNum = document.createElement("button");2521 copyNum.className = "sp-action-btn";2522 copyNum.type = "button";2523 copyNum.title = "Copy card number";2524 copyNum.textContent = "Copy number";2525 copyNum.addEventListener("click", async (e) => {2526 e.stopPropagation();2527 const full = await fetchFullCard();2528 if (!full?.cardNumber) return;2529 let alreadyWritten = false;2530 try { await navigator.clipboard.writeText(full.cardNumber); alreadyWritten = true; } catch {}2531 await chrome.runtime.sendMessage({2532 type: "COPY",2533 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },2534 }).catch(() => {});2535 flashBtn(copyNum);2536 });2537 actions.appendChild(copyNum);25382539 const copyCvc = document.createElement("button");2540 copyCvc.className = "sp-action-btn";2541 copyCvc.type = "button";2542 copyCvc.title = "Copy CVC";2543 copyCvc.textContent = "Copy CVC";2544 copyCvc.addEventListener("click", async (e) => {2545 e.stopPropagation();2546 const full = await fetchFullCard();2547 if (!full?.cvc) return;2548 let alreadyWritten = false;2549 try { await navigator.clipboard.writeText(full.cvc); alreadyWritten = true; } catch {}2550 await chrome.runtime.sendMessage({2551 type: "COPY",2552 payload: { text: full.cvc, label: "CVC", alreadyWritten },2553 }).catch(() => {});2554 flashBtn(copyCvc);2555 });2556 actions.appendChild(copyCvc);25572558 const closeBtn = document.createElement("button");2559 closeBtn.className = "sp-action-btn";2560 closeBtn.type = "button";2561 closeBtn.title = "Dismiss";2562 closeBtn.textContent = "✕";2563 closeBtn.addEventListener("click", (e) => {2564 e.stopPropagation();2565 dismiss(true);2566 });2567 actions.appendChild(closeBtn);25682569 platter.appendChild(actions);2570 container.appendChild(platter);25712572 document.body.appendChild(host);2573 function updatePosition() {2574 const r = anchorEl.getBoundingClientRect();2575 host.style.top = `${r.bottom + window.scrollY + 5}px`;2576 host.style.left = `${r.left + window.scrollX}px`;2577 host.style.width = `${Math.max(r.width, 320)}px`;2578 }2579 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);25802581 function onKeyDown(e: KeyboardEvent) {2582 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }2583 }2584 document.addEventListener("keydown", onKeyDown);25852586 let exitScheduled = false;2587 function dismiss(userAction = false) {2588 if (exitScheduled) return;2589 exitScheduled = true;2590 stopObserving();2591 document.removeEventListener("click", outsideClick, { capture: true });2592 document.removeEventListener("keydown", onKeyDown);2593 if (userAction) options.onDismiss?.();2594 container.classList.add("is-exiting");2595 setTimeout(() => host.remove(), 130);2596 }25972598 function outsideClick(ev: MouseEvent) {2599 const path = ev.composedPath();2600 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);2601 }2602 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);26032604 return dismiss;2605}