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