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 gap: 8px;373 }374 .domain-header-text {375 display: flex; flex-direction: column; gap: 2px;376 min-width: 0; flex: 1;377 }378 .domain-eyebrow {379 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;380 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;381 }382 .domain-name {383 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);384 font-family: "IBM Plex Mono", monospace;385 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;386 }387 .domain-add-btn {388 flex-shrink: 0;389 width: 22px; height: 22px;390 display: inline-flex; align-items: center; justify-content: center;391 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);392 border: 1px solid color-mix(in srgb, var(--peach-accent) 28%, transparent);393 border-radius: 6px;394 color: var(--peach-accent);395 cursor: pointer; padding: 0;396 transition: background 120ms, border-color 120ms, transform 80ms;397 font-family: "IBM Plex Mono", monospace;398 font-size: 14px; line-height: 1;399 }400 .domain-add-btn:hover {401 background: color-mix(in srgb, var(--peach-accent) 22%, transparent);402 border-color: color-mix(in srgb, var(--peach-accent) 48%, transparent);403 transform: scale(1.06);404 }405 .domain-add-btn:active { transform: scale(0.96); }406407 /* Search */408 .search-wrap {409 padding: 9px 12px 7px;410 border-bottom: 1px solid var(--peach-border);411 }412 .search {413 width: 100%; background: var(--peach-input-bg);414 border: 1px solid var(--peach-input-border);415 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;416 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;417 transition: border-color 120ms, box-shadow 120ms;418 }419 .search:focus {420 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);421 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);422 }423 .search::placeholder { color: var(--peach-text-muted); }424425 /* Results */426 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }427 .results::-webkit-scrollbar { display: none; }428429 /* Entry rows */430 .entry {431 display: flex; align-items: center; padding: 8px 12px;432 cursor: pointer; border-bottom: 1px solid var(--peach-border);433 transition: background 70ms; position: relative; outline: none;434 }435 .entry:last-child { border-bottom: none; }436 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }437 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }438 .entry.focused::before {439 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;440 width: 2px; border-radius: 2px;441 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);442 }443444 .entry-icon {445 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;446 background: var(--peach-input-bg); border: 1px solid var(--peach-border);447 display: flex; align-items: center; justify-content: center;448 overflow: hidden; margin-right: 10px;449 }450 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }451 .entry-icon.is-new {452 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);453 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);454 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);455 }456457 .entry-info { flex: 1; min-width: 0; }458 .entry-title {459 font-size: 12.5px; color: var(--peach-text);460 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;461 line-height: 1.3;462 }463 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }464 .entry-sub {465 font-size: 10px; color: var(--peach-text-dim);466 font-family: "IBM Plex Mono", monospace;467 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;468 margin-top: 1px;469 }470471 .totp-badge {472 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;473 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);474 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);475 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;476 }477 .passkey-badge {478 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;479 border-radius: 4px;480 background: color-mix(in srgb, var(--peach-accent) 14%, transparent);481 color: color-mix(in srgb, var(--peach-accent) 85%, transparent);482 border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);483 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;484 }485486 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }487488 .empty-hint {489 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);490 font-family: "IBM Plex Mono", monospace;491 }492493 /* Create form */494 .create-form-wrap {495 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;496 }497 .create-head {498 display: flex; align-items: center; gap: 8px;499 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);500 }501 .back-btn {502 background: transparent; border: none; cursor: pointer;503 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;504 display: flex; align-items: center; line-height: 0;505 transition: color 100ms, background 100ms;506 }507 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }508 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }509 .create-body { padding: 12px; display: grid; gap: 9px; }510 .f-group { display: grid; gap: 3px; }511 .f-label {512 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;513 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;514 }515 .f-input {516 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);517 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;518 font-size: 11px; font-family: "IBM Plex Mono", monospace;519 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;520 }521 .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); }522 .f-input::placeholder { color: var(--peach-text-muted); }523 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }524 .f-row { display: flex; gap: 6px; }525 .f-row .f-input { flex: 1; min-width: 0; }526 .gen-btn {527 padding: 0 10px; border-radius: 7px; border: none;528 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);529 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);530 font-size: 10px; font-family: "IBM Plex Mono", monospace;531 cursor: pointer; white-space: nowrap; flex-shrink: 0;532 display: flex; align-items: center; gap: 4px;533 transition: background 100ms;534 }535 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }536 .save-btn {537 width: 100%; padding: 8px; border-radius: 8px; border: none;538 background: var(--peach-accent); color: #fff; font-size: 12px;539 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;540 transition: background 120ms, box-shadow 120ms, transform 80ms;541 letter-spacing: 0.01em;542 }543 .save-btn:hover {544 background: color-mix(in srgb, var(--peach-accent) 82%, #000);545 box-shadow: 0 4px 20px var(--peach-glow, transparent);546 }547 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }548 .url-hint {549 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);550 font-family: "IBM Plex Mono", monospace; font-style: italic;551 }552553 /* Success */554 .success-wrap {555 display: flex; flex-direction: column; align-items: center;556 padding: 24px 16px 20px; gap: 8px;557 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;558 }559 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }560 .success-ring {561 width: 40px; height: 40px; border-radius: 50%;562 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);563 display: flex; align-items: center; justify-content: center;564 }565 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }566 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }567568 /* Silver platter (State 1 — single match) */569 .silver-platter {570 padding: 14px 14px 12px;571 display: flex; flex-direction: column; gap: 6px;572 }573 .sp-title {574 font-size: 14px; font-weight: 500; color: var(--peach-text);575 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;576 line-height: 1.25;577 }578 .sp-username {579 font-size: 12px; color: var(--peach-text-dim);580 font-family: "IBM Plex Mono", monospace;581 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;582 margin-bottom: 6px;583 }584 .sp-fill-btn {585 width: 100%; padding: 11px; border-radius: 9px; border: none;586 background: var(--peach-accent); color: #fff;587 font-size: 13px; font-weight: 500; cursor: pointer;588 font-family: Inter, sans-serif; letter-spacing: 0.01em;589 display: flex; align-items: center; justify-content: center; min-height: 38px;590 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;591 }592 .sp-fill-btn:hover {593 filter: brightness(1.1);594 box-shadow: 0 4px 20px var(--peach-glow, transparent);595 transform: translateY(-1px);596 }597 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }598 .sp-fill-btn.filled {599 background: #16a34a; transform: none; filter: none;600 box-shadow: 0 4px 16px rgba(22,163,74,0.35);601 cursor: default;602 }603 .sp-fill-btn:disabled { cursor: default; }604 .sp-actions {605 display: flex; gap: 4px; justify-content: space-between; align-items: center;606 margin-top: 4px;607 }608 .sp-action-btn {609 flex: 1; height: 28px; border: none; border-radius: 6px;610 background: transparent; color: var(--peach-text-dim);611 font-size: 12px; cursor: pointer;612 transition: background 120ms ease, color 120ms ease;613 font-family: Inter, sans-serif; letter-spacing: 0.04em;614 display: inline-flex; align-items: center; justify-content: center; gap: 4px;615 }616 .sp-action-btn:hover:not(:disabled) {617 background: color-mix(in srgb, var(--peach-text) 5%, transparent);618 color: var(--peach-text);619 }620 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }621 .sp-action-btn.flashed {622 color: var(--peach-accent);623 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);624 }625626 /* TOTP tray — shown below the silver-platter secondary actions */627 .sp-tray {628 margin-top: 8px;629 padding: 10px 12px;630 border-radius: 9px;631 background: var(--peach-input-bg);632 border: 1px solid var(--peach-input-border);633 display: flex; align-items: center; gap: 12px;634 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;635 }636 .sp-tray-label {637 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;638 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;639 flex-shrink: 0;640 }641 .sp-totp-code {642 font-size: 18px; font-weight: 500;643 color: var(--peach-text);644 font-family: "IBM Plex Mono", monospace;645 letter-spacing: 0.12em;646 flex: 1;647 text-align: center;648 }649 .sp-totp-ring {650 width: 22px; height: 22px; flex-shrink: 0;651 }652 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }653 .sp-totp-ring .ring-fill {654 stroke: var(--peach-accent);655 transition: stroke-dashoffset 1s linear;656 transform: rotate(-90deg);657 transform-origin: center;658 }659 .sp-totp-copy {660 background: transparent; border: none; cursor: pointer;661 padding: 4px 8px; border-radius: 5px;662 color: var(--peach-text-dim); font-size: 11px;663 font-family: Inter, sans-serif;664 transition: background 120ms, color 120ms;665 }666 .sp-totp-copy:hover {667 background: color-mix(in srgb, var(--peach-text) 5%, transparent);668 color: var(--peach-text);669 }670671 /* Card silver platter (State 7) — reuses .sp-* primary action classes */672 .card-platter {673 padding: 12px 14px 10px;674 display: flex; flex-direction: column; gap: 4px;675 }676 .cp-network {677 font-size: 10px; font-weight: 600;678 color: var(--peach-accent);679 letter-spacing: 0.08em; text-transform: uppercase;680 font-family: Inter, sans-serif;681 }682 .cp-nickname {683 font-size: 14px; font-weight: 500; color: var(--peach-text);684 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;685 line-height: 1.2;686 }687 .cp-last4 {688 font-size: 12px; color: var(--peach-text-dim);689 font-family: "IBM Plex Mono", monospace;690 letter-spacing: 0.04em;691 margin-top: 2px;692 }693 .cp-meta {694 font-size: 11px; color: var(--peach-text-dim);695 font-family: "IBM Plex Mono", monospace;696 margin-top: 2px;697 display: flex; gap: 10px; align-items: center;698 }699 .cp-meta-sep { opacity: 0.5; }700 .cp-fill-wrap { margin-top: 10px; }701702 /* Locked state */703 .locked-wrap {704 display: flex; flex-direction: column; align-items: stretch;705 padding: 14px 13px 13px; gap: 9px;706 }707 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }708 .locked-icon {709 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;710 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);711 display: flex; align-items: center; justify-content: center;712 }713 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }714 .locked-input {715 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);716 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;717 font-size: 11px; font-family: "IBM Plex Mono", monospace;718 outline: none; transition: border-color 120ms, box-shadow 120ms;719 }720 .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); }721 .locked-input::placeholder { color: var(--peach-text-muted); }722 .locked-btn {723 width: 100%; padding: 8px; border-radius: 8px; border: none;724 background: var(--peach-accent); color: #fff; font-size: 12px;725 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;726 transition: background 120ms;727 letter-spacing: 0.01em;728 box-shadow: 0 2px 16px var(--peach-glow, transparent);729 animation: peach-pulse-glow 2.4s ease-in-out infinite;730 }731 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }732 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }733 .locked-error {734 font-size: 10px; color: rgba(220,80,60,0.85);735 font-family: "IBM Plex Mono", monospace;736 }737738 @keyframes peach-pulse-glow {739 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }740 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),741 0 0 48px var(--peach-glow, transparent); }742 }743744 /* ── State 10: identity helper ─────────────────────────────── */745 .identity-list {746 max-height: 192px; overflow-y: auto; padding: 4px 0;747 scrollbar-width: none;748 }749 .identity-list::-webkit-scrollbar { display: none; }750 .peach-identity-row {751 display: flex; align-items: center; gap: 10px;752 padding: 9px 12px; cursor: pointer; border-radius: 8px;753 transition: background 100ms ease;754 }755 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }756 .peach-identity-value {757 font-family: "IBM Plex Mono", monospace; font-size: 13px;758 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;759 }760 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }761 .identity-alt-row {762 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;763 border-radius: 8px; transition: background 100ms ease;764 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;765 }766 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }767 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }768 .identity-alt-input-wrap.open { display: flex; }769 .identity-alt-input {770 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;771 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);772 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;773 outline: none; transition: border-color 120ms;774 }775 .identity-alt-input:focus {776 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);777 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);778 }779 .identity-alt-confirm {780 height: 32px; padding: 0 12px; border-radius: 7px; border: none;781 background: var(--peach-accent); color: #fff; font-size: 12px;782 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;783 }784 /* ── Generator view (after identity picked) ────────────────── */785 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }786 .gen-confirmed {787 display: flex; align-items: center; gap: 8px; padding: 7px 10px;788 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);789 border-radius: 8px;790 }791 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }792 .gen-confirmed-val {793 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);794 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;795 }796 .gen-label {797 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;798 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;799 }800 .gen-pw-row { display: flex; gap: 6px; align-items: center; }801 .gen-pw-val {802 flex: 1; padding: 8px 10px; border-radius: 7px;803 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);804 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);805 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;806 }807 .gen-regen-btn {808 height: 34px; padding: 0 10px; border-radius: 7px; border: none;809 background: color-mix(in srgb, var(--peach-text) 8%, transparent);810 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;811 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;812 }813 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }814 .gen-fill-btn {815 padding: 10px; border-radius: 8px; border: none;816 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;817 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;818 transition: filter 120ms ease, transform 80ms ease;819 }820 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }821 .gen-fill-btn:active { transform: translateY(0); }822 /* ── Save offer (after password filled) ───────────────────── */823 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }824 .save-offer-title {825 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;826 }827 .save-offer-row {828 display: flex; align-items: center; gap: 8px; padding: 6px 8px;829 border-radius: 7px; background: var(--peach-input-bg);830 border: 1px solid var(--peach-input-border);831 }832 .save-offer-lbl {833 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;834 font-family: "IBM Plex Mono", monospace;835 }836 .save-offer-val {837 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);838 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;839 }840 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }841 .save-offer-save {842 flex: 1; padding: 9px; border-radius: 8px; border: none;843 background: var(--peach-accent); color: #fff; font-size: 13px;844 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;845 }846 .save-offer-save:hover { filter: brightness(1.08); }847 .save-offer-skip {848 flex: 1; padding: 9px; border-radius: 8px; border: none;849 background: color-mix(in srgb, var(--peach-text) 8%, transparent);850 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;851 font-family: Inter, sans-serif; transition: background 100ms ease;852 }853 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }854`;855856export function showAutofillSelector(857 anchorEl: HTMLInputElement,858 entries: SelectorEntry[],859 onSelect: (id: string) => void,860 options: SelectorOptions = {}861): () => void {862 const host = document.createElement("div");863 host.style.cssText = "position: absolute; z-index: 2147483647;";864 host.style.setProperty("--peach-accent", accentHex);865 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));866 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));867 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));868 const shadow = host.attachShadow({ mode: "closed" });869870 const style = shadow.appendChild(document.createElement("style"));871 style.textContent = SELECTOR_STYLES;872873 const rect = anchorEl.getBoundingClientRect();874 host.style.top = `${rect.bottom + window.scrollY + 5}px`;875 host.style.left = `${rect.left + window.scrollX}px`;876 877 878 host.style.width = `${Math.max(rect.width, 320)}px`;879880 const container = shadow.appendChild(document.createElement("div"));881 container.className = "peach-inline-popup";882 container.setAttribute("data-theme", popupTheme);883884 const searchableEntries = options.searchableEntries ?? [];885 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();886887 let query = "";888 let focusedIndex = -1;889 let currentMode: "list" | "create" | "success" | "silver-platter" = "list";890 let searchInput: HTMLInputElement | null = null;891 892 893 const cleanupHooks: Array<() => void> = [];894895 896 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";897 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");898 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";899900 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {901 const seen = new Set<string>();902 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });903 }904905 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {906 const q = value.trim().toLowerCase();907 const deduped = uniqueEntries(source);908 if (!q) return deduped.slice(0, 8);909 return deduped.filter(e =>910 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)911 ).slice(0, 12);912 }913914 function activeEntries(): SelectorEntry[] {915 916 if (query.trim()) {917 const merged = uniqueEntries([...entries, ...searchableEntries]);918 return filterEntries(merged, query);919 }920 921 if (entries.length > 0) return entries;922 return filterEntries(searchableEntries, "").slice(0, 8);923 }924925 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {926 const icon = document.createElement("div");927 icon.className = `entry-icon${isNew ? " is-new" : ""}`;928 if (isNew) {929 icon.innerHTML = plusSvgStr();930 } else {931 932 933 934 void url;935 icon.innerHTML = keySvgStr();936 }937 return icon;938 }939940 function renderIdentityHelper() {941 container.innerHTML = "";942 const identities = options.savedIdentities ?? [];943944 945 const hdr = container.appendChild(document.createElement("div"));946 hdr.className = "domain-header";947 const eyebrow = hdr.appendChild(document.createElement("div"));948 eyebrow.className = "domain-eyebrow";949 eyebrow.textContent = "NEW ACCOUNT";950 const domName = hdr.appendChild(document.createElement("div"));951 domName.className = "domain-name";952 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");953954 955 const list = container.appendChild(document.createElement("div"));956 list.className = "identity-list";957 for (const id of identities) {958 const row = list.appendChild(document.createElement("div"));959 row.className = "peach-identity-row";960 row.setAttribute("tabindex", "-1");961 const val = row.appendChild(document.createElement("div"));962 val.className = "peach-identity-value";963 val.textContent = id.value;964 const meta = row.appendChild(document.createElement("div"));965 meta.className = "peach-identity-meta";966 meta.textContent = `used ${id.useCount}×`;967 row.addEventListener("click", () => {968 options.onFillFields?.(id.value, "");969 renderGenerator(id.value);970 });971 }972973 974 if (identities.length > 0) {975 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));976 }977978 979 const altRow = container.appendChild(document.createElement("div"));980 altRow.className = "identity-alt-row";981 altRow.textContent = "+ Use a different email…";982983 const altInputWrap = container.appendChild(document.createElement("div"));984 altInputWrap.className = "identity-alt-input-wrap";985 const altInput = altInputWrap.appendChild(document.createElement("input"));986 altInput.className = "identity-alt-input";987 altInput.type = "email";988 altInput.placeholder = "[email protected]";989 altInput.setAttribute("autocomplete", "off");990 const altConfirm = altInputWrap.appendChild(document.createElement("button"));991 altConfirm.className = "identity-alt-confirm";992 altConfirm.type = "button";993 altConfirm.textContent = "✓";994995 function fillCustomEmail() {996 const v = altInput.value.trim();997 if (!v) return;998 options.onFillFields?.(v, "");999 renderGenerator(v);1000 }10011002 altRow.addEventListener("click", () => {1003 altRow.style.display = "none";1004 altInputWrap.classList.add("open");1005 altInput.focus();1006 });1007 altConfirm.addEventListener("click", fillCustomEmail);1008 altInput.addEventListener("keydown", (e) => {1009 if (e.key === "Enter") { e.preventDefault(); fillCustomEmail(); }1010 });1011 }10121013 function renderGenerator(filledEmail: string) {1014 container.innerHTML = "";1015 const sitePolicy = detectPasswordPolicy();1016 let pw = generatePasswordForSite(sitePolicy);10171018 const wrap = container.appendChild(document.createElement("div"));1019 wrap.className = "gen-view";10201021 1022 const hdrRow = wrap.appendChild(document.createElement("div"));1023 hdrRow.className = "domain-header";1024 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1025 eyebrow2.className = "domain-eyebrow";1026 eyebrow2.textContent = "NEW ACCOUNT";1027 const domName2 = hdrRow.appendChild(document.createElement("div"));1028 domName2.className = "domain-name";1029 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");10301031 1032 const conf = wrap.appendChild(document.createElement("div"));1033 conf.className = "gen-confirmed";1034 const confCheck = conf.appendChild(document.createElement("span"));1035 confCheck.className = "gen-confirmed-check";1036 confCheck.textContent = "✓";1037 const confVal = conf.appendChild(document.createElement("div"));1038 confVal.className = "gen-confirmed-val";1039 confVal.textContent = filledEmail;10401041 1042 const lbl = wrap.appendChild(document.createElement("div"));1043 lbl.className = "gen-label";1044 lbl.textContent = "Generate a password";10451046 1047 const pwRow = wrap.appendChild(document.createElement("div"));1048 pwRow.className = "gen-pw-row";1049 const pwVal = pwRow.appendChild(document.createElement("div"));1050 pwVal.className = "gen-pw-val";1051 pwVal.textContent = pw;1052 const regenBtn = pwRow.appendChild(document.createElement("button"));1053 regenBtn.className = "gen-regen-btn";1054 regenBtn.type = "button";1055 regenBtn.textContent = "⟳ regen";1056 regenBtn.addEventListener("click", () => {1057 pw = generatePasswordForSite(sitePolicy);1058 pwVal.textContent = pw;1059 });10601061 1062 const fillBtn = wrap.appendChild(document.createElement("button"));1063 fillBtn.className = "gen-fill-btn";1064 fillBtn.type = "button";1065 fillBtn.textContent = "Copy & fill";1066 fillBtn.addEventListener("click", () => {1067 options.onFillFields?.("", pw);1068 renderSaveOffer(filledEmail, pw);1069 });1070 }10711072 function renderSaveOffer(filledEmail: string, filledPassword: string) {1073 container.innerHTML = "";1074 const domain = options.domain ?? location.hostname.replace(/^www\./, "");10751076 const wrap = container.appendChild(document.createElement("div"));1077 wrap.className = "save-offer";10781079 const title = wrap.appendChild(document.createElement("div"));1080 title.className = "save-offer-title";1081 title.textContent = "Save this login to Peach?";10821083 function detailRow(label: string, value: string) {1084 const row = wrap.appendChild(document.createElement("div"));1085 row.className = "save-offer-row";1086 const lbl = row.appendChild(document.createElement("div"));1087 lbl.className = "save-offer-lbl";1088 lbl.textContent = label;1089 const val = row.appendChild(document.createElement("div"));1090 val.className = "save-offer-val";1091 val.textContent = value;1092 }10931094 detailRow("site", domain);1095 detailRow("email", filledEmail);1096 detailRow("password", "••••••••••••••••");10971098 const btns = wrap.appendChild(document.createElement("div"));1099 btns.className = "save-offer-btns";11001101 const saveBtn = btns.appendChild(document.createElement("button"));1102 saveBtn.className = "save-offer-save";1103 saveBtn.type = "button";1104 saveBtn.textContent = "Save";1105 saveBtn.addEventListener("click", async () => {1106 saveBtn.disabled = true;1107 saveBtn.textContent = "Saving…";1108 try {1109 const res = await chrome.runtime.sendMessage({1110 type: "ADD_ENTRY",1111 payload: { title: sTitle, url: sOrigin, username: filledEmail, password: filledPassword, type: "login" },1112 });1113 if ((res as { ok?: boolean }).ok) {1114 showSuccess(sTitle);1115 } else {1116 saveBtn.disabled = false;1117 saveBtn.textContent = "Save";1118 }1119 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }1120 });11211122 const skipBtn = btns.appendChild(document.createElement("button"));1123 skipBtn.className = "save-offer-skip";1124 skipBtn.type = "button";1125 skipBtn.textContent = "Not now";1126 skipBtn.addEventListener("click", () => { dismiss(true); });1127 }11281129 function renderList() {1130 container.innerHTML = "";1131 focusedIndex = -1;1132 searchInput = null;11331134 const current = activeEntries();11351136 1137 1138 1139 const domHdr = document.createElement("div");1140 domHdr.className = "domain-header";1141 const headText = document.createElement("div");1142 headText.className = "domain-header-text";1143 const eyebrow = document.createElement("div");1144 eyebrow.className = "domain-eyebrow";1145 eyebrow.textContent = options.isSignup1146 ? "Sign up as"1147 : entries.length > 0 ? "Saved for this site" : "No saved login";1148 const domName = document.createElement("div");1149 domName.className = "domain-name";1150 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1151 headText.append(eyebrow, domName);1152 domHdr.appendChild(headText);11531154 if (!options.isSignup) {1155 const addBtn = document.createElement("button");1156 addBtn.className = "domain-add-btn";1157 addBtn.type = "button";1158 addBtn.setAttribute("aria-label", "Create new entry");1159 addBtn.title = "Create new entry";1160 addBtn.textContent = "+";1161 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });1162 domHdr.appendChild(addBtn);1163 }11641165 container.appendChild(domHdr);11661167 1168 const sw = document.createElement("div"); sw.className = "search-wrap";1169 searchInput = document.createElement("input");1170 searchInput.className = "search";1171 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";1172 searchInput.value = query;1173 searchInput.addEventListener("input", () => {1174 query = searchInput?.value ?? "";1175 renderList();1176 searchInput?.focus();1177 searchInput?.setSelectionRange(query.length, query.length);1178 });1179 sw.appendChild(searchInput);1180 container.appendChild(sw);11811182 1183 const results = document.createElement("div");1184 results.className = "results";1185 container.appendChild(results);11861187 if (current.length === 0 && entries.length > 0) {1188 1189 }11901191 for (let i = 0; i < current.length; i++) {1192 const e = current[i]!;1193 const entryEl = document.createElement("div");1194 entryEl.className = "entry";1195 entryEl.dataset.id = e.id;1196 entryEl.dataset.index = String(i);1197 entryEl.setAttribute("tabindex", "-1");11981199 entryEl.appendChild(makeEntryIcon(e.url));12001201 const info = document.createElement("div"); info.className = "entry-info";1202 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;1203 const subEl = document.createElement("div"); subEl.className = "entry-sub";1204 const domain = e.url ? getDomainFromUrl(e.url) : "";1205 subEl.textContent = domain || e.username || "";1206 info.append(titleEl, subEl);1207 entryEl.appendChild(info);12081209 if (e.hasTotp) {1210 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";1211 entryEl.appendChild(badge);1212 }1213 if (e.hasPasskey) {1214 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";1215 entryEl.appendChild(badge);1216 }12171218 entryEl.addEventListener("click", () => {1219 if (options.isSignup && options.onFillFields) {1220 1221 options.onFillFields(e.username ?? "", "");1222 dismiss();1223 } else {1224 onSelect(e.id);1225 dismiss();1226 }1227 });1228 results.appendChild(entryEl);1229 }12301231 1232 if (current.length > 0) {1233 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1234 }12351236 1237 const newEl = document.createElement("div");1238 newEl.className = "entry";1239 newEl.setAttribute("tabindex", "-1");1240 newEl.appendChild(makeEntryIcon(undefined, true));12411242 const newInfo = document.createElement("div"); newInfo.className = "entry-info";1243 const newTitle = document.createElement("div");1244 newTitle.className = "entry-title is-new";1245 newTitle.textContent = `New entry for ${sTitle}`;1246 const newSub = document.createElement("div");1247 newSub.className = "entry-sub";1248 newSub.textContent = sOrigin;1249 newInfo.append(newTitle, newSub);1250 newEl.appendChild(newInfo);1251 newEl.addEventListener("click", showCreateForm);1252 results.appendChild(newEl);12531254 1255 if (searchInput) {1256 requestAnimationFrame(() => searchInput?.focus());1257 }1258 }12591260 function setFocused(idx: number) {1261 const all = container.querySelectorAll<HTMLElement>(".entry");1262 all.forEach((el, i) => el.classList.toggle("focused", i === idx));1263 focusedIndex = idx;1264 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });1265 }12661267 async function showCreateForm() {1268 currentMode = "create";1269 container.innerHTML = "";12701271 1272 1273 1274 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;1275 if (!statusRes?.unlocked) {1276 renderLocked();1277 return;1278 }12791280 1281 1282 const sitePolicy = detectPasswordPolicy();12831284 const wrap = document.createElement("div");1285 wrap.className = "create-form-wrap";12861287 1288 const head = document.createElement("div"); head.className = "create-head";1289 const backBtn = document.createElement("button"); backBtn.className = "back-btn";1290 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>`;1291 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });1292 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";1293 head.append(backBtn, lbl);12941295 1296 const body = document.createElement("div"); body.className = "create-body";12971298 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {1299 const g = document.createElement("div"); g.className = "f-group";1300 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;1301 const inp = document.createElement("input"); inp.className = "f-input";1302 g.append(l, inp); return { g, inp };1303 }13041305 1306 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";1307 body.appendChild(tg.g);13081309 1310 const ug = fGroup("website");1311 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";1312 if (loginSubdomain) {1313 const h = document.createElement("div"); h.className = "url-hint";1314 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);1315 }1316 body.appendChild(ug.g);13171318 1319 const xg = fGroup("username / email");1320 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";1321 xg.inp.setAttribute("autocomplete", "off");1322 body.appendChild(xg.g);13231324 1325 const pg = fGroup("password");1326 const pwInp = document.createElement("input"); pwInp.className = "f-input";1327 pwInp.type = "text"; pwInp.value = prePassword || generatePasswordForSite(sitePolicy);1328 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';1329 pwInp.style.letterSpacing = "0.04em";1330 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");1331 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";1332 genBtn.innerHTML = genSvgStr() + " regen";1333 genBtn.addEventListener("click", () => { pwInp.value = generatePasswordForSite(sitePolicy); });1334 const pwRow = document.createElement("div"); pwRow.className = "f-row";1335 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);1336 body.appendChild(pg.g);13371338 1339 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";1340 saveBtn.textContent = "Save entry";1341 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";1342 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }1343 saveBtn.addEventListener("click", async () => {1344 const title = tg.inp.value.trim();1345 const pw = pwInp.value.trim();1346 const username = xg.inp.value.trim();1347 if (!title) {1348 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();1349 showFormError("Entry needs a title");1350 return;1351 }1352 if (!pw) {1353 pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus();1354 showFormError("Entry needs a password");1355 return;1356 }1357 errEl.style.display = "none";1358 saveBtn.disabled = true; saveBtn.textContent = "Saving…";1359 try {1360 const res = await chrome.runtime.sendMessage({1361 type: "ADD_ENTRY",1362 payload: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" },1363 });1364 if ((res as { ok?: boolean }).ok) {1365 1366 1367 1368 1369 1370 1371 options.onFillFields?.(username, pw);1372 showSuccess(title);1373 } else {1374 const errMsg = (res as { error?: string }).error === "vault locked"1375 ? "Vault locked — open Peach to save"1376 : "Could not save entry";1377 showFormError(errMsg);1378 saveBtn.disabled = false; saveBtn.textContent = "Save entry";1379 }1380 } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }1381 });1382 body.appendChild(saveBtn);1383 body.appendChild(errEl);13841385 wrap.append(head, body);1386 container.appendChild(wrap);1387 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });1388 }13891390 function showSuccess(title: string) {1391 currentMode = "success";1392 container.innerHTML = "";1393 const wrap = document.createElement("div"); wrap.className = "success-wrap";1394 const ring = document.createElement("div"); ring.className = "success-ring";1395 ring.innerHTML = checkSvgStr();1396 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;1397 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;1398 wrap.append(ring, t, s);1399 container.appendChild(wrap);1400 setTimeout(() => dismiss(), 1600);1401 }14021403 function flashActionBtn(btn: HTMLButtonElement) {1404 btn.classList.add("flashed");1405 setTimeout(() => btn.classList.remove("flashed"), 700);1406 }14071408 function renderSilverPlatter(entry: SelectorEntry) {1409 container.innerHTML = "";1410 searchInput = null;1411 focusedIndex = -1;1412 currentMode = "silver-platter";14131414 const domHdr = document.createElement("div");1415 domHdr.className = "domain-header";1416 const eyebrow = document.createElement("div");1417 eyebrow.className = "domain-eyebrow";1418 eyebrow.textContent = "Saved for this site";1419 const domName = document.createElement("div");1420 domName.className = "domain-name";1421 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1422 domHdr.append(eyebrow, domName);1423 container.appendChild(domHdr);14241425 const platter = document.createElement("div");1426 platter.className = "silver-platter";14271428 const titleEl = document.createElement("div");1429 titleEl.className = "sp-title";1430 titleEl.textContent = entry.title;1431 platter.appendChild(titleEl);14321433 if (entry.username) {1434 const userEl = document.createElement("div");1435 userEl.className = "sp-username";1436 userEl.textContent = entry.username;1437 platter.appendChild(userEl);1438 }14391440 const fillBtn = document.createElement("button");1441 fillBtn.className = "sp-fill-btn";1442 fillBtn.type = "button";1443 fillBtn.textContent = "Fill credentials";1444 fillBtn.addEventListener("click", () => {1445 if (fillBtn.disabled) return;1446 fillBtn.disabled = true;1447 fillBtn.classList.add("filled");1448 fillBtn.innerHTML = checkSvgStr();1449 1450 1451 1452 1453 onSelect(entry.id);1454 setTimeout(() => dismiss(), 400);1455 });1456 platter.appendChild(fillBtn);14571458 const actions = document.createElement("div");1459 actions.className = "sp-actions";14601461 const copyU = document.createElement("button");1462 copyU.className = "sp-action-btn";1463 copyU.type = "button";1464 copyU.title = "Copy username";1465 copyU.textContent = "Copy username";1466 copyU.disabled = !entry.username;1467 copyU.addEventListener("click", async (e) => {1468 e.stopPropagation();1469 if (!entry.username) return;1470 let alreadyWritten = false;1471 try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1472 1473 1474 1475 await chrome.runtime.sendMessage({1476 type: "COPY",1477 payload: { text: entry.username, label: "Username", alreadyWritten },1478 }).catch(() => {});1479 flashActionBtn(copyU);1480 });1481 actions.appendChild(copyU);14821483 const copyP = document.createElement("button");1484 copyP.className = "sp-action-btn";1485 copyP.type = "button";1486 copyP.title = "Copy password";1487 copyP.textContent = "Copy password";1488 copyP.addEventListener("click", async (e) => {1489 e.stopPropagation();1490 const resp = await chrome.runtime.sendMessage({1491 type: "GET_ENTRY_FOR_FILL",1492 payload: { id: entry.id },1493 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;1494 if (!resp?.ok || !resp.data?.password) return;1495 let alreadyWritten = false;1496 try { await navigator.clipboard.writeText(resp.data.password); alreadyWritten = true; } catch {}1497 await chrome.runtime.sendMessage({1498 type: "COPY",1499 payload: { text: resp.data.password, label: "Password", alreadyWritten },1500 }).catch(() => {});1501 flashActionBtn(copyP);1502 });1503 actions.appendChild(copyP);15041505 let trayEl: HTMLDivElement | null = null;1506 let trayInterval: ReturnType<typeof setInterval> | null = null;15071508 function closeTray() {1509 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }1510 trayEl?.remove();1511 trayEl = null;1512 }1513 1514 1515 cleanupHooks.push(closeTray);15161517 async function openTOTPTray() {1518 1519 1520 1521 const resp = await chrome.runtime.sendMessage({1522 type: "GET_ENTRY_FOR_FILL",1523 payload: { id: entry.id },1524 }).catch(() => null) as { ok: boolean; data?: { totp?: string } } | null;1525 const secret = resp?.ok ? resp.data?.totp : undefined;1526 if (!secret) return;15271528 trayEl = document.createElement("div");1529 trayEl.className = "sp-tray";15301531 const label = document.createElement("span");1532 label.className = "sp-tray-label";1533 label.textContent = "2FA";1534 trayEl.appendChild(label);15351536 const codeEl = document.createElement("div");1537 codeEl.className = "sp-totp-code";1538 trayEl.appendChild(codeEl);15391540 const ringNs = "http://www.w3.org/2000/svg";1541 const ring = document.createElementNS(ringNs, "svg");1542 ring.setAttribute("class", "sp-totp-ring");1543 ring.setAttribute("viewBox", "0 0 24 24");1544 const trackC = document.createElementNS(ringNs, "circle");1545 trackC.setAttribute("class", "ring-track");1546 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");1547 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");1548 const fillC = document.createElementNS(ringNs, "circle");1549 fillC.setAttribute("class", "ring-fill");1550 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");1551 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");1552 fillC.setAttribute("stroke-linecap", "round");1553 fillC.setAttribute("stroke-dasharray", "62.83");1554 ring.append(trackC, fillC);1555 trayEl.appendChild(ring);15561557 const copyBtn = document.createElement("button");1558 copyBtn.className = "sp-totp-copy";1559 copyBtn.type = "button";1560 copyBtn.textContent = "Copy";1561 trayEl.appendChild(copyBtn);15621563 function refresh() {1564 const { code, remaining } = generateTOTP(secret!);1565 1566 codeEl.textContent = code.length === 61567 ? `${code.slice(0, 3)} ${code.slice(3)}`1568 : code;1569 1570 1571 const fraction = (30 - remaining) / 30;1572 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));1573 }1574 refresh();1575 trayInterval = setInterval(refresh, 1000);15761577 copyBtn.addEventListener("click", async (e) => {1578 e.stopPropagation();1579 const { code } = generateTOTP(secret!);1580 let alreadyWritten = false;1581 try { await navigator.clipboard.writeText(code); alreadyWritten = true; } catch {}1582 await chrome.runtime.sendMessage({1583 type: "COPY",1584 payload: { text: code, label: "TOTP", alreadyWritten },1585 }).catch(() => {});1586 copyBtn.textContent = "Copied";1587 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);1588 });15891590 platter.appendChild(trayEl);1591 }15921593 if (entry.hasTotp) {1594 const moreBtn = document.createElement("button");1595 moreBtn.className = "sp-action-btn";1596 moreBtn.type = "button";1597 moreBtn.title = "Show 2FA code";1598 moreBtn.textContent = "···";1599 moreBtn.addEventListener("click", (e) => {1600 e.stopPropagation();1601 if (trayEl) {1602 closeTray();1603 moreBtn.classList.remove("flashed");1604 } else {1605 moreBtn.classList.add("flashed");1606 void openTOTPTray();1607 }1608 });1609 actions.appendChild(moreBtn);1610 }16111612 const editBtn = document.createElement("button");1613 editBtn.className = "sp-action-btn sp-action-edit";1614 editBtn.type = "button";1615 editBtn.title = "Edit entry";1616 editBtn.textContent = "Edit";1617 editBtn.addEventListener("click", (e) => {1618 e.stopPropagation();1619 closeTray();1620 void renderInlineEdit(entry);1621 });1622 actions.appendChild(editBtn);16231624 platter.appendChild(actions);1625 container.appendChild(platter);1626 }16271628 162916301631163216331634 async function renderInlineEdit(summary: SelectorEntry) {1635 container.innerHTML = "";1636 const wrap = document.createElement("div");1637 wrap.className = "create-form-wrap";16381639 const head = document.createElement("div");1640 head.className = "create-head";1641 const back = document.createElement("button");1642 back.className = "back-btn";1643 back.type = "button";1644 back.title = "Cancel";1645 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>`;1646 back.addEventListener("click", () => renderSilverPlatter(summary));1647 head.appendChild(back);1648 const headLbl = document.createElement("div");1649 headLbl.className = "create-label";1650 headLbl.textContent = "Edit entry";1651 head.appendChild(headLbl);1652 wrap.appendChild(head);16531654 const body = document.createElement("div");1655 body.className = "create-body";16561657 1658 const loading = document.createElement("div");1659 loading.className = "empty-hint";1660 loading.textContent = "loading…";1661 body.appendChild(loading);1662 wrap.appendChild(body);1663 container.appendChild(wrap);16641665 const res = (await chrome.runtime.sendMessage({1666 type: "GET_ENTRY_FOR_FILL",1667 payload: { id: summary.id },1668 }).catch(() => null)) as { ok: boolean; data?: { title?: string; username?: string; password?: string; url?: string; totp?: string; notes?: string } } | null;16691670 if (!res || !res.ok || !res.data) {1671 loading.textContent = "couldn't load this entry. try again from the popup.";1672 return;1673 }1674 body.innerHTML = "";16751676 function field(label: string, value: string, type: "text" | "password" = "text"): HTMLInputElement {1677 const group = document.createElement("div");1678 group.className = "f-group";1679 const lbl = document.createElement("div");1680 lbl.className = "f-label";1681 lbl.textContent = label;1682 const input = document.createElement("input");1683 input.className = "f-input";1684 input.type = type;1685 input.value = value;1686 input.autocomplete = "off";1687 group.append(lbl, input);1688 body.appendChild(group);1689 return input;1690 }16911692 const titleInput = field("title", res.data.title ?? "");1693 const urlInput = field("url", res.data.url ?? "");1694 urlInput.classList.add("url-f");1695 const usernameInput = field("username", res.data.username ?? "");16961697 1698 const pwGroup = document.createElement("div");1699 pwGroup.className = "f-group";1700 const pwLbl = document.createElement("div");1701 pwLbl.className = "f-label";1702 pwLbl.textContent = "password";1703 const pwRow = document.createElement("div");1704 pwRow.className = "f-row";1705 const pwInput = document.createElement("input");1706 pwInput.className = "f-input";1707 pwInput.type = "password";1708 pwInput.value = res.data.password ?? "";1709 pwInput.autocomplete = "new-password";1710 const revealBtn = document.createElement("button");1711 revealBtn.className = "gen-btn";1712 revealBtn.type = "button";1713 revealBtn.textContent = "show";1714 revealBtn.addEventListener("click", () => {1715 const reveal = pwInput.type === "password";1716 pwInput.type = reveal ? "text" : "password";1717 revealBtn.textContent = reveal ? "hide" : "show";1718 });1719 pwRow.append(pwInput, revealBtn);1720 pwGroup.append(pwLbl, pwRow);1721 body.appendChild(pwGroup);17221723 const totpInput = field("2fa secret", res.data.totp ?? "");17241725 1726 const notesGroup = document.createElement("div");1727 notesGroup.className = "f-group";1728 const notesLbl = document.createElement("div");1729 notesLbl.className = "f-label";1730 notesLbl.textContent = "notes";1731 const notesInput = document.createElement("textarea");1732 notesInput.className = "f-input";1733 notesInput.rows = 3;1734 notesInput.value = res.data.notes ?? "";1735 notesInput.style.fontFamily = "Inter, sans-serif";1736 notesInput.style.resize = "vertical";1737 notesGroup.append(notesLbl, notesInput);1738 body.appendChild(notesGroup);17391740 1741 const actionsRow = document.createElement("div");1742 actionsRow.className = "f-row";1743 const saveBtn = document.createElement("button");1744 saveBtn.className = "save-btn";1745 saveBtn.type = "button";1746 saveBtn.textContent = "Save changes";1747 saveBtn.style.flex = "1";1748 const cancelBtn = document.createElement("button");1749 cancelBtn.className = "gen-btn";1750 cancelBtn.type = "button";1751 cancelBtn.textContent = "cancel";1752 cancelBtn.addEventListener("click", () => renderSilverPlatter(summary));1753 actionsRow.append(saveBtn, cancelBtn);1754 body.appendChild(actionsRow);17551756 saveBtn.addEventListener("click", async () => {1757 saveBtn.disabled = true;1758 const originalLabel = saveBtn.textContent;1759 saveBtn.textContent = "Saving…";1760 const patch: Record<string, unknown> = {1761 title: titleInput.value.trim(),1762 url: urlInput.value.trim(),1763 username: usernameInput.value.trim(),1764 password: pwInput.value,1765 totp: totpInput.value.trim(),1766 notes: notesInput.value,1767 };1768 const updateRes = (await chrome.runtime.sendMessage({1769 type: "UPDATE_ENTRY",1770 payload: { id: summary.id, patch },1771 }).catch(() => null)) as { ok: boolean; error?: string } | null;1772 if (!updateRes?.ok) {1773 saveBtn.disabled = false;1774 saveBtn.textContent = originalLabel;1775 const err = document.createElement("div");1776 err.className = "empty-hint";1777 err.style.color = "color-mix(in srgb, #e55 70%, transparent)";1778 err.textContent = updateRes?.error ?? "Couldn't save. Try the popup.";1779 body.appendChild(err);1780 setTimeout(() => err.remove(), 3000);1781 return;1782 }1783 1784 const refreshed: SelectorEntry = {1785 id: summary.id,1786 title: patch.title as string,1787 username: patch.username as string,1788 url: patch.url as string,1789 hasTotp: Boolean(patch.totp),1790 hasPasskey: summary.hasPasskey,1791 };1792 renderSilverPlatter(refreshed);1793 });17941795 queueMicrotask(() => titleInput.focus());1796 }17971798 function renderLocked() {1799 container.innerHTML = "";18001801 1802 const domHdr = document.createElement("div");1803 domHdr.className = "domain-header";1804 const eyebrow = document.createElement("div");1805 eyebrow.className = "domain-eyebrow";1806 eyebrow.textContent = "vault locked";1807 const domName = document.createElement("div");1808 domName.className = "domain-name";1809 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1810 domHdr.append(eyebrow, domName);1811 container.appendChild(domHdr);18121813 const wrap = document.createElement("div");1814 wrap.className = "locked-wrap";18151816 1817 const head = document.createElement("div"); head.className = "locked-head";1818 const icon = document.createElement("div"); icon.className = "locked-icon";1819 icon.innerHTML = keySvgStr();1820 const title = document.createElement("div"); title.className = "locked-title";1821 title.textContent = "unlock to autofill";1822 head.append(icon, title);18231824 1825 const pwInp = document.createElement("input");1826 pwInp.className = "locked-input";1827 pwInp.type = "password";1828 pwInp.placeholder = "master password";1829 pwInp.setAttribute("autocomplete", "current-password");18301831 1832 const errEl = document.createElement("div");1833 errEl.className = "locked-error";1834 errEl.style.display = "none";18351836 1837 const unlockBtn = document.createElement("button");1838 unlockBtn.className = "locked-btn";1839 unlockBtn.type = "button";1840 unlockBtn.textContent = "unlock";18411842 async function tryUnlock() {1843 const pw = pwInp.value.trim();1844 if (!pw) { pwInp.focus(); return; }1845 unlockBtn.disabled = true;1846 unlockBtn.textContent = "unlocking…";1847 errEl.style.display = "none";1848 try {1849 const res = await chrome.runtime.sendMessage({1850 type: "UNLOCK",1851 payload: { password: pw },1852 });1853 if ((res as { ok?: boolean }).ok) {1854 dismiss();1855 options.onUnlocked?.();1856 } else {1857 errEl.textContent = "incorrect password";1858 errEl.style.display = "block";1859 pwInp.value = "";1860 pwInp.focus();1861 unlockBtn.disabled = false;1862 unlockBtn.textContent = "unlock";1863 }1864 } catch {1865 errEl.textContent = "could not reach Peach";1866 errEl.style.display = "block";1867 unlockBtn.disabled = false;1868 unlockBtn.textContent = "unlock";1869 }1870 }18711872 pwInp.addEventListener("keydown", (e) => {1873 e.stopPropagation();1874 if (e.key === "Enter") void tryUnlock();1875 });1876 pwInp.addEventListener("keyup", (e) => e.stopPropagation());1877 pwInp.addEventListener("keypress", (e) => e.stopPropagation());1878 unlockBtn.addEventListener("click", () => void tryUnlock());18791880 wrap.append(head, pwInp, errEl, unlockBtn);1881 container.appendChild(wrap);18821883 1884 1885 1886 1887 1888 1889 const captureKeydown = (e: KeyboardEvent) => {1890 if (!host.isConnected) {1891 document.removeEventListener("keydown", captureKeydown, true);1892 return;1893 }1894 1895 if (e.ctrlKey || e.metaKey || e.altKey || e.key === "Tab") return;1896 1897 e.stopImmediatePropagation();1898 if (e.key === "Escape") { dismiss(true); return; }1899 if (e.key === "Enter") { void tryUnlock(); return; }1900 1901 1902 if (document.activeElement !== host) {1903 pwInp.focus({ preventScroll: true });1904 }1905 };1906 captureKeydownRef = captureKeydown;1907 document.addEventListener("keydown", captureKeydown, true);19081909 1910 1911 pwInp.focus({ preventScroll: true });1912 requestAnimationFrame(() => { if (host.isConnected) pwInp.focus({ preventScroll: true }); });1913 }19141915 function handleKeyDown(e: KeyboardEvent) {1916 if (options.locked) {1917 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1918 return;1919 }1920 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter") {1921 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1922 return;1923 }1924 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");1925 const count = allEntryEls.length;1926 if (document.activeElement === searchInput) {1927 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }1928 if (count === 0) return;1929 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;1930 }1931 if (count === 0) return;1932 switch (e.key) {1933 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;1934 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;1935 case "Enter": {1936 if (focusedIndex >= 0) {1937 const el = allEntryEls[focusedIndex] as HTMLElement;1938 const id = el.dataset.id;1939 if (id) { onSelect(id); dismiss(); }1940 else if (el.classList.contains("is-new") || !id) {1941 1942 showCreateForm();1943 }1944 }1945 break;1946 }1947 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;1948 }1949 }19501951 document.addEventListener("keydown", handleKeyDown);19521953 document.body.appendChild(host);19541955 function updatePosition() {1956 const r = anchorEl.getBoundingClientRect();1957 host.style.top = `${r.bottom + window.scrollY + 5}px`;1958 host.style.left = `${r.left + window.scrollX}px`;1959 host.style.width = `${Math.max(r.width, 260)}px`;1960 }1961 1962 1963 1964 1965 1966 1967 1968 let captureKeydownRef: ((e: KeyboardEvent) => void) | null = null;1969 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);19701971 if (options.locked) {1972 renderLocked();1973 } else if (options.isSignup && options.savedIdentities) {1974 1975 1976 renderIdentityHelper();1977 } else if (entries.length === 1 && !options.isSignup) {1978 1979 1980 renderSilverPlatter(entries[0]!);1981 } else {1982 renderList();1983 }19841985 let exitScheduled = false;1986 function dismiss(userAction = false) {1987 1988 1989 1990 if (exitScheduled) return;1991 exitScheduled = true;1992 1993 1994 1995 stopObserving();1996 document.removeEventListener("click", outsideClick, { capture: true });1997 document.removeEventListener("keydown", handleKeyDown);1998 if (captureKeydownRef) {1999 document.removeEventListener("keydown", captureKeydownRef, true);2000 captureKeydownRef = null;2001 }2002 for (const cleanup of cleanupHooks) {2003 try { cleanup(); } catch { }2004 }2005 if (userAction) options.onDismiss?.();2006 container.classList.add("is-exiting");2007 setTimeout(() => host.remove(), 130);2008 }20092010 function outsideClick(ev: MouseEvent) {2011 const path = ev.composedPath();2012 2013 2014 2015 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);2016 }2017 2018 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);20192020 return dismiss;2021}20222023export interface CardSelectorEntry {2024 id: string;2025 title: string;2026 cardholderName?: string;2027 last4?: string;2028 expMonth?: string;2029 expYear?: string;2030 network?: import("../shared/types").CardNetwork;2031}20322033interface CardSelectorOptions {2034 domain?: string;2035 2036 onFill: (entry: import("../shared/types").VaultEntry) => void;2037 2038 onDismiss?: () => void;2039}20402041204220432044204520462047export function showPasskeyProGate(): void {2048 const domain = location.hostname.replace(/^www\./, "");20492050 const host = document.createElement("div");2051 host.setAttribute("data-peach-passkey-gate", "1");2052 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";2053 const shadow = host.attachShadow({ mode: "closed" });20542055 const style = document.createElement("style");2056 style.textContent = `2057 :host { all: initial; }2058 .gate-wrap {2059 position: fixed;2060 top: 50%;2061 left: 50%;2062 transform: translate(-50%, -50%);2063 width: 280px;2064 pointer-events: auto;2065 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;2066 }2067 @keyframes g-in {2068 from { opacity:0; transform: translate(-50%,-52%); }2069 to { opacity:1; transform: translate(-50%,-50%); }2070 }2071 .gate-wrap.is-exiting {2072 animation: g-out 130ms ease-in forwards;2073 }2074 @keyframes g-out {2075 to { opacity:0; transform: translate(-50%,-52%); }2076 }2077 .gate {2078 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2079 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2080 border-radius: 12px;2081 box-shadow: 0 12px 48px rgba(0,0,0,0.75);2082 padding: 16px;2083 font-family: Inter, sans-serif;2084 }2085 .gate-header {2086 display: flex;2087 align-items: center;2088 gap: 7px;2089 margin-bottom: 12px;2090 padding-bottom: 10px;2091 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2092 }2093 .gate-domain {2094 font-size: 12px;2095 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2096 font-family: "IBM Plex Mono", monospace;2097 overflow: hidden;2098 text-overflow: ellipsis;2099 white-space: nowrap;2100 }2101 .gate-title {2102 font-size: 14px;2103 font-weight: 500;2104 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2105 margin-bottom: 6px;2106 }2107 .gate-subtitle {2108 font-size: 12px;2109 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};2110 line-height: 1.5;2111 margin-bottom: 14px;2112 }2113 .gate-btn {2114 display: block;2115 width: 100%;2116 padding: 9px 0;2117 border-radius: 8px;2118 border: none;2119 cursor: pointer;2120 font-size: 13px;2121 font-family: Inter, sans-serif;2122 font-weight: 500;2123 transition: opacity 100ms;2124 margin-bottom: 7px;2125 text-align: center;2126 }2127 .gate-btn:last-child { margin-bottom: 0; }2128 .gate-btn--accent {2129 background: ${accentHex};2130 color: #fff;2131 }2132 .gate-btn--accent:hover { opacity: 0.88; }2133 .gate-btn--ghost {2134 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2135 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};2136 }2137 .gate-btn--ghost:hover { opacity: 0.75; }2138 .backdrop {2139 position: fixed;2140 inset: 0;2141 pointer-events: auto;2142 }2143 `;2144 shadow.appendChild(style);21452146 const backdrop = document.createElement("div");2147 backdrop.className = "backdrop";2148 shadow.appendChild(backdrop);21492150 const wrap = document.createElement("div");2151 wrap.className = "gate-wrap";2152 shadow.appendChild(wrap);21532154 const gate = document.createElement("div");2155 gate.className = "gate";2156 wrap.appendChild(gate);21572158 2159 const header = document.createElement("div");2160 header.className = "gate-header";2161 const icon = document.createElement("span");2162 icon.innerHTML = peachIconSvg();2163 const domainEl = document.createElement("span");2164 domainEl.className = "gate-domain";2165 domainEl.textContent = domain;2166 header.append(icon, domainEl);2167 gate.appendChild(header);21682169 const title = document.createElement("div");2170 title.className = "gate-title";2171 title.textContent = "Passkeys are part of Peach Pro.";2172 gate.appendChild(title);21732174 const subtitle = document.createElement("div");2175 subtitle.className = "gate-subtitle";2176 subtitle.textContent = "Store and use passkeys across all your devices, securely.";2177 gate.appendChild(subtitle);21782179 const upgradeBtn = document.createElement("button");2180 upgradeBtn.className = "gate-btn gate-btn--accent";2181 upgradeBtn.type = "button";2182 upgradeBtn.textContent = "Upgrade to Pro";2183 gate.appendChild(upgradeBtn);21842185 const laterBtn = document.createElement("button");2186 laterBtn.className = "gate-btn gate-btn--ghost";2187 laterBtn.type = "button";2188 laterBtn.textContent = "Not right now";2189 gate.appendChild(laterBtn);21902191 function dismiss() {2192 wrap.classList.add("is-exiting");2193 setTimeout(() => host.remove(), 130);2194 }21952196 upgradeBtn.addEventListener("click", () => {2197 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});2198 dismiss();2199 });2200 laterBtn.addEventListener("click", dismiss);2201 backdrop.addEventListener("click", dismiss);22022203 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {2204 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }2205 });22062207 document.body.appendChild(host);2208}220922102211221222132214221522162217export function showPasskeyRegistrationOffer(args: {2218 rpId: string;2219 rpName?: string;2220 userName?: string;2221 userDisplayName?: string;2222}): void {2223 const domain = location.hostname.replace(/^www\./, "");2224 const displayName = args.rpName || domain;22252226 const host = document.createElement("div");2227 host.setAttribute("data-peach-passkey-offer", "1");2228 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";2229 const shadow = host.attachShadow({ mode: "closed" });22302231 const style = document.createElement("style");2232 style.textContent = `2233 :host { all: initial; }2234 .offer-wrap {2235 position: fixed;2236 top: 16px;2237 right: 16px;2238 width: 320px;2239 pointer-events: auto;2240 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;2241 }2242 @keyframes o-in {2243 from { opacity: 0; transform: translateY(-6px); }2244 to { opacity: 1; transform: translateY(0); }2245 }2246 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }2247 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }2248 .offer {2249 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2250 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2251 border-radius: 12px;2252 box-shadow: 0 12px 48px rgba(0,0,0,0.55);2253 padding: 14px 14px 12px;2254 font-family: Inter, sans-serif;2255 position: relative;2256 }2257 .offer-close {2258 position: absolute;2259 top: 8px;2260 right: 8px;2261 width: 18px;2262 height: 18px;2263 border: none;2264 background: transparent;2265 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};2266 cursor: pointer;2267 font-size: 14px;2268 line-height: 1;2269 padding: 0;2270 }2271 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }2272 .offer-header {2273 display: flex;2274 align-items: center;2275 gap: 7px;2276 margin-bottom: 8px;2277 padding-bottom: 8px;2278 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2279 }2280 .offer-domain {2281 font-size: 12px;2282 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2283 font-family: "IBM Plex Mono", monospace;2284 overflow: hidden;2285 text-overflow: ellipsis;2286 white-space: nowrap;2287 }2288 .offer-title {2289 font-size: 13px;2290 font-weight: 500;2291 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2292 margin-bottom: 4px;2293 }2294 .offer-subtitle {2295 font-size: 11px;2296 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};2297 line-height: 1.45;2298 margin-bottom: 12px;2299 }2300 .offer-btn {2301 display: block;2302 width: 100%;2303 padding: 8px 0;2304 border-radius: 7px;2305 border: none;2306 cursor: pointer;2307 font-size: 12px;2308 font-family: Inter, sans-serif;2309 font-weight: 500;2310 transition: opacity 100ms;2311 margin-bottom: 6px;2312 text-align: center;2313 }2314 .offer-btn:last-child { margin-bottom: 0; }2315 .offer-btn--accent { background: ${accentHex}; color: #fff; }2316 .offer-btn--accent:hover { opacity: 0.88; }2317 .offer-btn--ghost {2318 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2319 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};2320 }2321 .offer-btn--ghost:hover { opacity: 0.75; }2322 `;2323 shadow.appendChild(style);23242325 const wrap = document.createElement("div");2326 wrap.className = "offer-wrap";2327 shadow.appendChild(wrap);23282329 const offer = document.createElement("div");2330 offer.className = "offer";2331 wrap.appendChild(offer);23322333 const closeBtn = document.createElement("button");2334 closeBtn.className = "offer-close";2335 closeBtn.type = "button";2336 closeBtn.setAttribute("aria-label", "Dismiss");2337 closeBtn.textContent = "✕";2338 offer.appendChild(closeBtn);23392340 const header = document.createElement("div");2341 header.className = "offer-header";2342 const icon = document.createElement("span");2343 icon.innerHTML = peachIconSvg();2344 const domainEl = document.createElement("span");2345 domainEl.className = "offer-domain";2346 domainEl.textContent = domain;2347 header.append(icon, domainEl);2348 offer.appendChild(header);23492350 const title = document.createElement("div");2351 title.className = "offer-title";2352 title.textContent = "This site supports passkeys.";2353 offer.appendChild(title);23542355 const subtitle = document.createElement("div");2356 subtitle.className = "offer-subtitle";2357 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";2358 offer.appendChild(subtitle);23592360 const saveBtn = document.createElement("button");2361 saveBtn.className = "offer-btn offer-btn--accent";2362 saveBtn.type = "button";2363 saveBtn.textContent = "Save passkey to Peach";2364 offer.appendChild(saveBtn);23652366 const laterBtn = document.createElement("button");2367 laterBtn.className = "offer-btn offer-btn--ghost";2368 laterBtn.type = "button";2369 laterBtn.textContent = "Not now";2370 offer.appendChild(laterBtn);23712372 function dismiss(decline: boolean) {2373 if (decline) {2374 chrome.runtime.sendMessage({2375 type: "PASSKEY_OFFER_DECLINE",2376 payload: { domain },2377 }).catch(() => {});2378 }2379 wrap.classList.add("is-exiting");2380 setTimeout(() => host.remove(), 130);2381 }23822383 saveBtn.addEventListener("click", () => {2384 chrome.runtime.sendMessage({2385 type: "INITIATE_FIREFOX_PASSKEY_CREATE",2386 payload: {2387 rpId: args.rpId,2388 rpName: args.rpName ?? displayName,2389 userName: args.userName ?? "user",2390 userDisplayName: args.userDisplayName,2391 },2392 }).catch(() => {});2393 dismiss(false);2394 });2395 laterBtn.addEventListener("click", () => dismiss(true));2396 closeBtn.addEventListener("click", () => dismiss(true));23972398 document.body.appendChild(host);2399}2400240124022403240424052406240724082409export function showPasskeyAvailable(args: {2410 rpId: string;2411 userName: string;2412 fromBrowser?: "chrome" | "firefox";2413 currentBrowser?: "chrome" | "firefox" | "unknown";2414}): void {2415 const domain = location.hostname.replace(/^www\./, "");2416 const crossBrowser =2417 args.fromBrowser &&2418 args.currentBrowser &&2419 args.currentBrowser !== "unknown" &&2420 args.fromBrowser !== args.currentBrowser;24212422 const host = document.createElement("div");2423 host.setAttribute("data-peach-passkey-available", "1");2424 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";2425 const shadow = host.attachShadow({ mode: "closed" });24262427 const style = document.createElement("style");2428 style.textContent = `2429 :host { all: initial; }2430 .avail-wrap {2431 position: fixed;2432 top: 16px;2433 right: 16px;2434 width: 320px;2435 pointer-events: auto;2436 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;2437 }2438 @keyframes a-in {2439 from { opacity: 0; transform: translateY(-6px); }2440 to { opacity: 1; transform: translateY(0); }2441 }2442 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }2443 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }2444 .avail {2445 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};2446 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};2447 border-radius: 12px;2448 box-shadow: 0 12px 48px rgba(0,0,0,0.55);2449 padding: 14px 14px 12px;2450 font-family: Inter, sans-serif;2451 position: relative;2452 }2453 .avail-close {2454 position: absolute;2455 top: 8px;2456 right: 8px;2457 width: 18px;2458 height: 18px;2459 border: none;2460 background: transparent;2461 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};2462 cursor: pointer;2463 font-size: 14px;2464 line-height: 1;2465 padding: 0;2466 }2467 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }2468 .avail-header {2469 display: flex;2470 align-items: center;2471 gap: 7px;2472 margin-bottom: 8px;2473 padding-bottom: 8px;2474 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};2475 }2476 .avail-domain {2477 font-size: 12px;2478 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};2479 font-family: "IBM Plex Mono", monospace;2480 }2481 .avail-label {2482 font-size: 9px;2483 letter-spacing: 0.16em;2484 text-transform: uppercase;2485 color: ${accentHex};2486 margin-bottom: 4px;2487 }2488 .avail-user {2489 font-size: 13px;2490 font-weight: 500;2491 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};2492 margin-bottom: 12px;2493 overflow: hidden;2494 text-overflow: ellipsis;2495 white-space: nowrap;2496 }2497 .avail-cross {2498 font-size: 11px;2499 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};2500 line-height: 1.45;2501 margin-bottom: 12px;2502 padding: 8px;2503 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};2504 border-radius: 6px;2505 }2506 .avail-btn {2507 display: block;2508 width: 100%;2509 padding: 8px 0;2510 border-radius: 7px;2511 border: none;2512 cursor: pointer;2513 font-size: 12px;2514 font-family: Inter, sans-serif;2515 font-weight: 500;2516 transition: opacity 100ms;2517 margin-bottom: 6px;2518 text-align: center;2519 }2520 .avail-btn:last-child { margin-bottom: 0; }2521 .avail-btn--accent { background: ${accentHex}; color: #fff; }2522 .avail-btn--accent:hover { opacity: 0.88; }2523 .avail-btn--ghost {2524 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};2525 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};2526 }2527 .avail-btn--ghost:hover { opacity: 0.75; }2528 `;2529 shadow.appendChild(style);25302531 const wrap = document.createElement("div");2532 wrap.className = "avail-wrap";2533 shadow.appendChild(wrap);25342535 const avail = document.createElement("div");2536 avail.className = "avail";2537 wrap.appendChild(avail);25382539 const closeBtn = document.createElement("button");2540 closeBtn.className = "avail-close";2541 closeBtn.type = "button";2542 closeBtn.setAttribute("aria-label", "Dismiss");2543 closeBtn.textContent = "✕";2544 avail.appendChild(closeBtn);25452546 const header = document.createElement("div");2547 header.className = "avail-header";2548 const icon = document.createElement("span");2549 icon.innerHTML = peachIconSvg();2550 const domainEl = document.createElement("span");2551 domainEl.className = "avail-domain";2552 domainEl.textContent = domain;2553 header.append(icon, domainEl);2554 avail.appendChild(header);25552556 const label = document.createElement("div");2557 label.className = "avail-label";2558 label.textContent = "PASSKEY";2559 avail.appendChild(label);25602561 const user = document.createElement("div");2562 user.className = "avail-user";2563 user.textContent = args.userName;2564 avail.appendChild(user);25652566 if (crossBrowser) {2567 const cross = document.createElement("div");2568 cross.className = "avail-cross";2569 cross.textContent =2570 args.fromBrowser === "chrome"2571 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."2572 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";2573 avail.appendChild(cross);2574 }25752576 const signInBtn = document.createElement("button");2577 signInBtn.className = "avail-btn avail-btn--accent";2578 signInBtn.type = "button";2579 signInBtn.textContent = "Sign in with passkey";2580 avail.appendChild(signInBtn);25812582 const passwordBtn = document.createElement("button");2583 passwordBtn.className = "avail-btn avail-btn--ghost";2584 passwordBtn.type = "button";2585 passwordBtn.textContent = "Use password instead";2586 avail.appendChild(passwordBtn);25872588 function dismiss() {2589 wrap.classList.add("is-exiting");2590 setTimeout(() => host.remove(), 130);2591 }25922593 signInBtn.addEventListener("click", () => {2594 2595 2596 2597 2598 2599 dismiss();2600 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});2601 });2602 passwordBtn.addEventListener("click", dismiss);2603 closeBtn.addEventListener("click", dismiss);26042605 document.body.appendChild(host);2606}260726082609261026112612261326142615export function showCardSelector(2616 anchorEl: HTMLInputElement,2617 cards: CardSelectorEntry[],2618 options: CardSelectorOptions2619): () => void {2620 if (cards.length === 0) return () => {};2621 const card = cards[0]!;26222623 const host = document.createElement("div");2624 host.style.cssText = "position: absolute; z-index: 2147483647;";2625 host.style.setProperty("--peach-accent", accentHex);2626 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));2627 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));2628 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));2629 const shadow = host.attachShadow({ mode: "closed" });26302631 const style = shadow.appendChild(document.createElement("style"));2632 style.textContent = SELECTOR_STYLES;26332634 const rect = anchorEl.getBoundingClientRect();2635 host.style.top = `${rect.bottom + window.scrollY + 5}px`;2636 host.style.left = `${rect.left + window.scrollX}px`;2637 host.style.width = `${Math.max(rect.width, 320)}px`;26382639 const container = shadow.appendChild(document.createElement("div"));2640 container.className = "peach-inline-popup";2641 container.setAttribute("data-theme", popupTheme);26422643 2644 const domHdr = document.createElement("div");2645 domHdr.className = "domain-header";2646 const eyebrow = document.createElement("div");2647 eyebrow.className = "domain-eyebrow";2648 eyebrow.textContent = "Pay with Peach";2649 const domName = document.createElement("div");2650 domName.className = "domain-name";2651 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2652 domHdr.append(eyebrow, domName);2653 container.appendChild(domHdr);26542655 const platter = document.createElement("div");2656 platter.className = "card-platter";26572658 if (card.network && card.network !== "unknown") {2659 const net = document.createElement("div");2660 net.className = "cp-network";2661 net.textContent = card.network;2662 platter.appendChild(net);2663 }26642665 const nick = document.createElement("div");2666 nick.className = "cp-nickname";2667 nick.textContent = card.title;2668 platter.appendChild(nick);26692670 if (card.last4) {2671 const l4 = document.createElement("div");2672 l4.className = "cp-last4";2673 l4.textContent = `•••• ${card.last4}`;2674 platter.appendChild(l4);2675 }26762677 const meta = document.createElement("div");2678 meta.className = "cp-meta";2679 if (card.cardholderName) {2680 const name = document.createElement("span");2681 name.textContent = card.cardholderName;2682 meta.appendChild(name);2683 }2684 if (card.expMonth && card.expYear) {2685 if (card.cardholderName) {2686 const sep = document.createElement("span");2687 sep.className = "cp-meta-sep";2688 sep.textContent = "·";2689 meta.appendChild(sep);2690 }2691 const exp = document.createElement("span");2692 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;2693 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;2694 meta.appendChild(exp);2695 }2696 if (meta.childElementCount > 0) platter.appendChild(meta);26972698 const fillWrap = document.createElement("div");2699 fillWrap.className = "cp-fill-wrap";2700 const fillBtn = document.createElement("button");2701 fillBtn.className = "sp-fill-btn";2702 fillBtn.type = "button";2703 fillBtn.textContent = "Fill card details";2704 fillBtn.addEventListener("click", async () => {2705 if (fillBtn.disabled) return;2706 fillBtn.disabled = true;2707 fillBtn.classList.add("filled");2708 fillBtn.innerHTML = checkSvgStr();2709 const resp = await chrome.runtime.sendMessage({2710 type: "GET_ENTRY_FOR_FILL",2711 payload: { id: card.id },2712 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;2713 if (resp?.ok && resp.data) options.onFill(resp.data);2714 setTimeout(() => dismiss(), 400);2715 });2716 fillWrap.appendChild(fillBtn);2717 platter.appendChild(fillWrap);27182719 2720 const actions = document.createElement("div");2721 actions.className = "sp-actions";27222723 function flashBtn(b: HTMLButtonElement) {2724 b.classList.add("flashed");2725 setTimeout(() => b.classList.remove("flashed"), 700);2726 }27272728 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {2729 const resp = await chrome.runtime.sendMessage({2730 type: "GET_ENTRY_FOR_FILL",2731 payload: { id: card.id },2732 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;2733 return resp?.ok && resp.data ? resp.data : null;2734 }27352736 const copyNum = document.createElement("button");2737 copyNum.className = "sp-action-btn";2738 copyNum.type = "button";2739 copyNum.title = "Copy card number";2740 copyNum.textContent = "Copy number";2741 copyNum.addEventListener("click", async (e) => {2742 e.stopPropagation();2743 const full = await fetchFullCard();2744 if (!full?.cardNumber) return;2745 let alreadyWritten = false;2746 try { await navigator.clipboard.writeText(full.cardNumber); alreadyWritten = true; } catch {}2747 await chrome.runtime.sendMessage({2748 type: "COPY",2749 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },2750 }).catch(() => {});2751 flashBtn(copyNum);2752 });2753 actions.appendChild(copyNum);27542755 const copyCvc = document.createElement("button");2756 copyCvc.className = "sp-action-btn";2757 copyCvc.type = "button";2758 copyCvc.title = "Copy CVC";2759 copyCvc.textContent = "Copy CVC";2760 copyCvc.addEventListener("click", async (e) => {2761 e.stopPropagation();2762 const full = await fetchFullCard();2763 if (!full?.cvc) return;2764 let alreadyWritten = false;2765 try { await navigator.clipboard.writeText(full.cvc); alreadyWritten = true; } catch {}2766 await chrome.runtime.sendMessage({2767 type: "COPY",2768 payload: { text: full.cvc, label: "CVC", alreadyWritten },2769 }).catch(() => {});2770 flashBtn(copyCvc);2771 });2772 actions.appendChild(copyCvc);27732774 const closeBtn = document.createElement("button");2775 closeBtn.className = "sp-action-btn";2776 closeBtn.type = "button";2777 closeBtn.title = "Dismiss";2778 closeBtn.textContent = "✕";2779 closeBtn.addEventListener("click", (e) => {2780 e.stopPropagation();2781 dismiss(true);2782 });2783 actions.appendChild(closeBtn);27842785 platter.appendChild(actions);2786 container.appendChild(platter);27872788 document.body.appendChild(host);2789 function updatePosition() {2790 const r = anchorEl.getBoundingClientRect();2791 host.style.top = `${r.bottom + window.scrollY + 5}px`;2792 host.style.left = `${r.left + window.scrollX}px`;2793 host.style.width = `${Math.max(r.width, 320)}px`;2794 }2795 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);27962797 function onKeyDown(e: KeyboardEvent) {2798 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }2799 }2800 document.addEventListener("keydown", onKeyDown);28012802 let exitScheduled = false;2803 function dismiss(userAction = false) {2804 if (exitScheduled) return;2805 exitScheduled = true;2806 stopObserving();2807 document.removeEventListener("click", outsideClick, { capture: true });2808 document.removeEventListener("keydown", onKeyDown);2809 if (userAction) options.onDismiss?.();2810 container.classList.add("is-exiting");2811 setTimeout(() => host.remove(), 130);2812 }28132814 function outsideClick(ev: MouseEvent) {2815 const path = ev.composedPath();2816 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);2817 }2818 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);28192820 return dismiss;2821}