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 {721 max-height: 192px; overflow-y: auto; padding: 4px 0;722 scrollbar-width: none;723 }724 .identity-list::-webkit-scrollbar { display: none; }725 .peach-identity-row {726 display: flex; align-items: center; gap: 10px;727 padding: 9px 12px; cursor: pointer; border-radius: 8px;728 transition: background 100ms ease;729 }730 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }731 .peach-identity-value {732 font-family: "IBM Plex Mono", monospace; font-size: 13px;733 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;734 }735 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }736 .identity-alt-row {737 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;738 border-radius: 8px; transition: background 100ms ease;739 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;740 }741 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }742 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }743 .identity-alt-input-wrap.open { display: flex; }744 .identity-alt-input {745 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;746 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);747 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;748 outline: none; transition: border-color 120ms;749 }750 .identity-alt-input:focus {751 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);752 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);753 }754 .identity-alt-confirm {755 height: 32px; padding: 0 12px; border-radius: 7px; border: none;756 background: var(--peach-accent); color: #fff; font-size: 12px;757 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;758 }759 /* ── Generator view (after identity picked) ────────────────── */760 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }761 .gen-confirmed {762 display: flex; align-items: center; gap: 8px; padding: 7px 10px;763 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);764 border-radius: 8px;765 }766 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }767 .gen-confirmed-val {768 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);769 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;770 }771 .gen-label {772 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;773 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;774 }775 .gen-pw-row { display: flex; gap: 6px; align-items: center; }776 .gen-pw-val {777 flex: 1; padding: 8px 10px; border-radius: 7px;778 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);779 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);780 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;781 }782 .gen-regen-btn {783 height: 34px; padding: 0 10px; border-radius: 7px; border: none;784 background: color-mix(in srgb, var(--peach-text) 8%, transparent);785 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;786 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;787 }788 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }789 .gen-fill-btn {790 padding: 10px; border-radius: 8px; border: none;791 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;792 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;793 transition: filter 120ms ease, transform 80ms ease;794 }795 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }796 .gen-fill-btn:active { transform: translateY(0); }797 /* ── Save offer (after password filled) ───────────────────── */798 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }799 .save-offer-title {800 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;801 }802 .save-offer-row {803 display: flex; align-items: center; gap: 8px; padding: 6px 8px;804 border-radius: 7px; background: var(--peach-input-bg);805 border: 1px solid var(--peach-input-border);806 }807 .save-offer-lbl {808 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;809 font-family: "IBM Plex Mono", monospace;810 }811 .save-offer-val {812 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);813 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;814 }815 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }816 .save-offer-save {817 flex: 1; padding: 9px; border-radius: 8px; border: none;818 background: var(--peach-accent); color: #fff; font-size: 13px;819 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;820 }821 .save-offer-save:hover { filter: brightness(1.08); }822 .save-offer-skip {823 flex: 1; padding: 9px; border-radius: 8px; border: none;824 background: color-mix(in srgb, var(--peach-text) 8%, transparent);825 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;826 font-family: Inter, sans-serif; transition: background 100ms ease;827 }828 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }829`;830831export function showAutofillSelector(832 anchorEl: HTMLInputElement,833 entries: SelectorEntry[],834 onSelect: (id: string) => void,835 options: SelectorOptions = {}836): () => void {837 const host = document.createElement("div");838 host.style.cssText = "position: absolute; z-index: 2147483647;";839 host.style.setProperty("--peach-accent", accentHex);840 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));841 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));842 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));843 const shadow = host.attachShadow({ mode: "closed" });844845 const style = shadow.appendChild(document.createElement("style"));846 style.textContent = SELECTOR_STYLES;847848 const rect = anchorEl.getBoundingClientRect();849 host.style.top = `${rect.bottom + window.scrollY + 5}px`;850 host.style.left = `${rect.left + window.scrollX}px`;851 852 853 host.style.width = `${Math.max(rect.width, 320)}px`;854855 const container = shadow.appendChild(document.createElement("div"));856 container.className = "peach-inline-popup";857 container.setAttribute("data-theme", popupTheme);858859 const searchableEntries = options.searchableEntries ?? [];860 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();861862 let query = "";863 let focusedIndex = -1;864 let currentMode: "list" | "create" | "success" | "silver-platter" = "list";865 let searchInput: HTMLInputElement | null = null;866 867 868 const cleanupHooks: Array<() => void> = [];869870 871 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";872 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");873 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";874875 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {876 const seen = new Set<string>();877 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });878 }879880 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {881 const q = value.trim().toLowerCase();882 const deduped = uniqueEntries(source);883 if (!q) return deduped.slice(0, 8);884 return deduped.filter(e =>885 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)886 ).slice(0, 12);887 }888889 function activeEntries(): SelectorEntry[] {890 891 if (query.trim()) {892 const merged = uniqueEntries([...entries, ...searchableEntries]);893 return filterEntries(merged, query);894 }895 896 if (entries.length > 0) return entries;897 return filterEntries(searchableEntries, "").slice(0, 8);898 }899900 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {901 const icon = document.createElement("div");902 icon.className = `entry-icon${isNew ? " is-new" : ""}`;903 if (isNew) {904 icon.innerHTML = plusSvgStr();905 } else {906 907 908 909 void url;910 icon.innerHTML = keySvgStr();911 }912 return icon;913 }914915 function renderIdentityHelper() {916 container.innerHTML = "";917 const identities = options.savedIdentities ?? [];918919 920 const hdr = container.appendChild(document.createElement("div"));921 hdr.className = "domain-header";922 const eyebrow = hdr.appendChild(document.createElement("div"));923 eyebrow.className = "domain-eyebrow";924 eyebrow.textContent = "NEW ACCOUNT";925 const domName = hdr.appendChild(document.createElement("div"));926 domName.className = "domain-name";927 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");928929 930 const list = container.appendChild(document.createElement("div"));931 list.className = "identity-list";932 for (const id of identities) {933 const row = list.appendChild(document.createElement("div"));934 row.className = "peach-identity-row";935 row.setAttribute("tabindex", "-1");936 const val = row.appendChild(document.createElement("div"));937 val.className = "peach-identity-value";938 val.textContent = id.value;939 const meta = row.appendChild(document.createElement("div"));940 meta.className = "peach-identity-meta";941 meta.textContent = `used ${id.useCount}×`;942 row.addEventListener("click", () => {943 options.onFillFields?.(id.value, "");944 renderGenerator(id.value);945 });946 }947948 949 if (identities.length > 0) {950 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));951 }952953 954 const altRow = container.appendChild(document.createElement("div"));955 altRow.className = "identity-alt-row";956 altRow.textContent = "+ Use a different email…";957958 const altInputWrap = container.appendChild(document.createElement("div"));959 altInputWrap.className = "identity-alt-input-wrap";960 const altInput = altInputWrap.appendChild(document.createElement("input"));961 altInput.className = "identity-alt-input";962 altInput.type = "email";963 altInput.placeholder = "[email protected]";964 altInput.setAttribute("autocomplete", "off");965 const altConfirm = altInputWrap.appendChild(document.createElement("button"));966 altConfirm.className = "identity-alt-confirm";967 altConfirm.type = "button";968 altConfirm.textContent = "✓";969970 function fillCustomEmail() {971 const v = altInput.value.trim();972 if (!v) return;973 options.onFillFields?.(v, "");974 renderGenerator(v);975 }976977 altRow.addEventListener("click", () => {978 altRow.style.display = "none";979 altInputWrap.classList.add("open");980 altInput.focus();981 });982 altConfirm.addEventListener("click", fillCustomEmail);983 altInput.addEventListener("keydown", (e) => {984 if (e.key === "Enter") { e.preventDefault(); fillCustomEmail(); }985 });986 }987988 function renderGenerator(filledEmail: string) {989 container.innerHTML = "";990 const sitePolicy = detectPasswordPolicy();991 let pw = generatePasswordForSite(sitePolicy);992993 const wrap = container.appendChild(document.createElement("div"));994 wrap.className = "gen-view";995996 997 const hdrRow = wrap.appendChild(document.createElement("div"));998 hdrRow.className = "domain-header";999 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1000 eyebrow2.className = "domain-eyebrow";1001 eyebrow2.textContent = "NEW ACCOUNT";1002 const domName2 = hdrRow.appendChild(document.createElement("div"));1003 domName2.className = "domain-name";1004 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");10051006 1007 const conf = wrap.appendChild(document.createElement("div"));1008 conf.className = "gen-confirmed";1009 const confCheck = conf.appendChild(document.createElement("span"));1010 confCheck.className = "gen-confirmed-check";1011 confCheck.textContent = "✓";1012 const confVal = conf.appendChild(document.createElement("div"));1013 confVal.className = "gen-confirmed-val";1014 confVal.textContent = filledEmail;10151016 1017 const lbl = wrap.appendChild(document.createElement("div"));1018 lbl.className = "gen-label";1019 lbl.textContent = "Generate a password";10201021 1022 const pwRow = wrap.appendChild(document.createElement("div"));1023 pwRow.className = "gen-pw-row";1024 const pwVal = pwRow.appendChild(document.createElement("div"));1025 pwVal.className = "gen-pw-val";1026 pwVal.textContent = pw;1027 const regenBtn = pwRow.appendChild(document.createElement("button"));1028 regenBtn.className = "gen-regen-btn";1029 regenBtn.type = "button";1030 regenBtn.textContent = "⟳ regen";1031 regenBtn.addEventListener("click", () => {1032 pw = generatePasswordForSite(sitePolicy);1033 pwVal.textContent = pw;1034 });10351036 1037 const fillBtn = wrap.appendChild(document.createElement("button"));1038 fillBtn.className = "gen-fill-btn";1039 fillBtn.type = "button";1040 fillBtn.textContent = "Copy & fill";1041 fillBtn.addEventListener("click", () => {1042 options.onFillFields?.("", pw);1043 renderSaveOffer(filledEmail, pw);1044 });1045 }10461047 function renderSaveOffer(filledEmail: string, filledPassword: string) {1048 container.innerHTML = "";1049 const domain = options.domain ?? location.hostname.replace(/^www\./, "");10501051 const wrap = container.appendChild(document.createElement("div"));1052 wrap.className = "save-offer";10531054 const title = wrap.appendChild(document.createElement("div"));1055 title.className = "save-offer-title";1056 title.textContent = "Save this login to Peach?";10571058 function detailRow(label: string, value: string) {1059 const row = wrap.appendChild(document.createElement("div"));1060 row.className = "save-offer-row";1061 const lbl = row.appendChild(document.createElement("div"));1062 lbl.className = "save-offer-lbl";1063 lbl.textContent = label;1064 const val = row.appendChild(document.createElement("div"));1065 val.className = "save-offer-val";1066 val.textContent = value;1067 }10681069 detailRow("site", domain);1070 detailRow("email", filledEmail);1071 detailRow("password", "••••••••••••••••");10721073 const btns = wrap.appendChild(document.createElement("div"));1074 btns.className = "save-offer-btns";10751076 const saveBtn = btns.appendChild(document.createElement("button"));1077 saveBtn.className = "save-offer-save";1078 saveBtn.type = "button";1079 saveBtn.textContent = "Save";1080 saveBtn.addEventListener("click", async () => {1081 saveBtn.disabled = true;1082 saveBtn.textContent = "Saving…";1083 try {1084 const res = await chrome.runtime.sendMessage({1085 type: "ADD_ENTRY",1086 payload: { title: sTitle, url: sOrigin, username: filledEmail, password: filledPassword, type: "login" },1087 });1088 if ((res as { ok?: boolean }).ok) {1089 showSuccess(sTitle);1090 } else {1091 saveBtn.disabled = false;1092 saveBtn.textContent = "Save";1093 }1094 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }1095 });10961097 const skipBtn = btns.appendChild(document.createElement("button"));1098 skipBtn.className = "save-offer-skip";1099 skipBtn.type = "button";1100 skipBtn.textContent = "Not now";1101 skipBtn.addEventListener("click", () => { dismiss(true); });1102 }11031104 function renderList() {1105 container.innerHTML = "";1106 focusedIndex = -1;1107 searchInput = null;11081109 const current = activeEntries();11101111 1112 const domHdr = document.createElement("div");1113 domHdr.className = "domain-header";1114 const eyebrow = document.createElement("div");1115 eyebrow.className = "domain-eyebrow";1116 eyebrow.textContent = options.isSignup1117 ? "Sign up as"1118 : entries.length > 0 ? "Saved for this site" : "No saved login";1119 const domName = document.createElement("div");1120 domName.className = "domain-name";1121 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1122 domHdr.append(eyebrow, domName);1123 container.appendChild(domHdr);11241125 1126 const sw = document.createElement("div"); sw.className = "search-wrap";1127 searchInput = document.createElement("input");1128 searchInput.className = "search";1129 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";1130 searchInput.value = query;1131 searchInput.addEventListener("input", () => {1132 query = searchInput?.value ?? "";1133 renderList();1134 searchInput?.focus();1135 searchInput?.setSelectionRange(query.length, query.length);1136 });1137 sw.appendChild(searchInput);1138 container.appendChild(sw);11391140 1141 const results = document.createElement("div");1142 results.className = "results";1143 container.appendChild(results);11441145 if (current.length === 0 && entries.length > 0) {1146 1147 }11481149 for (let i = 0; i < current.length; i++) {1150 const e = current[i]!;1151 const entryEl = document.createElement("div");1152 entryEl.className = "entry";1153 entryEl.dataset.id = e.id;1154 entryEl.dataset.index = String(i);1155 entryEl.setAttribute("tabindex", "-1");11561157 entryEl.appendChild(makeEntryIcon(e.url));11581159 const info = document.createElement("div"); info.className = "entry-info";1160 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;1161 const subEl = document.createElement("div"); subEl.className = "entry-sub";1162 const domain = e.url ? getDomainFromUrl(e.url) : "";1163 subEl.textContent = domain || e.username || "";1164 info.append(titleEl, subEl);1165 entryEl.appendChild(info);11661167 if (e.hasTotp) {1168 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";1169 entryEl.appendChild(badge);1170 }1171 if (e.hasPasskey) {1172 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";1173 entryEl.appendChild(badge);1174 }11751176 entryEl.addEventListener("click", () => {1177 if (options.isSignup && options.onFillFields) {1178 1179 options.onFillFields(e.username ?? "", "");1180 dismiss();1181 } else {1182 onSelect(e.id);1183 dismiss();1184 }1185 });1186 results.appendChild(entryEl);1187 }11881189 1190 if (current.length > 0) {1191 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1192 }11931194 1195 const newEl = document.createElement("div");1196 newEl.className = "entry";1197 newEl.setAttribute("tabindex", "-1");1198 newEl.appendChild(makeEntryIcon(undefined, true));11991200 const newInfo = document.createElement("div"); newInfo.className = "entry-info";1201 const newTitle = document.createElement("div");1202 newTitle.className = "entry-title is-new";1203 newTitle.textContent = `New entry for ${sTitle}`;1204 const newSub = document.createElement("div");1205 newSub.className = "entry-sub";1206 newSub.textContent = sOrigin;1207 newInfo.append(newTitle, newSub);1208 newEl.appendChild(newInfo);1209 newEl.addEventListener("click", showCreateForm);1210 results.appendChild(newEl);12111212 1213 if (searchInput) {1214 requestAnimationFrame(() => searchInput?.focus());1215 }1216 }12171218 function setFocused(idx: number) {1219 const all = container.querySelectorAll<HTMLElement>(".entry");1220 all.forEach((el, i) => el.classList.toggle("focused", i === idx));1221 focusedIndex = idx;1222 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });1223 }12241225 async function showCreateForm() {1226 currentMode = "create";1227 container.innerHTML = "";12281229 1230 1231 1232 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;1233 if (!statusRes?.unlocked) {1234 renderLocked();1235 return;1236 }12371238 1239 1240 const sitePolicy = detectPasswordPolicy();12411242 const wrap = document.createElement("div");1243 wrap.className = "create-form-wrap";12441245 1246 const head = document.createElement("div"); head.className = "create-head";1247 const backBtn = document.createElement("button"); backBtn.className = "back-btn";1248 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>`;1249 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });1250 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";1251 head.append(backBtn, lbl);12521253 1254 const body = document.createElement("div"); body.className = "create-body";12551256 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {1257 const g = document.createElement("div"); g.className = "f-group";1258 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;1259 const inp = document.createElement("input"); inp.className = "f-input";1260 g.append(l, inp); return { g, inp };1261 }12621263 1264 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";1265 body.appendChild(tg.g);12661267 1268 const ug = fGroup("website");1269 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";1270 if (loginSubdomain) {1271 const h = document.createElement("div"); h.className = "url-hint";1272 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);1273 }1274 body.appendChild(ug.g);12751276 1277 const xg = fGroup("username / email");1278 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";1279 xg.inp.setAttribute("autocomplete", "off");1280 body.appendChild(xg.g);12811282 1283 const pg = fGroup("password");1284 const pwInp = document.createElement("input"); pwInp.className = "f-input";1285 pwInp.type = "text"; pwInp.value = prePassword || generatePasswordForSite(sitePolicy);1286 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';1287 pwInp.style.letterSpacing = "0.04em";1288 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");1289 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";1290 genBtn.innerHTML = genSvgStr() + " regen";1291 genBtn.addEventListener("click", () => { pwInp.value = generatePasswordForSite(sitePolicy); });1292 const pwRow = document.createElement("div"); pwRow.className = "f-row";1293 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);1294 body.appendChild(pg.g);12951296 1297 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";1298 saveBtn.textContent = "Save entry";1299 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";1300 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }1301 saveBtn.addEventListener("click", async () => {1302 const title = tg.inp.value.trim();1303 const pw = pwInp.value.trim();1304 const username = xg.inp.value.trim();1305 if (!title) {1306 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();1307 showFormError("Entry needs a title");1308 return;1309 }1310 if (!pw) {1311 pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus();1312 showFormError("Entry needs a password");1313 return;1314 }1315 errEl.style.display = "none";1316 saveBtn.disabled = true; saveBtn.textContent = "Saving…";1317 try {1318 const res = await chrome.runtime.sendMessage({1319 type: "ADD_ENTRY",1320 payload: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" },1321 });1322 if ((res as { ok?: boolean }).ok) {1323 1324 1325 1326 1327 1328 1329 options.onFillFields?.(username, pw);1330 showSuccess(title);1331 } else {1332 const errMsg = (res as { error?: string }).error === "vault locked"1333 ? "Vault locked — open Peach to save"1334 : "Could not save entry";1335 showFormError(errMsg);1336 saveBtn.disabled = false; saveBtn.textContent = "Save entry";1337 }1338 } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }1339 });1340 body.appendChild(saveBtn);1341 body.appendChild(errEl);13421343 wrap.append(head, body);1344 container.appendChild(wrap);1345 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });1346 }13471348 function showSuccess(title: string) {1349 currentMode = "success";1350 container.innerHTML = "";1351 const wrap = document.createElement("div"); wrap.className = "success-wrap";1352 const ring = document.createElement("div"); ring.className = "success-ring";1353 ring.innerHTML = checkSvgStr();1354 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;1355 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;1356 wrap.append(ring, t, s);1357 container.appendChild(wrap);1358 setTimeout(() => dismiss(), 1600);1359 }13601361 function flashActionBtn(btn: HTMLButtonElement) {1362 btn.classList.add("flashed");1363 setTimeout(() => btn.classList.remove("flashed"), 700);1364 }13651366 function renderSilverPlatter(entry: SelectorEntry) {1367 container.innerHTML = "";1368 searchInput = null;1369 focusedIndex = -1;1370 currentMode = "silver-platter";13711372 const domHdr = document.createElement("div");1373 domHdr.className = "domain-header";1374 const eyebrow = document.createElement("div");1375 eyebrow.className = "domain-eyebrow";1376 eyebrow.textContent = "Saved for this site";1377 const domName = document.createElement("div");1378 domName.className = "domain-name";1379 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1380 domHdr.append(eyebrow, domName);1381 container.appendChild(domHdr);13821383 const platter = document.createElement("div");1384 platter.className = "silver-platter";13851386 const titleEl = document.createElement("div");1387 titleEl.className = "sp-title";1388 titleEl.textContent = entry.title;1389 platter.appendChild(titleEl);13901391 if (entry.username) {1392 const userEl = document.createElement("div");1393 userEl.className = "sp-username";1394 userEl.textContent = entry.username;1395 platter.appendChild(userEl);1396 }13971398 const fillBtn = document.createElement("button");1399 fillBtn.className = "sp-fill-btn";1400 fillBtn.type = "button";1401 fillBtn.textContent = "Fill credentials";1402 fillBtn.addEventListener("click", () => {1403 if (fillBtn.disabled) return;1404 fillBtn.disabled = true;1405 fillBtn.classList.add("filled");1406 fillBtn.innerHTML = checkSvgStr();1407 1408 1409 1410 1411 onSelect(entry.id);1412 setTimeout(() => dismiss(), 400);1413 });1414 platter.appendChild(fillBtn);14151416 const actions = document.createElement("div");1417 actions.className = "sp-actions";14181419 const copyU = document.createElement("button");1420 copyU.className = "sp-action-btn";1421 copyU.type = "button";1422 copyU.title = "Copy username";1423 copyU.textContent = "Copy username";1424 copyU.disabled = !entry.username;1425 copyU.addEventListener("click", async (e) => {1426 e.stopPropagation();1427 if (!entry.username) return;1428 let alreadyWritten = false;1429 try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1430 1431 1432 1433 await chrome.runtime.sendMessage({1434 type: "COPY",1435 payload: { text: entry.username, label: "Username", alreadyWritten },1436 }).catch(() => {});1437 flashActionBtn(copyU);1438 });1439 actions.appendChild(copyU);14401441 const copyP = document.createElement("button");1442 copyP.className = "sp-action-btn";1443 copyP.type = "button";1444 copyP.title = "Copy password";1445 copyP.textContent = "Copy password";1446 copyP.addEventListener("click", async (e) => {1447 e.stopPropagation();1448 const resp = await chrome.runtime.sendMessage({1449 type: "GET_ENTRY_FOR_FILL",1450 payload: { id: entry.id },1451 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;1452 if (!resp?.ok || !resp.data?.password) return;1453 let alreadyWritten = false;1454 try { await navigator.clipboard.writeText(resp.data.password); alreadyWritten = true; } catch {}1455 await chrome.runtime.sendMessage({1456 type: "COPY",1457 payload: { text: resp.data.password, label: "Password", alreadyWritten },1458 }).catch(() => {});1459 flashActionBtn(copyP);1460 });1461 actions.appendChild(copyP);14621463 let trayEl: HTMLDivElement | null = null;1464 let trayInterval: ReturnType<typeof setInterval> | null = null;14651466 function closeTray() {1467 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }1468 trayEl?.remove();1469 trayEl = null;1470 }1471 1472 1473 cleanupHooks.push(closeTray);14741475 async function openTOTPTray() {1476 1477 1478 1479 const resp = await chrome.runtime.sendMessage({1480 type: "GET_ENTRY_FOR_FILL",1481 payload: { id: entry.id },1482 }).catch(() => null) as { ok: boolean; data?: { totp?: string } } | null;1483 const secret = resp?.ok ? resp.data?.totp : undefined;1484 if (!secret) return;14851486 trayEl = document.createElement("div");1487 trayEl.className = "sp-tray";14881489 const label = document.createElement("span");1490 label.className = "sp-tray-label";1491 label.textContent = "2FA";1492 trayEl.appendChild(label);14931494 const codeEl = document.createElement("div");1495 codeEl.className = "sp-totp-code";1496 trayEl.appendChild(codeEl);14971498 const ringNs = "http://www.w3.org/2000/svg";1499 const ring = document.createElementNS(ringNs, "svg");1500 ring.setAttribute("class", "sp-totp-ring");1501 ring.setAttribute("viewBox", "0 0 24 24");1502 const trackC = document.createElementNS(ringNs, "circle");1503 trackC.setAttribute("class", "ring-track");1504 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");1505 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");1506 const fillC = document.createElementNS(ringNs, "circle");1507 fillC.setAttribute("class", "ring-fill");1508 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");1509 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");1510 fillC.setAttribute("stroke-linecap", "round");1511 fillC.setAttribute("stroke-dasharray", "62.83");1512 ring.append(trackC, fillC);1513 trayEl.appendChild(ring);15141515 const copyBtn = document.createElement("button");1516 copyBtn.className = "sp-totp-copy";1517 copyBtn.type = "button";1518 copyBtn.textContent = "Copy";1519 trayEl.appendChild(copyBtn);15201521 function refresh() {1522 const { code, remaining } = generateTOTP(secret!);1523 1524 codeEl.textContent = code.length === 61525 ? `${code.slice(0, 3)} ${code.slice(3)}`1526 : code;1527 1528 1529 const fraction = (30 - remaining) / 30;1530 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));1531 }1532 refresh();1533 trayInterval = setInterval(refresh, 1000);15341535 copyBtn.addEventListener("click", async (e) => {1536 e.stopPropagation();1537 const { code } = generateTOTP(secret!);1538 let alreadyWritten = false;1539 try { await navigator.clipboard.writeText(code); alreadyWritten = true; } catch {}1540 await chrome.runtime.sendMessage({1541 type: "COPY",1542 payload: { text: code, label: "TOTP", alreadyWritten },1543 }).catch(() => {});1544 copyBtn.textContent = "Copied";1545 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);1546 });15471548 platter.appendChild(trayEl);1549 }15501551 if (entry.hasTotp) {1552 const moreBtn = document.createElement("button");1553 moreBtn.className = "sp-action-btn";1554 moreBtn.type = "button";1555 moreBtn.title = "Show 2FA code";1556 moreBtn.textContent = "···";1557 moreBtn.addEventListener("click", (e) => {1558 e.stopPropagation();1559 if (trayEl) {1560 closeTray();1561 moreBtn.classList.remove("flashed");1562 } else {1563 moreBtn.classList.add("flashed");1564 void openTOTPTray();1565 }1566 });1567 actions.appendChild(moreBtn);1568 }15691570 const editBtn = document.createElement("button");1571 editBtn.className = "sp-action-btn sp-action-edit";1572 editBtn.type = "button";1573 editBtn.title = "Edit entry";1574 editBtn.textContent = "Edit";1575 editBtn.addEventListener("click", (e) => {1576 e.stopPropagation();1577 closeTray();1578 void renderInlineEdit(entry);1579 });1580 actions.appendChild(editBtn);15811582 platter.appendChild(actions);1583 container.appendChild(platter);1584 }15851586 158715881589159015911592 async function renderInlineEdit(summary: SelectorEntry) {1593 container.innerHTML = "";1594 const wrap = document.createElement("div");1595 wrap.className = "create-form-wrap";15961597 const head = document.createElement("div");1598 head.className = "create-head";1599 const back = document.createElement("button");1600 back.className = "back-btn";1601 back.type = "button";1602 back.title = "Cancel";1603 back.innerHTML = `<svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><path d="M7 2L3 5.5L7 9"/></svg>`;1604 back.addEventListener("click", () => renderSilverPlatter(summary));1605 head.appendChild(back);1606 const headLbl = document.createElement("div");1607 headLbl.className = "create-label";1608 headLbl.textContent = "Edit entry";1609 head.appendChild(headLbl);1610 wrap.appendChild(head);16111612 const body = document.createElement("div");1613 body.className = "create-body";16141615 1616 const loading = document.createElement("div");1617 loading.className = "empty-hint";1618 loading.textContent = "loading…";1619 body.appendChild(loading);1620 wrap.appendChild(body);1621 container.appendChild(wrap);16221623 const res = (await chrome.runtime.sendMessage({1624 type: "GET_ENTRY_FOR_FILL",1625 payload: { id: summary.id },1626 }).catch(() => null)) as { ok: boolean; data?: { title?: string; username?: string; password?: string; url?: string; totp?: string; notes?: string } } | null;16271628 if (!res || !res.ok || !res.data) {1629 loading.textContent = "couldn't load this entry. try again from the popup.";1630 return;1631 }1632 body.innerHTML = "";16331634 function field(label: string, value: string, type: "text" | "password" = "text"): HTMLInputElement {1635 const group = document.createElement("div");1636 group.className = "f-group";1637 const lbl = document.createElement("div");1638 lbl.className = "f-label";1639 lbl.textContent = label;1640 const input = document.createElement("input");1641 input.className = "f-input";1642 input.type = type;1643 input.value = value;1644 input.autocomplete = "off";1645 group.append(lbl, input);1646 body.appendChild(group);1647 return input;1648 }16491650 const titleInput = field("title", res.data.title ?? "");1651 const urlInput = field("url", res.data.url ?? "");1652 urlInput.classList.add("url-f");1653 const usernameInput = field("username", res.data.username ?? "");16541655 1656 const pwGroup = document.createElement("div");1657 pwGroup.className = "f-group";1658 const pwLbl = document.createElement("div");1659 pwLbl.className = "f-label";1660 pwLbl.textContent = "password";1661 const pwRow = document.createElement("div");1662 pwRow.className = "f-row";1663 const pwInput = document.createElement("input");1664 pwInput.className = "f-input";1665 pwInput.type = "password";1666 pwInput.value = res.data.password ?? "";1667 pwInput.autocomplete = "new-password";1668 const revealBtn = document.createElement("button");1669 revealBtn.className = "gen-btn";1670 revealBtn.type = "button";1671 revealBtn.textContent = "show";1672 revealBtn.addEventListener("click", () => {1673 const reveal = pwInput.type === "password";1674 pwInput.type = reveal ? "text" : "password";1675 revealBtn.textContent = reveal ? "hide" : "show";1676 });1677 pwRow.append(pwInput, revealBtn);1678 pwGroup.append(pwLbl, pwRow);1679 body.appendChild(pwGroup);16801681 const totpInput = field("2fa secret", res.data.totp ?? "");16821683 1684 const notesGroup = document.createElement("div");1685 notesGroup.className = "f-group";1686 const notesLbl = document.createElement("div");1687 notesLbl.className = "f-label";1688 notesLbl.textContent = "notes";1689 const notesInput = document.createElement("textarea");1690 notesInput.className = "f-input";1691 notesInput.rows = 3;1692 notesInput.value = res.data.notes ?? "";1693 notesInput.style.fontFamily = "Inter, sans-serif";1694 notesInput.style.resize = "vertical";1695 notesGroup.append(notesLbl, notesInput);1696 body.appendChild(notesGroup);16971698 1699 const actionsRow = document.createElement("div");1700 actionsRow.className = "f-row";1701 const saveBtn = document.createElement("button");1702 saveBtn.className = "save-btn";1703 saveBtn.type = "button";1704 saveBtn.textContent = "Save changes";1705 saveBtn.style.flex = "1";1706 const cancelBtn = document.createElement("button");1707 cancelBtn.className = "gen-btn";1708 cancelBtn.type = "button";1709 cancelBtn.textContent = "cancel";1710 cancelBtn.addEventListener("click", () => renderSilverPlatter(summary));1711 actionsRow.append(saveBtn, cancelBtn);1712 body.appendChild(actionsRow);17131714 saveBtn.addEventListener("click", async () => {1715 saveBtn.disabled = true;1716 const originalLabel = saveBtn.textContent;1717 saveBtn.textContent = "Saving…";1718 const patch: Record<string, unknown> = {1719 title: titleInput.value.trim(),1720 url: urlInput.value.trim(),1721 username: usernameInput.value.trim(),1722 password: pwInput.value,1723 totp: totpInput.value.trim(),1724 notes: notesInput.value,1725 };1726 const updateRes = (await chrome.runtime.sendMessage({1727 type: "UPDATE_ENTRY",1728 payload: { id: summary.id, patch },1729 }).catch(() => null)) as { ok: boolean; error?: string } | null;1730 if (!updateRes?.ok) {1731 saveBtn.disabled = false;1732 saveBtn.textContent = originalLabel;1733 const err = document.createElement("div");1734 err.className = "empty-hint";1735 err.style.color = "color-mix(in srgb, #e55 70%, transparent)";1736 err.textContent = updateRes?.error ?? "Couldn't save. Try the popup.";1737 body.appendChild(err);1738 setTimeout(() => err.remove(), 3000);1739 return;1740 }1741 1742 const refreshed: SelectorEntry = {1743 id: summary.id,1744 title: patch.title as string,1745 username: patch.username as string,1746 url: patch.url as string,1747 hasTotp: Boolean(patch.totp),1748 hasPasskey: summary.hasPasskey,1749 };1750 renderSilverPlatter(refreshed);1751 });17521753 queueMicrotask(() => titleInput.focus());1754 }17551756 function renderLocked() {1757 container.innerHTML = "";17581759 1760 const domHdr = document.createElement("div");1761 domHdr.className = "domain-header";1762 const eyebrow = document.createElement("div");1763 eyebrow.className = "domain-eyebrow";1764 eyebrow.textContent = "vault locked";1765 const domName = document.createElement("div");1766 domName.className = "domain-name";1767 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1768 domHdr.append(eyebrow, domName);1769 container.appendChild(domHdr);17701771 const wrap = document.createElement("div");1772 wrap.className = "locked-wrap";17731774 1775 const head = document.createElement("div"); head.className = "locked-head";1776 const icon = document.createElement("div"); icon.className = "locked-icon";1777 icon.innerHTML = keySvgStr();1778 const title = document.createElement("div"); title.className = "locked-title";1779 title.textContent = "unlock to autofill";1780 head.append(icon, title);17811782 1783 const pwInp = document.createElement("input");1784 pwInp.className = "locked-input";1785 pwInp.type = "password";1786 pwInp.placeholder = "master password";1787 pwInp.setAttribute("autocomplete", "current-password");17881789 1790 const errEl = document.createElement("div");1791 errEl.className = "locked-error";1792 errEl.style.display = "none";17931794 1795 const unlockBtn = document.createElement("button");1796 unlockBtn.className = "locked-btn";1797 unlockBtn.type = "button";1798 unlockBtn.textContent = "unlock";17991800 async function tryUnlock() {1801 const pw = pwInp.value.trim();1802 if (!pw) { pwInp.focus(); return; }1803 unlockBtn.disabled = true;1804 unlockBtn.textContent = "unlocking…";1805 errEl.style.display = "none";1806 try {1807 const res = await chrome.runtime.sendMessage({1808 type: "UNLOCK",1809 payload: { password: pw },1810 });1811 if ((res as { ok?: boolean }).ok) {1812 dismiss();1813 options.onUnlocked?.();1814 } else {1815 errEl.textContent = "incorrect password";1816 errEl.style.display = "block";1817 pwInp.value = "";1818 pwInp.focus();1819 unlockBtn.disabled = false;1820 unlockBtn.textContent = "unlock";1821 }1822 } catch {1823 errEl.textContent = "could not reach Peach";1824 errEl.style.display = "block";1825 unlockBtn.disabled = false;1826 unlockBtn.textContent = "unlock";1827 }1828 }18291830 pwInp.addEventListener("keydown", (e) => {1831 e.stopPropagation();1832 if (e.key === "Enter") void tryUnlock();1833 });1834 pwInp.addEventListener("keyup", (e) => e.stopPropagation());1835 pwInp.addEventListener("keypress", (e) => e.stopPropagation());1836 unlockBtn.addEventListener("click", () => void tryUnlock());18371838 wrap.append(head, pwInp, errEl, unlockBtn);1839 container.appendChild(wrap);18401841 1842 1843 1844 1845 1846 1847 const captureKeydown = (e: KeyboardEvent) => {1848 if (!host.isConnected) {1849 document.removeEventListener("keydown", captureKeydown, true);1850 return;1851 }1852 1853 if (e.ctrlKey || e.metaKey || e.altKey || e.key === "Tab") return;1854 1855 e.stopImmediatePropagation();1856 if (e.key === "Escape") { dismiss(true); return; }1857 if (e.key === "Enter") { void tryUnlock(); return; }1858 1859 1860 if (document.activeElement !== host) {1861 pwInp.focus({ preventScroll: true });1862 }1863 };1864 captureKeydownRef = captureKeydown;1865 document.addEventListener("keydown", captureKeydown, true);18661867 1868 1869 pwInp.focus({ preventScroll: true });1870 requestAnimationFrame(() => { if (host.isConnected) pwInp.focus({ preventScroll: true }); });1871 }18721873 function handleKeyDown(e: KeyboardEvent) {1874 if (options.locked) {1875 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1876 return;1877 }1878 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter") {1879 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1880 return;1881 }1882 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");1883 const count = allEntryEls.length;1884 if (document.activeElement === searchInput) {1885 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }1886 if (count === 0) return;1887 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;1888 }1889 if (count === 0) return;1890 switch (e.key) {1891 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;1892 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;1893 case "Enter": {1894 if (focusedIndex >= 0) {1895 const el = allEntryEls[focusedIndex] as HTMLElement;1896 const id = el.dataset.id;1897 if (id) { onSelect(id); dismiss(); }1898 else if (el.classList.contains("is-new") || !id) {1899 1900 showCreateForm();1901 }1902 }1903 break;1904 }1905 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;1906 }1907 }19081909 document.addEventListener("keydown", handleKeyDown);19101911 document.body.appendChild(host);19121913 function updatePosition() {1914 const r = anchorEl.getBoundingClientRect();1915 host.style.top = `${r.bottom + window.scrollY + 5}px`;1916 host.style.left = `${r.left + window.scrollX}px`;1917 host.style.width = `${Math.max(r.width, 260)}px`;1918 }1919 1920 1921 1922 1923 1924 1925 1926 let captureKeydownRef: ((e: KeyboardEvent) => void) | null = null;1927 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);19281929 if (options.locked) {1930 renderLocked();1931 } else if (options.isSignup && options.savedIdentities) {1932 1933 1934 renderIdentityHelper();1935 } else if (entries.length === 1 && !options.isSignup) {1936 1937 1938 renderSilverPlatter(entries[0]!);1939 } else {1940 renderList();1941 }19421943 let exitScheduled = false;1944 function dismiss(userAction = false) {1945 1946 1947 1948 if (exitScheduled) return;1949 exitScheduled = true;1950 1951 1952 1953 stopObserving();1954 document.removeEventListener("click", outsideClick, { capture: true });1955 document.removeEventListener("keydown", handleKeyDown);1956 if (captureKeydownRef) {1957 document.removeEventListener("keydown", captureKeydownRef, true);1958 captureKeydownRef = null;1959 }1960 for (const cleanup of cleanupHooks) {1961 try { cleanup(); } catch { }1962 }1963 if (userAction) options.onDismiss?.();1964 container.classList.add("is-exiting");1965 setTimeout(() => host.remove(), 130);1966 }19671968 function outsideClick(ev: MouseEvent) {1969 const path = ev.composedPath();1970 1971 1972 1973 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);1974 }1975 1976 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);19771978 return dismiss;1979}19801981export interface CardSelectorEntry {1982 id: string;1983 title: string;1984 cardholderName?: string;1985 last4?: string;1986 expMonth?: string;1987 expYear?: string;1988 network?: import("../shared/types").CardNetwork;1989}19901991interface CardSelectorOptions {1992 domain?: string;1993 1994 onFill: (entry: import("../shared/types").VaultEntry) => void;1995 1996 onDismiss?: () => void;1997}19981999200020012002200320042005export function showPasskeyProGate(): void {2006 const domain = location.hostname.replace(/^www\./, "");20072008 const host = document.createElement("div");2009 host.setAttribute("data-peach-passkey-gate", "1");2010 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";2011 const shadow = host.attachShadow({ mode: "closed" });20122013 const style = document.createElement("style");2014 style.textContent = `2015 :host { all: initial; }2016 .gate-wrap {2017 position: fixed;2018 top: 50%;2019 left: 50%;2020 transform: translate(-50%, -50%);2021 width: 280px;2022 pointer-events: auto;2023 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;2024 }2025 @keyframes g-in {2026 from { opacity:0; transform: translate(-50%,-52%); }2027 to { opacity:1; transform: translate(-50%,-50%); }2028 }2029 .gate-wrap.is-exiting {2030 animation: g-out 130ms ease-in forwards;2031 }2032 @keyframes g-out {2033 to { opacity:0; transform: translate(-50%,-52%); }2034 }2035 .gate {2036 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2037 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2038 border-radius: 12px;2039 box-shadow: 0 12px 48px rgba(0,0,0,0.75);2040 padding: 16px;2041 font-family: Inter, sans-serif;2042 }2043 .gate-header {2044 display: flex;2045 align-items: center;2046 gap: 7px;2047 margin-bottom: 12px;2048 padding-bottom: 10px;2049 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2050 }2051 .gate-domain {2052 font-size: 12px;2053 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2054 font-family: "IBM Plex Mono", monospace;2055 overflow: hidden;2056 text-overflow: ellipsis;2057 white-space: nowrap;2058 }2059 .gate-title {2060 font-size: 14px;2061 font-weight: 500;2062 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2063 margin-bottom: 6px;2064 }2065 .gate-subtitle {2066 font-size: 12px;2067 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};2068 line-height: 1.5;2069 margin-bottom: 14px;2070 }2071 .gate-btn {2072 display: block;2073 width: 100%;2074 padding: 9px 0;2075 border-radius: 8px;2076 border: none;2077 cursor: pointer;2078 font-size: 13px;2079 font-family: Inter, sans-serif;2080 font-weight: 500;2081 transition: opacity 100ms;2082 margin-bottom: 7px;2083 text-align: center;2084 }2085 .gate-btn:last-child { margin-bottom: 0; }2086 .gate-btn--accent {2087 background: ${accentHex};2088 color: #fff;2089 }2090 .gate-btn--accent:hover { opacity: 0.88; }2091 .gate-btn--ghost {2092 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2093 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};2094 }2095 .gate-btn--ghost:hover { opacity: 0.75; }2096 .backdrop {2097 position: fixed;2098 inset: 0;2099 pointer-events: auto;2100 }2101 `;2102 shadow.appendChild(style);21032104 const backdrop = document.createElement("div");2105 backdrop.className = "backdrop";2106 shadow.appendChild(backdrop);21072108 const wrap = document.createElement("div");2109 wrap.className = "gate-wrap";2110 shadow.appendChild(wrap);21112112 const gate = document.createElement("div");2113 gate.className = "gate";2114 wrap.appendChild(gate);21152116 2117 const header = document.createElement("div");2118 header.className = "gate-header";2119 const icon = document.createElement("span");2120 icon.innerHTML = peachIconSvg();2121 const domainEl = document.createElement("span");2122 domainEl.className = "gate-domain";2123 domainEl.textContent = domain;2124 header.append(icon, domainEl);2125 gate.appendChild(header);21262127 const title = document.createElement("div");2128 title.className = "gate-title";2129 title.textContent = "Passkeys are part of Peach Pro.";2130 gate.appendChild(title);21312132 const subtitle = document.createElement("div");2133 subtitle.className = "gate-subtitle";2134 subtitle.textContent = "Store and use passkeys across all your devices, securely.";2135 gate.appendChild(subtitle);21362137 const upgradeBtn = document.createElement("button");2138 upgradeBtn.className = "gate-btn gate-btn--accent";2139 upgradeBtn.type = "button";2140 upgradeBtn.textContent = "Upgrade to Pro";2141 gate.appendChild(upgradeBtn);21422143 const laterBtn = document.createElement("button");2144 laterBtn.className = "gate-btn gate-btn--ghost";2145 laterBtn.type = "button";2146 laterBtn.textContent = "Not right now";2147 gate.appendChild(laterBtn);21482149 function dismiss() {2150 wrap.classList.add("is-exiting");2151 setTimeout(() => host.remove(), 130);2152 }21532154 upgradeBtn.addEventListener("click", () => {2155 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});2156 dismiss();2157 });2158 laterBtn.addEventListener("click", dismiss);2159 backdrop.addEventListener("click", dismiss);21602161 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {2162 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }2163 });21642165 document.body.appendChild(host);2166}216721682169217021712172217321742175export function showPasskeyRegistrationOffer(args: {2176 rpId: string;2177 rpName?: string;2178 userName?: string;2179 userDisplayName?: string;2180}): void {2181 const domain = location.hostname.replace(/^www\./, "");2182 const displayName = args.rpName || domain;21832184 const host = document.createElement("div");2185 host.setAttribute("data-peach-passkey-offer", "1");2186 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";2187 const shadow = host.attachShadow({ mode: "closed" });21882189 const style = document.createElement("style");2190 style.textContent = `2191 :host { all: initial; }2192 .offer-wrap {2193 position: fixed;2194 top: 16px;2195 right: 16px;2196 width: 320px;2197 pointer-events: auto;2198 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;2199 }2200 @keyframes o-in {2201 from { opacity: 0; transform: translateY(-6px); }2202 to { opacity: 1; transform: translateY(0); }2203 }2204 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }2205 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }2206 .offer {2207 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2208 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2209 border-radius: 12px;2210 box-shadow: 0 12px 48px rgba(0,0,0,0.55);2211 padding: 14px 14px 12px;2212 font-family: Inter, sans-serif;2213 position: relative;2214 }2215 .offer-close {2216 position: absolute;2217 top: 8px;2218 right: 8px;2219 width: 18px;2220 height: 18px;2221 border: none;2222 background: transparent;2223 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};2224 cursor: pointer;2225 font-size: 14px;2226 line-height: 1;2227 padding: 0;2228 }2229 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }2230 .offer-header {2231 display: flex;2232 align-items: center;2233 gap: 7px;2234 margin-bottom: 8px;2235 padding-bottom: 8px;2236 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2237 }2238 .offer-domain {2239 font-size: 12px;2240 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2241 font-family: "IBM Plex Mono", monospace;2242 overflow: hidden;2243 text-overflow: ellipsis;2244 white-space: nowrap;2245 }2246 .offer-title {2247 font-size: 13px;2248 font-weight: 500;2249 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2250 margin-bottom: 4px;2251 }2252 .offer-subtitle {2253 font-size: 11px;2254 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};2255 line-height: 1.45;2256 margin-bottom: 12px;2257 }2258 .offer-btn {2259 display: block;2260 width: 100%;2261 padding: 8px 0;2262 border-radius: 7px;2263 border: none;2264 cursor: pointer;2265 font-size: 12px;2266 font-family: Inter, sans-serif;2267 font-weight: 500;2268 transition: opacity 100ms;2269 margin-bottom: 6px;2270 text-align: center;2271 }2272 .offer-btn:last-child { margin-bottom: 0; }2273 .offer-btn--accent { background: ${accentHex}; color: #fff; }2274 .offer-btn--accent:hover { opacity: 0.88; }2275 .offer-btn--ghost {2276 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2277 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};2278 }2279 .offer-btn--ghost:hover { opacity: 0.75; }2280 `;2281 shadow.appendChild(style);22822283 const wrap = document.createElement("div");2284 wrap.className = "offer-wrap";2285 shadow.appendChild(wrap);22862287 const offer = document.createElement("div");2288 offer.className = "offer";2289 wrap.appendChild(offer);22902291 const closeBtn = document.createElement("button");2292 closeBtn.className = "offer-close";2293 closeBtn.type = "button";2294 closeBtn.setAttribute("aria-label", "Dismiss");2295 closeBtn.textContent = "✕";2296 offer.appendChild(closeBtn);22972298 const header = document.createElement("div");2299 header.className = "offer-header";2300 const icon = document.createElement("span");2301 icon.innerHTML = peachIconSvg();2302 const domainEl = document.createElement("span");2303 domainEl.className = "offer-domain";2304 domainEl.textContent = domain;2305 header.append(icon, domainEl);2306 offer.appendChild(header);23072308 const title = document.createElement("div");2309 title.className = "offer-title";2310 title.textContent = "This site supports passkeys.";2311 offer.appendChild(title);23122313 const subtitle = document.createElement("div");2314 subtitle.className = "offer-subtitle";2315 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";2316 offer.appendChild(subtitle);23172318 const saveBtn = document.createElement("button");2319 saveBtn.className = "offer-btn offer-btn--accent";2320 saveBtn.type = "button";2321 saveBtn.textContent = "Save passkey to Peach";2322 offer.appendChild(saveBtn);23232324 const laterBtn = document.createElement("button");2325 laterBtn.className = "offer-btn offer-btn--ghost";2326 laterBtn.type = "button";2327 laterBtn.textContent = "Not now";2328 offer.appendChild(laterBtn);23292330 function dismiss(decline: boolean) {2331 if (decline) {2332 chrome.runtime.sendMessage({2333 type: "PASSKEY_OFFER_DECLINE",2334 payload: { domain },2335 }).catch(() => {});2336 }2337 wrap.classList.add("is-exiting");2338 setTimeout(() => host.remove(), 130);2339 }23402341 saveBtn.addEventListener("click", () => {2342 chrome.runtime.sendMessage({2343 type: "INITIATE_FIREFOX_PASSKEY_CREATE",2344 payload: {2345 rpId: args.rpId,2346 rpName: args.rpName ?? displayName,2347 userName: args.userName ?? "user",2348 userDisplayName: args.userDisplayName,2349 },2350 }).catch(() => {});2351 dismiss(false);2352 });2353 laterBtn.addEventListener("click", () => dismiss(true));2354 closeBtn.addEventListener("click", () => dismiss(true));23552356 document.body.appendChild(host);2357}2358235923602361236223632364236523662367export function showPasskeyAvailable(args: {2368 rpId: string;2369 userName: string;2370 fromBrowser?: "chrome" | "firefox";2371 currentBrowser?: "chrome" | "firefox" | "unknown";2372}): void {2373 const domain = location.hostname.replace(/^www\./, "");2374 const crossBrowser =2375 args.fromBrowser &&2376 args.currentBrowser &&2377 args.currentBrowser !== "unknown" &&2378 args.fromBrowser !== args.currentBrowser;23792380 const host = document.createElement("div");2381 host.setAttribute("data-peach-passkey-available", "1");2382 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";2383 const shadow = host.attachShadow({ mode: "closed" });23842385 const style = document.createElement("style");2386 style.textContent = `2387 :host { all: initial; }2388 .avail-wrap {2389 position: fixed;2390 top: 16px;2391 right: 16px;2392 width: 320px;2393 pointer-events: auto;2394 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;2395 }2396 @keyframes a-in {2397 from { opacity: 0; transform: translateY(-6px); }2398 to { opacity: 1; transform: translateY(0); }2399 }2400 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }2401 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }2402 .avail {2403 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2404 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2405 border-radius: 12px;2406 box-shadow: 0 12px 48px rgba(0,0,0,0.55);2407 padding: 14px 14px 12px;2408 font-family: Inter, sans-serif;2409 position: relative;2410 }2411 .avail-close {2412 position: absolute;2413 top: 8px;2414 right: 8px;2415 width: 18px;2416 height: 18px;2417 border: none;2418 background: transparent;2419 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};2420 cursor: pointer;2421 font-size: 14px;2422 line-height: 1;2423 padding: 0;2424 }2425 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }2426 .avail-header {2427 display: flex;2428 align-items: center;2429 gap: 7px;2430 margin-bottom: 8px;2431 padding-bottom: 8px;2432 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2433 }2434 .avail-domain {2435 font-size: 12px;2436 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2437 font-family: "IBM Plex Mono", monospace;2438 }2439 .avail-label {2440 font-size: 9px;2441 letter-spacing: 0.16em;2442 text-transform: uppercase;2443 color: ${accentHex};2444 margin-bottom: 4px;2445 }2446 .avail-user {2447 font-size: 13px;2448 font-weight: 500;2449 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2450 margin-bottom: 12px;2451 overflow: hidden;2452 text-overflow: ellipsis;2453 white-space: nowrap;2454 }2455 .avail-cross {2456 font-size: 11px;2457 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};2458 line-height: 1.45;2459 margin-bottom: 12px;2460 padding: 8px;2461 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};2462 border-radius: 6px;2463 }2464 .avail-btn {2465 display: block;2466 width: 100%;2467 padding: 8px 0;2468 border-radius: 7px;2469 border: none;2470 cursor: pointer;2471 font-size: 12px;2472 font-family: Inter, sans-serif;2473 font-weight: 500;2474 transition: opacity 100ms;2475 margin-bottom: 6px;2476 text-align: center;2477 }2478 .avail-btn:last-child { margin-bottom: 0; }2479 .avail-btn--accent { background: ${accentHex}; color: #fff; }2480 .avail-btn--accent:hover { opacity: 0.88; }2481 .avail-btn--ghost {2482 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2483 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};2484 }2485 .avail-btn--ghost:hover { opacity: 0.75; }2486 `;2487 shadow.appendChild(style);24882489 const wrap = document.createElement("div");2490 wrap.className = "avail-wrap";2491 shadow.appendChild(wrap);24922493 const avail = document.createElement("div");2494 avail.className = "avail";2495 wrap.appendChild(avail);24962497 const closeBtn = document.createElement("button");2498 closeBtn.className = "avail-close";2499 closeBtn.type = "button";2500 closeBtn.setAttribute("aria-label", "Dismiss");2501 closeBtn.textContent = "✕";2502 avail.appendChild(closeBtn);25032504 const header = document.createElement("div");2505 header.className = "avail-header";2506 const icon = document.createElement("span");2507 icon.innerHTML = peachIconSvg();2508 const domainEl = document.createElement("span");2509 domainEl.className = "avail-domain";2510 domainEl.textContent = domain;2511 header.append(icon, domainEl);2512 avail.appendChild(header);25132514 const label = document.createElement("div");2515 label.className = "avail-label";2516 label.textContent = "PASSKEY";2517 avail.appendChild(label);25182519 const user = document.createElement("div");2520 user.className = "avail-user";2521 user.textContent = args.userName;2522 avail.appendChild(user);25232524 if (crossBrowser) {2525 const cross = document.createElement("div");2526 cross.className = "avail-cross";2527 cross.textContent =2528 args.fromBrowser === "chrome"2529 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."2530 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";2531 avail.appendChild(cross);2532 }25332534 const signInBtn = document.createElement("button");2535 signInBtn.className = "avail-btn avail-btn--accent";2536 signInBtn.type = "button";2537 signInBtn.textContent = "Sign in with passkey";2538 avail.appendChild(signInBtn);25392540 const passwordBtn = document.createElement("button");2541 passwordBtn.className = "avail-btn avail-btn--ghost";2542 passwordBtn.type = "button";2543 passwordBtn.textContent = "Use password instead";2544 avail.appendChild(passwordBtn);25452546 function dismiss() {2547 wrap.classList.add("is-exiting");2548 setTimeout(() => host.remove(), 130);2549 }25502551 signInBtn.addEventListener("click", () => {2552 2553 2554 2555 2556 2557 dismiss();2558 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});2559 });2560 passwordBtn.addEventListener("click", dismiss);2561 closeBtn.addEventListener("click", dismiss);25622563 document.body.appendChild(host);2564}256525662567256825692570257125722573export function showCardSelector(2574 anchorEl: HTMLInputElement,2575 cards: CardSelectorEntry[],2576 options: CardSelectorOptions2577): () => void {2578 if (cards.length === 0) return () => {};2579 const card = cards[0]!;25802581 const host = document.createElement("div");2582 host.style.cssText = "position: absolute; z-index: 2147483647;";2583 host.style.setProperty("--peach-accent", accentHex);2584 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));2585 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));2586 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));2587 const shadow = host.attachShadow({ mode: "closed" });25882589 const style = shadow.appendChild(document.createElement("style"));2590 style.textContent = SELECTOR_STYLES;25912592 const rect = anchorEl.getBoundingClientRect();2593 host.style.top = `${rect.bottom + window.scrollY + 5}px`;2594 host.style.left = `${rect.left + window.scrollX}px`;2595 host.style.width = `${Math.max(rect.width, 320)}px`;25962597 const container = shadow.appendChild(document.createElement("div"));2598 container.className = "peach-inline-popup";2599 container.setAttribute("data-theme", popupTheme);26002601 2602 const domHdr = document.createElement("div");2603 domHdr.className = "domain-header";2604 const eyebrow = document.createElement("div");2605 eyebrow.className = "domain-eyebrow";2606 eyebrow.textContent = "Pay with Peach";2607 const domName = document.createElement("div");2608 domName.className = "domain-name";2609 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2610 domHdr.append(eyebrow, domName);2611 container.appendChild(domHdr);26122613 const platter = document.createElement("div");2614 platter.className = "card-platter";26152616 if (card.network && card.network !== "unknown") {2617 const net = document.createElement("div");2618 net.className = "cp-network";2619 net.textContent = card.network;2620 platter.appendChild(net);2621 }26222623 const nick = document.createElement("div");2624 nick.className = "cp-nickname";2625 nick.textContent = card.title;2626 platter.appendChild(nick);26272628 if (card.last4) {2629 const l4 = document.createElement("div");2630 l4.className = "cp-last4";2631 l4.textContent = `•••• ${card.last4}`;2632 platter.appendChild(l4);2633 }26342635 const meta = document.createElement("div");2636 meta.className = "cp-meta";2637 if (card.cardholderName) {2638 const name = document.createElement("span");2639 name.textContent = card.cardholderName;2640 meta.appendChild(name);2641 }2642 if (card.expMonth && card.expYear) {2643 if (card.cardholderName) {2644 const sep = document.createElement("span");2645 sep.className = "cp-meta-sep";2646 sep.textContent = "·";2647 meta.appendChild(sep);2648 }2649 const exp = document.createElement("span");2650 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;2651 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;2652 meta.appendChild(exp);2653 }2654 if (meta.childElementCount > 0) platter.appendChild(meta);26552656 const fillWrap = document.createElement("div");2657 fillWrap.className = "cp-fill-wrap";2658 const fillBtn = document.createElement("button");2659 fillBtn.className = "sp-fill-btn";2660 fillBtn.type = "button";2661 fillBtn.textContent = "Fill card details";2662 fillBtn.addEventListener("click", async () => {2663 if (fillBtn.disabled) return;2664 fillBtn.disabled = true;2665 fillBtn.classList.add("filled");2666 fillBtn.innerHTML = checkSvgStr();2667 const resp = await chrome.runtime.sendMessage({2668 type: "GET_ENTRY_FOR_FILL",2669 payload: { id: card.id },2670 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;2671 if (resp?.ok && resp.data) options.onFill(resp.data);2672 setTimeout(() => dismiss(), 400);2673 });2674 fillWrap.appendChild(fillBtn);2675 platter.appendChild(fillWrap);26762677 2678 const actions = document.createElement("div");2679 actions.className = "sp-actions";26802681 function flashBtn(b: HTMLButtonElement) {2682 b.classList.add("flashed");2683 setTimeout(() => b.classList.remove("flashed"), 700);2684 }26852686 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {2687 const resp = await chrome.runtime.sendMessage({2688 type: "GET_ENTRY_FOR_FILL",2689 payload: { id: card.id },2690 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;2691 return resp?.ok && resp.data ? resp.data : null;2692 }26932694 const copyNum = document.createElement("button");2695 copyNum.className = "sp-action-btn";2696 copyNum.type = "button";2697 copyNum.title = "Copy card number";2698 copyNum.textContent = "Copy number";2699 copyNum.addEventListener("click", async (e) => {2700 e.stopPropagation();2701 const full = await fetchFullCard();2702 if (!full?.cardNumber) return;2703 let alreadyWritten = false;2704 try { await navigator.clipboard.writeText(full.cardNumber); alreadyWritten = true; } catch {}2705 await chrome.runtime.sendMessage({2706 type: "COPY",2707 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },2708 }).catch(() => {});2709 flashBtn(copyNum);2710 });2711 actions.appendChild(copyNum);27122713 const copyCvc = document.createElement("button");2714 copyCvc.className = "sp-action-btn";2715 copyCvc.type = "button";2716 copyCvc.title = "Copy CVC";2717 copyCvc.textContent = "Copy CVC";2718 copyCvc.addEventListener("click", async (e) => {2719 e.stopPropagation();2720 const full = await fetchFullCard();2721 if (!full?.cvc) return;2722 let alreadyWritten = false;2723 try { await navigator.clipboard.writeText(full.cvc); alreadyWritten = true; } catch {}2724 await chrome.runtime.sendMessage({2725 type: "COPY",2726 payload: { text: full.cvc, label: "CVC", alreadyWritten },2727 }).catch(() => {});2728 flashBtn(copyCvc);2729 });2730 actions.appendChild(copyCvc);27312732 const closeBtn = document.createElement("button");2733 closeBtn.className = "sp-action-btn";2734 closeBtn.type = "button";2735 closeBtn.title = "Dismiss";2736 closeBtn.textContent = "✕";2737 closeBtn.addEventListener("click", (e) => {2738 e.stopPropagation();2739 dismiss(true);2740 });2741 actions.appendChild(closeBtn);27422743 platter.appendChild(actions);2744 container.appendChild(platter);27452746 document.body.appendChild(host);2747 function updatePosition() {2748 const r = anchorEl.getBoundingClientRect();2749 host.style.top = `${r.bottom + window.scrollY + 5}px`;2750 host.style.left = `${r.left + window.scrollX}px`;2751 host.style.width = `${Math.max(r.width, 320)}px`;2752 }2753 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);27542755 function onKeyDown(e: KeyboardEvent) {2756 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }2757 }2758 document.addEventListener("keydown", onKeyDown);27592760 let exitScheduled = false;2761 function dismiss(userAction = false) {2762 if (exitScheduled) return;2763 exitScheduled = true;2764 stopObserving();2765 document.removeEventListener("click", outsideClick, { capture: true });2766 document.removeEventListener("keydown", onKeyDown);2767 if (userAction) options.onDismiss?.();2768 container.classList.add("is-exiting");2769 setTimeout(() => host.remove(), 130);2770 }27712772 function outsideClick(ev: MouseEvent) {2773 const path = ev.composedPath();2774 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);2775 }2776 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);27772778 return dismiss;2779}