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}4344interface SelectorOptions {45 domain?: string;46 searchableEntries?: SelectorEntry[];47 48 isSignup?: boolean;49 50515253 savedIdentities?: SavedIdentity[];54 5556575859 onFillFields?: (username: string, password: string) => void;60 6162636465 locked?: boolean;66 67686970 onUnlocked?: () => void;71 7273747576 onDismiss?: () => void;77}7879function getDomainFromUrl(url: string): string {80 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }81}8283function keySvgStr(): string {84 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>`;85}8687function plusSvgStr(): string {88 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>`;89}9091function peachIconSvg(): string {92 93 94 95 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>`;96}9798function genSvgStr(): string {99 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>`;100}101102function checkSvgStr(): string {103 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>`;104}105106function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {107 const { hostname, origin } = location;108 const parts = hostname.split(".");109 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",110 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",111 "oidc", "federation", "saml", "cas", "ldap"];112 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());113 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;114 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);115 return { origin, title, loginSubdomain };116}117118119120121122123export function showRememberSiteToast(124 pageDomain: string,125 entryId: string,126): void {127 const host = document.createElement("div");128 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";129 host.style.setProperty("--peach-accent", accentHex);130 const shadow = host.attachShadow({ mode: "closed" });131132 const style = document.createElement("style");133 style.textContent = `134 .bar {135 display:flex;align-items:center;gap:10px;padding:10px 14px;136 background:#131109;border:1px solid rgba(240,232,216,0.09);137 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);138 font-family:Inter,sans-serif;white-space:nowrap;139 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;140 }141 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }142 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }143 .yes {144 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;145 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;146 }147 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }148 .no {149 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;150 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;151 }152 `;153 shadow.appendChild(style);154155 const bar = document.createElement("div"); bar.className = "bar";156 const txt = document.createElement("span"); txt.className = "txt";157 txt.textContent = `Remember ${pageDomain} for this entry?`;158 const yesBtn = document.createElement("button"); yesBtn.className = "yes";159 yesBtn.textContent = "Remember";160 yesBtn.addEventListener("click", async () => {161 await chrome.runtime.sendMessage({162 type: "ADD_ADDITIONAL_URL",163 payload: { id: entryId, url: location.origin },164 });165 host.remove();166 });167 const noBtn = document.createElement("button"); noBtn.className = "no";168 noBtn.textContent = "Not now";169 noBtn.addEventListener("click", () => host.remove());170 bar.append(txt, yesBtn, noBtn);171 shadow.appendChild(bar);172 document.body.appendChild(host);173 setTimeout(() => host.remove(), 9000);174}175176function generatePasswordForSite(policy: PasswordPolicy = {}): string {177 return generateSmartPassword(policy);178}179180export function observeAnchorPosition(181 anchorEl: HTMLElement,182 onUpdate: () => void183): () => void {184 let rafId = 0;185 let burstUntil = 0;186187 const runUpdate = () => {188 rafId = 0;189 onUpdate();190 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);191 };192 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };193 const scheduleBurst = (ms = 220) => {194 burstUntil = Math.max(burstUntil, performance.now() + ms);195 scheduleUpdate();196 };197198 const resizeObserver = new ResizeObserver(() => scheduleBurst());199 resizeObserver.observe(anchorEl);200 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);201202 const mutationObserver = new MutationObserver(() => scheduleBurst());203 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });204 if (anchorEl.parentElement) {205 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });206 }207208 const onScroll = () => scheduleUpdate();209 window.addEventListener("scroll", onScroll, true);210 window.addEventListener("resize", onScroll);211 window.visualViewport?.addEventListener("resize", onScroll);212 window.visualViewport?.addEventListener("scroll", onScroll);213 anchorEl.addEventListener("focus", () => scheduleBurst());214 anchorEl.addEventListener("input", () => scheduleBurst());215216 scheduleBurst(260);217218 return () => {219 if (rafId) cancelAnimationFrame(rafId);220 resizeObserver.disconnect();221 mutationObserver.disconnect();222 window.removeEventListener("scroll", onScroll, true);223 window.removeEventListener("resize", onScroll);224 window.visualViewport?.removeEventListener("resize", onScroll);225 window.visualViewport?.removeEventListener("scroll", onScroll);226 };227}228229export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {230 const button = document.createElement("button");231 button.type = "button";232 button.setAttribute("aria-label", "Peach autofill");233 button.title = "Fill with Peach";234 button.innerHTML = peachIconSvg();235 236 237 238 239 button.style.cssText = `240 position: fixed; z-index: 2147483646;241 width: 26px; height: 26px; cursor: pointer;242 pointer-events: auto; border: none; padding: 3px;243 display: inline-flex; align-items: center; justify-content: center;244 background: transparent; border-radius: 999px;245 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;246 opacity: 0;247 `;248 button.addEventListener("mouseenter", () => {249 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;250 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;251 button.style.transform = "scale(1.08)";252 });253 button.addEventListener("mouseleave", () => {254 button.style.background = "transparent";255 button.style.boxShadow = "none";256 button.style.transform = "none";257 });258 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });259 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });260261 document.body.appendChild(button);262263 function updatePosition() {264 const rect = field.getBoundingClientRect();265 if (rect.width === 0 || rect.height === 0) {266 button.style.opacity = "0"; button.style.pointerEvents = "none";267 return;268 }269 const sz = 26;270 const inset = Math.max(5, Math.min(10, rect.height * 0.22));271 const top = rect.top + rect.height / 2 - sz / 2;272 273 274 const minLeft = rect.left + 4;275 const left = Math.max(minLeft, rect.right - sz - inset);276 277 if (rect.width < 40) {278 button.style.opacity = "0"; button.style.pointerEvents = "none";279 return;280 }281 button.style.top = `${top}px`;282 button.style.left = `${left}px`;283 button.style.opacity = "1"; button.style.pointerEvents = "auto";284 }285 updatePosition();286 requestAnimationFrame(() => { button.style.opacity = "1"; });287 const stopObserving = observeAnchorPosition(field, updatePosition);288 return () => { stopObserving(); button.remove(); };289}290291292293294295296297const SELECTOR_STYLES = `298 :host { all: initial; }299 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }300301 .peach-inline-popup[data-theme="dark"] {302 --peach-bg: #1a1410;303 --peach-bg-card: #221c16;304 --peach-text: #f5f0ea;305 --peach-text-dim: #8a7a6a;306 --peach-text-muted: #4a3a2a;307 --peach-border: rgba(255,255,255,0.07);308 --peach-input-bg: rgba(255,255,255,0.05);309 --peach-input-border: rgba(255,255,255,0.08);310 }311 .peach-inline-popup[data-theme="light"] {312 --peach-bg: #fdf8f4;313 --peach-bg-card: #f5ede4;314 --peach-text: #1c1310;315 --peach-text-dim: #6b5a4a;316 --peach-text-muted: #a89080;317 --peach-border: rgba(0,0,0,0.08);318 --peach-input-bg: rgba(0,0,0,0.04);319 --peach-input-border: rgba(0,0,0,0.10);320 }321322 .peach-inline-popup {323 background: var(--peach-bg);324 border: 1px solid var(--peach-border-accent, var(--peach-border));325 border-radius: 14px;326 overflow: hidden;327 box-shadow:328 0 8px 40px var(--peach-glow, transparent),329 0 2px 12px rgba(0,0,0,0.5);330 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;331 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;332 }333 /* Light theme: shadow against a light page is too strong otherwise — drop both334 the accent glow and the dark drop-shadow opacity to keep the popup elegant. */335 .peach-inline-popup[data-theme="light"] {336 box-shadow:337 0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),338 0 2px 12px rgba(0,0,0,0.12);339 }340 .peach-inline-popup.is-exiting {341 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;342 pointer-events: none;343 }344345 @keyframes peach-enter {346 0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }347 65% { opacity: 1; filter: blur(0); }348 100% { opacity: 1; transform: translateY(0) scale(1); }349 }350 @keyframes peach-exit {351 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }352 100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }353 }354 @keyframes peach-slide-in {355 from { opacity: 0; transform: translateX(12px); }356 to { opacity: 1; transform: translateX(0); }357 }358 @keyframes peach-slide-back {359 from { opacity: 0; transform: translateX(-12px); }360 to { opacity: 1; transform: translateX(0); }361 }362 .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }363 .peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }364365 /* Domain header */366 .domain-header {367 padding: 9px 13px 7px;368 border-bottom: 1px solid var(--peach-border);369 display: flex; align-items: center; justify-content: space-between;370 }371 .domain-eyebrow {372 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;373 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;374 }375 .domain-name {376 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);377 font-family: "IBM Plex Mono", monospace;378 }379380 /* Search */381 .search-wrap {382 padding: 9px 12px 7px;383 border-bottom: 1px solid var(--peach-border);384 }385 .search {386 width: 100%; background: var(--peach-input-bg);387 border: 1px solid var(--peach-input-border);388 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;389 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;390 transition: border-color 120ms, box-shadow 120ms;391 }392 .search:focus {393 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);394 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);395 }396 .search::placeholder { color: var(--peach-text-muted); }397398 /* Results */399 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }400 .results::-webkit-scrollbar { display: none; }401402 /* Entry rows */403 .entry {404 display: flex; align-items: center; padding: 8px 12px;405 cursor: pointer; border-bottom: 1px solid var(--peach-border);406 transition: background 70ms; position: relative; outline: none;407 }408 .entry:last-child { border-bottom: none; }409 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }410 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }411 .entry.focused::before {412 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;413 width: 2px; border-radius: 2px;414 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);415 }416417 .entry-icon {418 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;419 background: var(--peach-input-bg); border: 1px solid var(--peach-border);420 display: flex; align-items: center; justify-content: center;421 overflow: hidden; margin-right: 10px;422 }423 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }424 .entry-icon.is-new {425 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);426 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);427 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);428 }429430 .entry-info { flex: 1; min-width: 0; }431 .entry-title {432 font-size: 12.5px; color: var(--peach-text);433 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;434 line-height: 1.3;435 }436 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }437 .entry-sub {438 font-size: 10px; color: var(--peach-text-dim);439 font-family: "IBM Plex Mono", monospace;440 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;441 margin-top: 1px;442 }443444 .totp-badge {445 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;446 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);447 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);448 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;449 }450451 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }452453 .empty-hint {454 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);455 font-family: "IBM Plex Mono", monospace;456 }457458 /* Create form */459 .create-form-wrap {460 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;461 }462 .create-head {463 display: flex; align-items: center; gap: 8px;464 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);465 }466 .back-btn {467 background: transparent; border: none; cursor: pointer;468 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;469 display: flex; align-items: center; line-height: 0;470 transition: color 100ms, background 100ms;471 }472 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }473 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }474 .create-body { padding: 12px; display: grid; gap: 9px; }475 .f-group { display: grid; gap: 3px; }476 .f-label {477 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;478 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;479 }480 .f-input {481 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);482 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;483 font-size: 11px; font-family: "IBM Plex Mono", monospace;484 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;485 }486 .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); }487 .f-input::placeholder { color: var(--peach-text-muted); }488 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }489 .f-row { display: flex; gap: 6px; }490 .f-row .f-input { flex: 1; min-width: 0; }491 .gen-btn {492 padding: 0 10px; border-radius: 7px; border: none;493 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);494 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);495 font-size: 10px; font-family: "IBM Plex Mono", monospace;496 cursor: pointer; white-space: nowrap; flex-shrink: 0;497 display: flex; align-items: center; gap: 4px;498 transition: background 100ms;499 }500 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }501 .save-btn {502 width: 100%; padding: 8px; border-radius: 8px; border: none;503 background: var(--peach-accent); color: #fff; font-size: 12px;504 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;505 transition: background 120ms, box-shadow 120ms, transform 80ms;506 letter-spacing: 0.01em;507 }508 .save-btn:hover {509 background: color-mix(in srgb, var(--peach-accent) 82%, #000);510 box-shadow: 0 4px 20px var(--peach-glow, transparent);511 }512 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }513 .url-hint {514 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);515 font-family: "IBM Plex Mono", monospace; font-style: italic;516 }517518 /* Success */519 .success-wrap {520 display: flex; flex-direction: column; align-items: center;521 padding: 24px 16px 20px; gap: 8px;522 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;523 }524 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }525 .success-ring {526 width: 40px; height: 40px; border-radius: 50%;527 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);528 display: flex; align-items: center; justify-content: center;529 }530 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }531 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }532533 /* Silver platter (State 1 — single match) */534 .silver-platter {535 padding: 14px 14px 12px;536 display: flex; flex-direction: column; gap: 6px;537 }538 .sp-title {539 font-size: 14px; font-weight: 500; color: var(--peach-text);540 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;541 line-height: 1.25;542 }543 .sp-username {544 font-size: 12px; color: var(--peach-text-dim);545 font-family: "IBM Plex Mono", monospace;546 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;547 margin-bottom: 6px;548 }549 .sp-fill-btn {550 width: 100%; padding: 11px; border-radius: 9px; border: none;551 background: var(--peach-accent); color: #fff;552 font-size: 13px; font-weight: 500; cursor: pointer;553 font-family: Inter, sans-serif; letter-spacing: 0.01em;554 display: flex; align-items: center; justify-content: center; min-height: 38px;555 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;556 }557 .sp-fill-btn:hover {558 filter: brightness(1.1);559 box-shadow: 0 4px 20px var(--peach-glow, transparent);560 transform: translateY(-1px);561 }562 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }563 .sp-fill-btn.filled {564 background: #16a34a; transform: none; filter: none;565 box-shadow: 0 4px 16px rgba(22,163,74,0.35);566 cursor: default;567 }568 .sp-fill-btn:disabled { cursor: default; }569 .sp-actions {570 display: flex; gap: 4px; justify-content: space-between; align-items: center;571 margin-top: 4px;572 }573 .sp-action-btn {574 flex: 1; height: 28px; border: none; border-radius: 6px;575 background: transparent; color: var(--peach-text-dim);576 font-size: 12px; cursor: pointer;577 transition: background 120ms ease, color 120ms ease;578 font-family: Inter, sans-serif; letter-spacing: 0.04em;579 display: inline-flex; align-items: center; justify-content: center; gap: 4px;580 }581 .sp-action-btn:hover:not(:disabled) {582 background: color-mix(in srgb, var(--peach-text) 5%, transparent);583 color: var(--peach-text);584 }585 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }586 .sp-action-btn.flashed {587 color: var(--peach-accent);588 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);589 }590591 /* TOTP tray — shown below the silver-platter secondary actions */592 .sp-tray {593 margin-top: 8px;594 padding: 10px 12px;595 border-radius: 9px;596 background: var(--peach-input-bg);597 border: 1px solid var(--peach-input-border);598 display: flex; align-items: center; gap: 12px;599 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;600 }601 .sp-tray-label {602 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;603 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;604 flex-shrink: 0;605 }606 .sp-totp-code {607 font-size: 18px; font-weight: 500;608 color: var(--peach-text);609 font-family: "IBM Plex Mono", monospace;610 letter-spacing: 0.12em;611 flex: 1;612 text-align: center;613 }614 .sp-totp-ring {615 width: 22px; height: 22px; flex-shrink: 0;616 }617 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }618 .sp-totp-ring .ring-fill {619 stroke: var(--peach-accent);620 transition: stroke-dashoffset 1s linear;621 transform: rotate(-90deg);622 transform-origin: center;623 }624 .sp-totp-copy {625 background: transparent; border: none; cursor: pointer;626 padding: 4px 8px; border-radius: 5px;627 color: var(--peach-text-dim); font-size: 11px;628 font-family: Inter, sans-serif;629 transition: background 120ms, color 120ms;630 }631 .sp-totp-copy:hover {632 background: color-mix(in srgb, var(--peach-text) 5%, transparent);633 color: var(--peach-text);634 }635636 /* Card silver platter (State 7) — reuses .sp-* primary action classes */637 .card-platter {638 padding: 12px 14px 10px;639 display: flex; flex-direction: column; gap: 4px;640 }641 .cp-network {642 font-size: 10px; font-weight: 600;643 color: var(--peach-accent);644 letter-spacing: 0.08em; text-transform: uppercase;645 font-family: Inter, sans-serif;646 }647 .cp-nickname {648 font-size: 14px; font-weight: 500; color: var(--peach-text);649 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;650 line-height: 1.2;651 }652 .cp-last4 {653 font-size: 12px; color: var(--peach-text-dim);654 font-family: "IBM Plex Mono", monospace;655 letter-spacing: 0.04em;656 margin-top: 2px;657 }658 .cp-meta {659 font-size: 11px; color: var(--peach-text-dim);660 font-family: "IBM Plex Mono", monospace;661 margin-top: 2px;662 display: flex; gap: 10px; align-items: center;663 }664 .cp-meta-sep { opacity: 0.5; }665 .cp-fill-wrap { margin-top: 10px; }666667 /* Locked state */668 .locked-wrap {669 display: flex; flex-direction: column; align-items: stretch;670 padding: 14px 13px 13px; gap: 9px;671 }672 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }673 .locked-icon {674 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;675 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);676 display: flex; align-items: center; justify-content: center;677 }678 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }679 .locked-input {680 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);681 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;682 font-size: 11px; font-family: "IBM Plex Mono", monospace;683 outline: none; transition: border-color 120ms, box-shadow 120ms;684 }685 .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); }686 .locked-input::placeholder { color: var(--peach-text-muted); }687 .locked-btn {688 width: 100%; padding: 8px; border-radius: 8px; border: none;689 background: var(--peach-accent); color: #fff; font-size: 12px;690 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;691 transition: background 120ms;692 letter-spacing: 0.01em;693 box-shadow: 0 2px 16px var(--peach-glow, transparent);694 animation: peach-pulse-glow 2.4s ease-in-out infinite;695 }696 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }697 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }698 .locked-error {699 font-size: 10px; color: rgba(220,80,60,0.85);700 font-family: "IBM Plex Mono", monospace;701 }702703 @keyframes peach-pulse-glow {704 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }705 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),706 0 0 48px var(--peach-glow, transparent); }707 }708709 /* ── State 10: identity helper ─────────────────────────────── */710 .identity-list { max-height: 192px; overflow-y: auto; padding: 4px 0; }711 .peach-identity-row {712 display: flex; align-items: center; gap: 10px;713 padding: 9px 12px; cursor: pointer; border-radius: 8px;714 transition: background 100ms ease;715 }716 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }717 .peach-identity-value {718 font-family: "IBM Plex Mono", monospace; font-size: 13px;719 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;720 }721 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }722 .identity-alt-row {723 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;724 border-radius: 8px; transition: background 100ms ease;725 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;726 }727 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }728 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }729 .identity-alt-input-wrap.open { display: flex; }730 .identity-alt-input {731 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;732 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);733 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;734 outline: none; transition: border-color 120ms;735 }736 .identity-alt-input:focus {737 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);738 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);739 }740 .identity-alt-confirm {741 height: 32px; padding: 0 12px; border-radius: 7px; border: none;742 background: var(--peach-accent); color: #fff; font-size: 12px;743 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;744 }745 /* ── Generator view (after identity picked) ────────────────── */746 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }747 .gen-confirmed {748 display: flex; align-items: center; gap: 8px; padding: 7px 10px;749 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);750 border-radius: 8px;751 }752 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }753 .gen-confirmed-val {754 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);755 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;756 }757 .gen-label {758 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;759 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;760 }761 .gen-pw-row { display: flex; gap: 6px; align-items: center; }762 .gen-pw-val {763 flex: 1; padding: 8px 10px; border-radius: 7px;764 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);765 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);766 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;767 }768 .gen-regen-btn {769 height: 34px; padding: 0 10px; border-radius: 7px; border: none;770 background: color-mix(in srgb, var(--peach-text) 8%, transparent);771 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;772 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;773 }774 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }775 .gen-fill-btn {776 padding: 10px; border-radius: 8px; border: none;777 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;778 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;779 transition: filter 120ms ease, transform 80ms ease;780 }781 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }782 .gen-fill-btn:active { transform: translateY(0); }783 /* ── Save offer (after password filled) ───────────────────── */784 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }785 .save-offer-title {786 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;787 }788 .save-offer-row {789 display: flex; align-items: center; gap: 8px; padding: 6px 8px;790 border-radius: 7px; background: var(--peach-input-bg);791 border: 1px solid var(--peach-input-border);792 }793 .save-offer-lbl {794 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;795 font-family: "IBM Plex Mono", monospace;796 }797 .save-offer-val {798 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);799 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;800 }801 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }802 .save-offer-save {803 flex: 1; padding: 9px; border-radius: 8px; border: none;804 background: var(--peach-accent); color: #fff; font-size: 13px;805 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;806 }807 .save-offer-save:hover { filter: brightness(1.08); }808 .save-offer-skip {809 flex: 1; padding: 9px; border-radius: 8px; border: none;810 background: color-mix(in srgb, var(--peach-text) 8%, transparent);811 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;812 font-family: Inter, sans-serif; transition: background 100ms ease;813 }814 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }815`;816817export function showAutofillSelector(818 anchorEl: HTMLInputElement,819 entries: SelectorEntry[],820 onSelect: (id: string) => void,821 options: SelectorOptions = {}822): () => void {823 const host = document.createElement("div");824 host.style.cssText = "position: absolute; z-index: 2147483647;";825 host.style.setProperty("--peach-accent", accentHex);826 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));827 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));828 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));829 const shadow = host.attachShadow({ mode: "closed" });830831 const style = shadow.appendChild(document.createElement("style"));832 style.textContent = SELECTOR_STYLES;833834 const rect = anchorEl.getBoundingClientRect();835 host.style.top = `${rect.bottom + window.scrollY + 5}px`;836 host.style.left = `${rect.left + window.scrollX}px`;837 838 839 host.style.width = `${Math.max(rect.width, 320)}px`;840841 const container = shadow.appendChild(document.createElement("div"));842 container.className = "peach-inline-popup";843 container.setAttribute("data-theme", popupTheme);844845 const searchableEntries = options.searchableEntries ?? [];846 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();847848 let query = "";849 let focusedIndex = -1;850 let currentMode: "list" | "create" | "success" | "silver-platter" = "list";851 let searchInput: HTMLInputElement | null = null;852 853 854 const cleanupHooks: Array<() => void> = [];855856 857 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";858 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");859 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";860861 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {862 const seen = new Set<string>();863 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });864 }865866 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {867 const q = value.trim().toLowerCase();868 const deduped = uniqueEntries(source);869 if (!q) return deduped.slice(0, 8);870 return deduped.filter(e =>871 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)872 ).slice(0, 12);873 }874875 function activeEntries(): SelectorEntry[] {876 877 if (query.trim()) {878 const merged = uniqueEntries([...entries, ...searchableEntries]);879 return filterEntries(merged, query);880 }881 882 if (entries.length > 0) return entries;883 return filterEntries(searchableEntries, "").slice(0, 8);884 }885886 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {887 const icon = document.createElement("div");888 icon.className = `entry-icon${isNew ? " is-new" : ""}`;889 if (isNew) {890 icon.innerHTML = plusSvgStr();891 } else {892 893 894 895 void url;896 icon.innerHTML = keySvgStr();897 }898 return icon;899 }900901 function renderIdentityHelper() {902 container.innerHTML = "";903 const identities = options.savedIdentities ?? [];904905 906 const hdr = container.appendChild(document.createElement("div"));907 hdr.className = "domain-header";908 const eyebrow = hdr.appendChild(document.createElement("div"));909 eyebrow.className = "domain-eyebrow";910 eyebrow.textContent = "NEW ACCOUNT";911 const domName = hdr.appendChild(document.createElement("div"));912 domName.className = "domain-name";913 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");914915 916 const list = container.appendChild(document.createElement("div"));917 list.className = "identity-list";918 for (const id of identities) {919 const row = list.appendChild(document.createElement("div"));920 row.className = "peach-identity-row";921 row.setAttribute("tabindex", "-1");922 const val = row.appendChild(document.createElement("div"));923 val.className = "peach-identity-value";924 val.textContent = id.value;925 const meta = row.appendChild(document.createElement("div"));926 meta.className = "peach-identity-meta";927 meta.textContent = `used ${id.useCount}×`;928 row.addEventListener("click", () => {929 options.onFillFields?.(id.value, "");930 renderGenerator(id.value);931 });932 }933934 935 if (identities.length > 0) {936 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));937 }938939 940 const altRow = container.appendChild(document.createElement("div"));941 altRow.className = "identity-alt-row";942 altRow.textContent = "+ Use a different email…";943944 const altInputWrap = container.appendChild(document.createElement("div"));945 altInputWrap.className = "identity-alt-input-wrap";946 const altInput = altInputWrap.appendChild(document.createElement("input"));947 altInput.className = "identity-alt-input";948 altInput.type = "email";949 altInput.placeholder = "[email protected]";950 altInput.setAttribute("autocomplete", "off");951 const altConfirm = altInputWrap.appendChild(document.createElement("button"));952 altConfirm.className = "identity-alt-confirm";953 altConfirm.type = "button";954 altConfirm.textContent = "✓";955956 function fillCustomEmail() {957 const v = altInput.value.trim();958 if (!v) return;959 options.onFillFields?.(v, "");960 renderGenerator(v);961 }962963 altRow.addEventListener("click", () => {964 altRow.style.display = "none";965 altInputWrap.classList.add("open");966 altInput.focus();967 });968 altConfirm.addEventListener("click", fillCustomEmail);969 altInput.addEventListener("keydown", (e) => {970 if (e.key === "Enter") { e.preventDefault(); fillCustomEmail(); }971 });972 }973974 function renderGenerator(filledEmail: string) {975 container.innerHTML = "";976 const sitePolicy = detectPasswordPolicy();977 let pw = generatePasswordForSite(sitePolicy);978979 const wrap = container.appendChild(document.createElement("div"));980 wrap.className = "gen-view";981982 983 const hdrRow = wrap.appendChild(document.createElement("div"));984 hdrRow.className = "domain-header";985 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));986 eyebrow2.className = "domain-eyebrow";987 eyebrow2.textContent = "NEW ACCOUNT";988 const domName2 = hdrRow.appendChild(document.createElement("div"));989 domName2.className = "domain-name";990 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");991992 993 const conf = wrap.appendChild(document.createElement("div"));994 conf.className = "gen-confirmed";995 const confCheck = conf.appendChild(document.createElement("span"));996 confCheck.className = "gen-confirmed-check";997 confCheck.textContent = "✓";998 const confVal = conf.appendChild(document.createElement("div"));999 confVal.className = "gen-confirmed-val";1000 confVal.textContent = filledEmail;10011002 1003 const lbl = wrap.appendChild(document.createElement("div"));1004 lbl.className = "gen-label";1005 lbl.textContent = "Generate a password";10061007 1008 const pwRow = wrap.appendChild(document.createElement("div"));1009 pwRow.className = "gen-pw-row";1010 const pwVal = pwRow.appendChild(document.createElement("div"));1011 pwVal.className = "gen-pw-val";1012 pwVal.textContent = pw;1013 const regenBtn = pwRow.appendChild(document.createElement("button"));1014 regenBtn.className = "gen-regen-btn";1015 regenBtn.type = "button";1016 regenBtn.textContent = "⟳ regen";1017 regenBtn.addEventListener("click", () => {1018 pw = generatePasswordForSite(sitePolicy);1019 pwVal.textContent = pw;1020 });10211022 1023 const fillBtn = wrap.appendChild(document.createElement("button"));1024 fillBtn.className = "gen-fill-btn";1025 fillBtn.type = "button";1026 fillBtn.textContent = "Copy & fill";1027 fillBtn.addEventListener("click", () => {1028 options.onFillFields?.("", pw);1029 renderSaveOffer(filledEmail, pw);1030 });1031 }10321033 function renderSaveOffer(filledEmail: string, filledPassword: string) {1034 container.innerHTML = "";1035 const domain = options.domain ?? location.hostname.replace(/^www\./, "");10361037 const wrap = container.appendChild(document.createElement("div"));1038 wrap.className = "save-offer";10391040 const title = wrap.appendChild(document.createElement("div"));1041 title.className = "save-offer-title";1042 title.textContent = "Save this login to Peach?";10431044 function detailRow(label: string, value: string) {1045 const row = wrap.appendChild(document.createElement("div"));1046 row.className = "save-offer-row";1047 const lbl = row.appendChild(document.createElement("div"));1048 lbl.className = "save-offer-lbl";1049 lbl.textContent = label;1050 const val = row.appendChild(document.createElement("div"));1051 val.className = "save-offer-val";1052 val.textContent = value;1053 }10541055 detailRow("site", domain);1056 detailRow("email", filledEmail);1057 detailRow("password", "••••••••••••••••");10581059 const btns = wrap.appendChild(document.createElement("div"));1060 btns.className = "save-offer-btns";10611062 const saveBtn = btns.appendChild(document.createElement("button"));1063 saveBtn.className = "save-offer-save";1064 saveBtn.type = "button";1065 saveBtn.textContent = "Save";1066 saveBtn.addEventListener("click", async () => {1067 saveBtn.disabled = true;1068 saveBtn.textContent = "Saving…";1069 try {1070 const res = await chrome.runtime.sendMessage({1071 type: "ADD_ENTRY",1072 payload: { title: sTitle, url: sOrigin, username: filledEmail, password: filledPassword, type: "login" },1073 });1074 if ((res as { ok?: boolean }).ok) {1075 showSuccess(sTitle);1076 } else {1077 saveBtn.disabled = false;1078 saveBtn.textContent = "Save";1079 }1080 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }1081 });10821083 const skipBtn = btns.appendChild(document.createElement("button"));1084 skipBtn.className = "save-offer-skip";1085 skipBtn.type = "button";1086 skipBtn.textContent = "Not now";1087 skipBtn.addEventListener("click", () => { dismiss(true); });1088 }10891090 function renderList() {1091 container.innerHTML = "";1092 focusedIndex = -1;1093 searchInput = null;10941095 const current = activeEntries();10961097 1098 const domHdr = document.createElement("div");1099 domHdr.className = "domain-header";1100 const eyebrow = document.createElement("div");1101 eyebrow.className = "domain-eyebrow";1102 eyebrow.textContent = options.isSignup1103 ? "Sign up as"1104 : entries.length > 0 ? "Saved for this site" : "No saved login";1105 const domName = document.createElement("div");1106 domName.className = "domain-name";1107 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1108 domHdr.append(eyebrow, domName);1109 container.appendChild(domHdr);11101111 1112 const sw = document.createElement("div"); sw.className = "search-wrap";1113 searchInput = document.createElement("input");1114 searchInput.className = "search";1115 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";1116 searchInput.value = query;1117 searchInput.addEventListener("input", () => {1118 query = searchInput?.value ?? "";1119 renderList();1120 searchInput?.focus();1121 searchInput?.setSelectionRange(query.length, query.length);1122 });1123 sw.appendChild(searchInput);1124 container.appendChild(sw);11251126 1127 const results = document.createElement("div");1128 results.className = "results";1129 container.appendChild(results);11301131 if (current.length === 0 && entries.length > 0) {1132 1133 }11341135 for (let i = 0; i < current.length; i++) {1136 const e = current[i]!;1137 const entryEl = document.createElement("div");1138 entryEl.className = "entry";1139 entryEl.dataset.id = e.id;1140 entryEl.dataset.index = String(i);1141 entryEl.setAttribute("tabindex", "-1");11421143 entryEl.appendChild(makeEntryIcon(e.url));11441145 const info = document.createElement("div"); info.className = "entry-info";1146 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;1147 const subEl = document.createElement("div"); subEl.className = "entry-sub";1148 const domain = e.url ? getDomainFromUrl(e.url) : "";1149 subEl.textContent = domain || e.username || "";1150 info.append(titleEl, subEl);1151 entryEl.appendChild(info);11521153 if (e.hasTotp) {1154 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";1155 entryEl.appendChild(badge);1156 }11571158 entryEl.addEventListener("click", () => {1159 if (options.isSignup && options.onFillFields) {1160 1161 options.onFillFields(e.username ?? "", "");1162 dismiss();1163 } else {1164 onSelect(e.id);1165 dismiss();1166 }1167 });1168 results.appendChild(entryEl);1169 }11701171 1172 if (current.length > 0) {1173 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1174 }11751176 1177 const newEl = document.createElement("div");1178 newEl.className = "entry";1179 newEl.setAttribute("tabindex", "-1");1180 newEl.appendChild(makeEntryIcon(undefined, true));11811182 const newInfo = document.createElement("div"); newInfo.className = "entry-info";1183 const newTitle = document.createElement("div");1184 newTitle.className = "entry-title is-new";1185 newTitle.textContent = `New entry for ${sTitle}`;1186 const newSub = document.createElement("div");1187 newSub.className = "entry-sub";1188 newSub.textContent = sOrigin;1189 newInfo.append(newTitle, newSub);1190 newEl.appendChild(newInfo);1191 newEl.addEventListener("click", showCreateForm);1192 results.appendChild(newEl);11931194 1195 if (searchInput) {1196 requestAnimationFrame(() => searchInput?.focus());1197 }1198 }11991200 function setFocused(idx: number) {1201 const all = container.querySelectorAll<HTMLElement>(".entry");1202 all.forEach((el, i) => el.classList.toggle("focused", i === idx));1203 focusedIndex = idx;1204 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });1205 }12061207 async function showCreateForm() {1208 currentMode = "create";1209 container.innerHTML = "";12101211 1212 1213 1214 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;1215 if (!statusRes?.unlocked) {1216 renderLocked();1217 return;1218 }12191220 1221 1222 const sitePolicy = detectPasswordPolicy();12231224 const wrap = document.createElement("div");1225 wrap.className = "create-form-wrap";12261227 1228 const head = document.createElement("div"); head.className = "create-head";1229 const backBtn = document.createElement("button"); backBtn.className = "back-btn";1230 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>`;1231 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });1232 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";1233 head.append(backBtn, lbl);12341235 1236 const body = document.createElement("div"); body.className = "create-body";12371238 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {1239 const g = document.createElement("div"); g.className = "f-group";1240 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;1241 const inp = document.createElement("input"); inp.className = "f-input";1242 g.append(l, inp); return { g, inp };1243 }12441245 1246 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";1247 body.appendChild(tg.g);12481249 1250 const ug = fGroup("website");1251 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";1252 if (loginSubdomain) {1253 const h = document.createElement("div"); h.className = "url-hint";1254 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);1255 }1256 body.appendChild(ug.g);12571258 1259 const xg = fGroup("username / email");1260 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";1261 xg.inp.setAttribute("autocomplete", "off");1262 body.appendChild(xg.g);12631264 1265 const pg = fGroup("password");1266 const pwInp = document.createElement("input"); pwInp.className = "f-input";1267 pwInp.type = "text"; pwInp.value = prePassword || generatePasswordForSite(sitePolicy);1268 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';1269 pwInp.style.letterSpacing = "0.04em";1270 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");1271 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";1272 genBtn.innerHTML = genSvgStr() + " regen";1273 genBtn.addEventListener("click", () => { pwInp.value = generatePasswordForSite(sitePolicy); });1274 const pwRow = document.createElement("div"); pwRow.className = "f-row";1275 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);1276 body.appendChild(pg.g);12771278 1279 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";1280 saveBtn.textContent = "Save entry";1281 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";1282 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }1283 saveBtn.addEventListener("click", async () => {1284 const title = tg.inp.value.trim();1285 const pw = pwInp.value.trim();1286 const username = xg.inp.value.trim();1287 if (!title) {1288 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();1289 showFormError("Entry needs a title");1290 return;1291 }1292 if (!pw) {1293 pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus();1294 showFormError("Entry needs a password");1295 return;1296 }1297 errEl.style.display = "none";1298 saveBtn.disabled = true; saveBtn.textContent = "Saving…";1299 try {1300 const res = await chrome.runtime.sendMessage({1301 type: "ADD_ENTRY",1302 payload: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" },1303 });1304 if ((res as { ok?: boolean }).ok) {1305 1306 1307 1308 1309 1310 1311 options.onFillFields?.(username, pw);1312 showSuccess(title);1313 } else {1314 const errMsg = (res as { error?: string }).error === "vault locked"1315 ? "Vault locked — open Peach to save"1316 : "Could not save entry";1317 showFormError(errMsg);1318 saveBtn.disabled = false; saveBtn.textContent = "Save entry";1319 }1320 } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }1321 });1322 body.appendChild(saveBtn);1323 body.appendChild(errEl);13241325 wrap.append(head, body);1326 container.appendChild(wrap);1327 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });1328 }13291330 function showSuccess(title: string) {1331 currentMode = "success";1332 container.innerHTML = "";1333 const wrap = document.createElement("div"); wrap.className = "success-wrap";1334 const ring = document.createElement("div"); ring.className = "success-ring";1335 ring.innerHTML = checkSvgStr();1336 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;1337 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;1338 wrap.append(ring, t, s);1339 container.appendChild(wrap);1340 setTimeout(() => dismiss(), 1600);1341 }13421343 function flashActionBtn(btn: HTMLButtonElement) {1344 btn.classList.add("flashed");1345 setTimeout(() => btn.classList.remove("flashed"), 700);1346 }13471348 function renderSilverPlatter(entry: SelectorEntry) {1349 container.innerHTML = "";1350 searchInput = null;1351 focusedIndex = -1;1352 currentMode = "silver-platter";13531354 const domHdr = document.createElement("div");1355 domHdr.className = "domain-header";1356 const eyebrow = document.createElement("div");1357 eyebrow.className = "domain-eyebrow";1358 eyebrow.textContent = "Saved for this site";1359 const domName = document.createElement("div");1360 domName.className = "domain-name";1361 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1362 domHdr.append(eyebrow, domName);1363 container.appendChild(domHdr);13641365 const platter = document.createElement("div");1366 platter.className = "silver-platter";13671368 const titleEl = document.createElement("div");1369 titleEl.className = "sp-title";1370 titleEl.textContent = entry.title;1371 platter.appendChild(titleEl);13721373 if (entry.username) {1374 const userEl = document.createElement("div");1375 userEl.className = "sp-username";1376 userEl.textContent = entry.username;1377 platter.appendChild(userEl);1378 }13791380 const fillBtn = document.createElement("button");1381 fillBtn.className = "sp-fill-btn";1382 fillBtn.type = "button";1383 fillBtn.textContent = "Fill credentials";1384 fillBtn.addEventListener("click", () => {1385 if (fillBtn.disabled) return;1386 fillBtn.disabled = true;1387 fillBtn.classList.add("filled");1388 fillBtn.innerHTML = checkSvgStr();1389 1390 1391 1392 1393 onSelect(entry.id);1394 setTimeout(() => dismiss(), 400);1395 });1396 platter.appendChild(fillBtn);13971398 const actions = document.createElement("div");1399 actions.className = "sp-actions";14001401 const copyU = document.createElement("button");1402 copyU.className = "sp-action-btn";1403 copyU.type = "button";1404 copyU.title = "Copy username";1405 copyU.textContent = "Copy username";1406 copyU.disabled = !entry.username;1407 copyU.addEventListener("click", async (e) => {1408 e.stopPropagation();1409 if (!entry.username) return;1410 let alreadyWritten = false;1411 try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1412 1413 1414 1415 await chrome.runtime.sendMessage({1416 type: "COPY",1417 payload: { text: entry.username, label: "Username", alreadyWritten },1418 }).catch(() => {});1419 flashActionBtn(copyU);1420 });1421 actions.appendChild(copyU);14221423 const copyP = document.createElement("button");1424 copyP.className = "sp-action-btn";1425 copyP.type = "button";1426 copyP.title = "Copy password";1427 copyP.textContent = "Copy password";1428 copyP.addEventListener("click", async (e) => {1429 e.stopPropagation();1430 const resp = await chrome.runtime.sendMessage({1431 type: "GET_ENTRY_FOR_FILL",1432 payload: { id: entry.id },1433 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;1434 if (!resp?.ok || !resp.data?.password) return;1435 let alreadyWritten = false;1436 try { await navigator.clipboard.writeText(resp.data.password); alreadyWritten = true; } catch {}1437 await chrome.runtime.sendMessage({1438 type: "COPY",1439 payload: { text: resp.data.password, label: "Password", alreadyWritten },1440 }).catch(() => {});1441 flashActionBtn(copyP);1442 });1443 actions.appendChild(copyP);14441445 let trayEl: HTMLDivElement | null = null;1446 let trayInterval: ReturnType<typeof setInterval> | null = null;14471448 function closeTray() {1449 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }1450 trayEl?.remove();1451 trayEl = null;1452 }1453 1454 1455 cleanupHooks.push(closeTray);14561457 async function openTOTPTray() {1458 1459 1460 1461 const resp = await chrome.runtime.sendMessage({1462 type: "GET_ENTRY_FOR_FILL",1463 payload: { id: entry.id },1464 }).catch(() => null) as { ok: boolean; data?: { totp?: string } } | null;1465 const secret = resp?.ok ? resp.data?.totp : undefined;1466 if (!secret) return;14671468 trayEl = document.createElement("div");1469 trayEl.className = "sp-tray";14701471 const label = document.createElement("span");1472 label.className = "sp-tray-label";1473 label.textContent = "2FA";1474 trayEl.appendChild(label);14751476 const codeEl = document.createElement("div");1477 codeEl.className = "sp-totp-code";1478 trayEl.appendChild(codeEl);14791480 const ringNs = "http://www.w3.org/2000/svg";1481 const ring = document.createElementNS(ringNs, "svg");1482 ring.setAttribute("class", "sp-totp-ring");1483 ring.setAttribute("viewBox", "0 0 24 24");1484 const trackC = document.createElementNS(ringNs, "circle");1485 trackC.setAttribute("class", "ring-track");1486 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");1487 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");1488 const fillC = document.createElementNS(ringNs, "circle");1489 fillC.setAttribute("class", "ring-fill");1490 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");1491 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");1492 fillC.setAttribute("stroke-linecap", "round");1493 fillC.setAttribute("stroke-dasharray", "62.83");1494 ring.append(trackC, fillC);1495 trayEl.appendChild(ring);14961497 const copyBtn = document.createElement("button");1498 copyBtn.className = "sp-totp-copy";1499 copyBtn.type = "button";1500 copyBtn.textContent = "Copy";1501 trayEl.appendChild(copyBtn);15021503 function refresh() {1504 const { code, remaining } = generateTOTP(secret!);1505 1506 codeEl.textContent = code.length === 61507 ? `${code.slice(0, 3)} ${code.slice(3)}`1508 : code;1509 1510 1511 const fraction = (30 - remaining) / 30;1512 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));1513 }1514 refresh();1515 trayInterval = setInterval(refresh, 1000);15161517 copyBtn.addEventListener("click", async (e) => {1518 e.stopPropagation();1519 const { code } = generateTOTP(secret!);1520 let alreadyWritten = false;1521 try { await navigator.clipboard.writeText(code); alreadyWritten = true; } catch {}1522 await chrome.runtime.sendMessage({1523 type: "COPY",1524 payload: { text: code, label: "TOTP", alreadyWritten },1525 }).catch(() => {});1526 copyBtn.textContent = "Copied";1527 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);1528 });15291530 platter.appendChild(trayEl);1531 }15321533 if (entry.hasTotp) {1534 const moreBtn = document.createElement("button");1535 moreBtn.className = "sp-action-btn";1536 moreBtn.type = "button";1537 moreBtn.title = "Show 2FA code";1538 moreBtn.textContent = "···";1539 moreBtn.addEventListener("click", (e) => {1540 e.stopPropagation();1541 if (trayEl) {1542 closeTray();1543 moreBtn.classList.remove("flashed");1544 } else {1545 moreBtn.classList.add("flashed");1546 void openTOTPTray();1547 }1548 });1549 actions.appendChild(moreBtn);1550 }15511552 const closeBtn = document.createElement("button");1553 closeBtn.className = "sp-action-btn";1554 closeBtn.type = "button";1555 closeBtn.title = "Dismiss";1556 closeBtn.textContent = "✕";1557 closeBtn.addEventListener("click", (e) => {1558 e.stopPropagation();1559 closeTray();1560 dismiss(true);1561 });1562 actions.appendChild(closeBtn);15631564 platter.appendChild(actions);1565 container.appendChild(platter);1566 }15671568 function renderLocked() {1569 container.innerHTML = "";15701571 1572 const domHdr = document.createElement("div");1573 domHdr.className = "domain-header";1574 const eyebrow = document.createElement("div");1575 eyebrow.className = "domain-eyebrow";1576 eyebrow.textContent = "vault locked";1577 const domName = document.createElement("div");1578 domName.className = "domain-name";1579 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1580 domHdr.append(eyebrow, domName);1581 container.appendChild(domHdr);15821583 const wrap = document.createElement("div");1584 wrap.className = "locked-wrap";15851586 1587 const head = document.createElement("div"); head.className = "locked-head";1588 const icon = document.createElement("div"); icon.className = "locked-icon";1589 icon.innerHTML = keySvgStr();1590 const title = document.createElement("div"); title.className = "locked-title";1591 title.textContent = "unlock to autofill";1592 head.append(icon, title);15931594 1595 const pwInp = document.createElement("input");1596 pwInp.className = "locked-input";1597 pwInp.type = "password";1598 pwInp.placeholder = "master password";1599 pwInp.setAttribute("autocomplete", "current-password");16001601 1602 const errEl = document.createElement("div");1603 errEl.className = "locked-error";1604 errEl.style.display = "none";16051606 1607 const unlockBtn = document.createElement("button");1608 unlockBtn.className = "locked-btn";1609 unlockBtn.type = "button";1610 unlockBtn.textContent = "unlock";16111612 async function tryUnlock() {1613 const pw = pwInp.value.trim();1614 if (!pw) { pwInp.focus(); return; }1615 unlockBtn.disabled = true;1616 unlockBtn.textContent = "unlocking…";1617 errEl.style.display = "none";1618 try {1619 const res = await chrome.runtime.sendMessage({1620 type: "UNLOCK",1621 payload: { password: pw },1622 });1623 if ((res as { ok?: boolean }).ok) {1624 dismiss();1625 options.onUnlocked?.();1626 } else {1627 errEl.textContent = "incorrect password";1628 errEl.style.display = "block";1629 pwInp.value = "";1630 pwInp.focus();1631 unlockBtn.disabled = false;1632 unlockBtn.textContent = "unlock";1633 }1634 } catch {1635 errEl.textContent = "could not reach Peach";1636 errEl.style.display = "block";1637 unlockBtn.disabled = false;1638 unlockBtn.textContent = "unlock";1639 }1640 }16411642 pwInp.addEventListener("keydown", (e) => {1643 e.stopPropagation();1644 if (e.key === "Enter") void tryUnlock();1645 });1646 pwInp.addEventListener("keyup", (e) => e.stopPropagation());1647 pwInp.addEventListener("keypress", (e) => e.stopPropagation());1648 unlockBtn.addEventListener("click", () => void tryUnlock());16491650 wrap.append(head, pwInp, errEl, unlockBtn);1651 container.appendChild(wrap);16521653 1654 1655 1656 1657 1658 1659 const captureKeydown = (e: KeyboardEvent) => {1660 if (!host.isConnected) {1661 document.removeEventListener("keydown", captureKeydown, true);1662 return;1663 }1664 1665 if (e.ctrlKey || e.metaKey || e.altKey || e.key === "Tab") return;1666 1667 e.stopImmediatePropagation();1668 if (e.key === "Escape") { dismiss(true); return; }1669 if (e.key === "Enter") { void tryUnlock(); return; }1670 1671 1672 if (document.activeElement !== host) {1673 pwInp.focus({ preventScroll: true });1674 }1675 };1676 captureKeydownRef = captureKeydown;1677 document.addEventListener("keydown", captureKeydown, true);16781679 1680 1681 pwInp.focus({ preventScroll: true });1682 requestAnimationFrame(() => { if (host.isConnected) pwInp.focus({ preventScroll: true }); });1683 }16841685 function handleKeyDown(e: KeyboardEvent) {1686 if (options.locked) {1687 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1688 return;1689 }1690 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter") {1691 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1692 return;1693 }1694 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");1695 const count = allEntryEls.length;1696 if (document.activeElement === searchInput) {1697 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }1698 if (count === 0) return;1699 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;1700 }1701 if (count === 0) return;1702 switch (e.key) {1703 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;1704 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;1705 case "Enter": {1706 if (focusedIndex >= 0) {1707 const el = allEntryEls[focusedIndex] as HTMLElement;1708 const id = el.dataset.id;1709 if (id) { onSelect(id); dismiss(); }1710 else if (el.classList.contains("is-new") || !id) {1711 1712 showCreateForm();1713 }1714 }1715 break;1716 }1717 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;1718 }1719 }17201721 document.addEventListener("keydown", handleKeyDown);17221723 document.body.appendChild(host);17241725 function updatePosition() {1726 const r = anchorEl.getBoundingClientRect();1727 host.style.top = `${r.bottom + window.scrollY + 5}px`;1728 host.style.left = `${r.left + window.scrollX}px`;1729 host.style.width = `${Math.max(r.width, 260)}px`;1730 }1731 1732 1733 1734 1735 1736 1737 1738 let captureKeydownRef: ((e: KeyboardEvent) => void) | null = null;1739 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);17401741 if (options.locked) {1742 renderLocked();1743 } else if (options.isSignup && options.savedIdentities) {1744 1745 1746 renderIdentityHelper();1747 } else if (entries.length === 1 && !options.isSignup) {1748 1749 1750 renderSilverPlatter(entries[0]!);1751 } else {1752 renderList();1753 }17541755 let exitScheduled = false;1756 function dismiss(userAction = false) {1757 1758 1759 1760 if (exitScheduled) return;1761 exitScheduled = true;1762 1763 1764 1765 stopObserving();1766 document.removeEventListener("click", outsideClick, { capture: true });1767 document.removeEventListener("keydown", handleKeyDown);1768 if (captureKeydownRef) {1769 document.removeEventListener("keydown", captureKeydownRef, true);1770 captureKeydownRef = null;1771 }1772 for (const cleanup of cleanupHooks) {1773 try { cleanup(); } catch { }1774 }1775 if (userAction) options.onDismiss?.();1776 container.classList.add("is-exiting");1777 setTimeout(() => host.remove(), 130);1778 }17791780 function outsideClick(ev: MouseEvent) {1781 const path = ev.composedPath();1782 1783 1784 1785 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);1786 }1787 1788 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);17891790 return dismiss;1791}17921793export interface CardSelectorEntry {1794 id: string;1795 title: string;1796 cardholderName?: string;1797 last4?: string;1798 expMonth?: string;1799 expYear?: string;1800 network?: import("../shared/types").CardNetwork;1801}18021803interface CardSelectorOptions {1804 domain?: string;1805 1806 onFill: (entry: import("../shared/types").VaultEntry) => void;1807 1808 onDismiss?: () => void;1809}181018111812181318141815181618171818export function showCardSelector(1819 anchorEl: HTMLInputElement,1820 cards: CardSelectorEntry[],1821 options: CardSelectorOptions1822): () => void {1823 if (cards.length === 0) return () => {};1824 const card = cards[0]!;18251826 const host = document.createElement("div");1827 host.style.cssText = "position: absolute; z-index: 2147483647;";1828 host.style.setProperty("--peach-accent", accentHex);1829 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));1830 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));1831 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1832 const shadow = host.attachShadow({ mode: "closed" });18331834 const style = shadow.appendChild(document.createElement("style"));1835 style.textContent = SELECTOR_STYLES;18361837 const rect = anchorEl.getBoundingClientRect();1838 host.style.top = `${rect.bottom + window.scrollY + 5}px`;1839 host.style.left = `${rect.left + window.scrollX}px`;1840 host.style.width = `${Math.max(rect.width, 320)}px`;18411842 const container = shadow.appendChild(document.createElement("div"));1843 container.className = "peach-inline-popup";1844 container.setAttribute("data-theme", popupTheme);18451846 1847 const domHdr = document.createElement("div");1848 domHdr.className = "domain-header";1849 const eyebrow = document.createElement("div");1850 eyebrow.className = "domain-eyebrow";1851 eyebrow.textContent = "Pay with Peach";1852 const domName = document.createElement("div");1853 domName.className = "domain-name";1854 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1855 domHdr.append(eyebrow, domName);1856 container.appendChild(domHdr);18571858 const platter = document.createElement("div");1859 platter.className = "card-platter";18601861 if (card.network && card.network !== "unknown") {1862 const net = document.createElement("div");1863 net.className = "cp-network";1864 net.textContent = card.network;1865 platter.appendChild(net);1866 }18671868 const nick = document.createElement("div");1869 nick.className = "cp-nickname";1870 nick.textContent = card.title;1871 platter.appendChild(nick);18721873 if (card.last4) {1874 const l4 = document.createElement("div");1875 l4.className = "cp-last4";1876 l4.textContent = `•••• ${card.last4}`;1877 platter.appendChild(l4);1878 }18791880 const meta = document.createElement("div");1881 meta.className = "cp-meta";1882 if (card.cardholderName) {1883 const name = document.createElement("span");1884 name.textContent = card.cardholderName;1885 meta.appendChild(name);1886 }1887 if (card.expMonth && card.expYear) {1888 if (card.cardholderName) {1889 const sep = document.createElement("span");1890 sep.className = "cp-meta-sep";1891 sep.textContent = "·";1892 meta.appendChild(sep);1893 }1894 const exp = document.createElement("span");1895 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;1896 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;1897 meta.appendChild(exp);1898 }1899 if (meta.childElementCount > 0) platter.appendChild(meta);19001901 const fillWrap = document.createElement("div");1902 fillWrap.className = "cp-fill-wrap";1903 const fillBtn = document.createElement("button");1904 fillBtn.className = "sp-fill-btn";1905 fillBtn.type = "button";1906 fillBtn.textContent = "Fill card details";1907 fillBtn.addEventListener("click", async () => {1908 if (fillBtn.disabled) return;1909 fillBtn.disabled = true;1910 fillBtn.classList.add("filled");1911 fillBtn.innerHTML = checkSvgStr();1912 const resp = await chrome.runtime.sendMessage({1913 type: "GET_ENTRY_FOR_FILL",1914 payload: { id: card.id },1915 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;1916 if (resp?.ok && resp.data) options.onFill(resp.data);1917 setTimeout(() => dismiss(), 400);1918 });1919 fillWrap.appendChild(fillBtn);1920 platter.appendChild(fillWrap);19211922 1923 const actions = document.createElement("div");1924 actions.className = "sp-actions";19251926 function flashBtn(b: HTMLButtonElement) {1927 b.classList.add("flashed");1928 setTimeout(() => b.classList.remove("flashed"), 700);1929 }19301931 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {1932 const resp = await chrome.runtime.sendMessage({1933 type: "GET_ENTRY_FOR_FILL",1934 payload: { id: card.id },1935 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;1936 return resp?.ok && resp.data ? resp.data : null;1937 }19381939 const copyNum = document.createElement("button");1940 copyNum.className = "sp-action-btn";1941 copyNum.type = "button";1942 copyNum.title = "Copy card number";1943 copyNum.textContent = "Copy number";1944 copyNum.addEventListener("click", async (e) => {1945 e.stopPropagation();1946 const full = await fetchFullCard();1947 if (!full?.cardNumber) return;1948 let alreadyWritten = false;1949 try { await navigator.clipboard.writeText(full.cardNumber); alreadyWritten = true; } catch {}1950 await chrome.runtime.sendMessage({1951 type: "COPY",1952 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },1953 }).catch(() => {});1954 flashBtn(copyNum);1955 });1956 actions.appendChild(copyNum);19571958 const copyCvc = document.createElement("button");1959 copyCvc.className = "sp-action-btn";1960 copyCvc.type = "button";1961 copyCvc.title = "Copy CVC";1962 copyCvc.textContent = "Copy CVC";1963 copyCvc.addEventListener("click", async (e) => {1964 e.stopPropagation();1965 const full = await fetchFullCard();1966 if (!full?.cvc) return;1967 let alreadyWritten = false;1968 try { await navigator.clipboard.writeText(full.cvc); alreadyWritten = true; } catch {}1969 await chrome.runtime.sendMessage({1970 type: "COPY",1971 payload: { text: full.cvc, label: "CVC", alreadyWritten },1972 }).catch(() => {});1973 flashBtn(copyCvc);1974 });1975 actions.appendChild(copyCvc);19761977 const closeBtn = document.createElement("button");1978 closeBtn.className = "sp-action-btn";1979 closeBtn.type = "button";1980 closeBtn.title = "Dismiss";1981 closeBtn.textContent = "✕";1982 closeBtn.addEventListener("click", (e) => {1983 e.stopPropagation();1984 dismiss(true);1985 });1986 actions.appendChild(closeBtn);19871988 platter.appendChild(actions);1989 container.appendChild(platter);19901991 document.body.appendChild(host);1992 function updatePosition() {1993 const r = anchorEl.getBoundingClientRect();1994 host.style.top = `${r.bottom + window.scrollY + 5}px`;1995 host.style.left = `${r.left + window.scrollX}px`;1996 host.style.width = `${Math.max(r.width, 320)}px`;1997 }1998 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);19992000 function onKeyDown(e: KeyboardEvent) {2001 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }2002 }2003 document.addEventListener("keydown", onKeyDown);20042005 let exitScheduled = false;2006 function dismiss(userAction = false) {2007 if (exitScheduled) return;2008 exitScheduled = true;2009 stopObserving();2010 document.removeEventListener("click", outsideClick, { capture: true });2011 document.removeEventListener("keydown", onKeyDown);2012 if (userAction) options.onDismiss?.();2013 container.classList.add("is-exiting");2014 setTimeout(() => host.remove(), 130);2015 }20162017 function outsideClick(ev: MouseEvent) {2018 const path = ev.composedPath();2019 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);2020 }2021 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);20222023 return dismiss;2024}