123456789101112import { ACCENT_COLORS, type PasswordPolicy } 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 5051525354 onFillFields?: (username: string, password: string) => void;55 5657585960 locked?: boolean;61 62636465 onUnlocked?: () => void;66 6768697071 onDismiss?: () => void;72}7374function getDomainFromUrl(url: string): string {75 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }76}7778function keySvgStr(): string {79 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>`;80}8182function plusSvgStr(): string {83 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>`;84}8586function peachIconSvg(): string {87 88 89 90 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>`;91}9293function genSvgStr(): string {94 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>`;95}9697function checkSvgStr(): string {98 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>`;99}100101function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {102 const { hostname, origin } = location;103 const parts = hostname.split(".");104 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",105 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",106 "oidc", "federation", "saml", "cas", "ldap"];107 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());108 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;109 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);110 return { origin, title, loginSubdomain };111}112113114115116117118export function showRememberSiteToast(119 pageDomain: string,120 entryId: string,121): void {122 const host = document.createElement("div");123 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";124 host.style.setProperty("--peach-accent", accentHex);125 const shadow = host.attachShadow({ mode: "closed" });126127 const style = document.createElement("style");128 style.textContent = `129 .bar {130 display:flex;align-items:center;gap:10px;padding:10px 14px;131 background:#131109;border:1px solid rgba(240,232,216,0.09);132 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);133 font-family:Inter,sans-serif;white-space:nowrap;134 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;135 }136 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }137 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }138 .yes {139 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;140 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;141 }142 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }143 .no {144 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;145 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;146 }147 `;148 shadow.appendChild(style);149150 const bar = document.createElement("div"); bar.className = "bar";151 const txt = document.createElement("span"); txt.className = "txt";152 txt.textContent = `Remember ${pageDomain} for this entry?`;153 const yesBtn = document.createElement("button"); yesBtn.className = "yes";154 yesBtn.textContent = "Remember";155 yesBtn.addEventListener("click", async () => {156 await chrome.runtime.sendMessage({157 type: "ADD_ADDITIONAL_URL",158 payload: { id: entryId, url: location.origin },159 });160 host.remove();161 });162 const noBtn = document.createElement("button"); noBtn.className = "no";163 noBtn.textContent = "Not now";164 noBtn.addEventListener("click", () => host.remove());165 bar.append(txt, yesBtn, noBtn);166 shadow.appendChild(bar);167 document.body.appendChild(host);168 setTimeout(() => host.remove(), 9000);169}170171function generatePasswordForSite(policy: PasswordPolicy = {}): string {172 return generateSmartPassword(policy);173}174175export function observeAnchorPosition(176 anchorEl: HTMLElement,177 onUpdate: () => void178): () => void {179 let rafId = 0;180 let burstUntil = 0;181182 const runUpdate = () => {183 rafId = 0;184 onUpdate();185 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);186 };187 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };188 const scheduleBurst = (ms = 220) => {189 burstUntil = Math.max(burstUntil, performance.now() + ms);190 scheduleUpdate();191 };192193 const resizeObserver = new ResizeObserver(() => scheduleBurst());194 resizeObserver.observe(anchorEl);195 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);196197 const mutationObserver = new MutationObserver(() => scheduleBurst());198 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });199 if (anchorEl.parentElement) {200 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });201 }202203 const onScroll = () => scheduleUpdate();204 window.addEventListener("scroll", onScroll, true);205 window.addEventListener("resize", onScroll);206 window.visualViewport?.addEventListener("resize", onScroll);207 window.visualViewport?.addEventListener("scroll", onScroll);208 anchorEl.addEventListener("focus", () => scheduleBurst());209 anchorEl.addEventListener("input", () => scheduleBurst());210211 scheduleBurst(260);212213 return () => {214 if (rafId) cancelAnimationFrame(rafId);215 resizeObserver.disconnect();216 mutationObserver.disconnect();217 window.removeEventListener("scroll", onScroll, true);218 window.removeEventListener("resize", onScroll);219 window.visualViewport?.removeEventListener("resize", onScroll);220 window.visualViewport?.removeEventListener("scroll", onScroll);221 };222}223224export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {225 const button = document.createElement("button");226 button.type = "button";227 button.setAttribute("aria-label", "Peach autofill");228 button.title = "Fill with Peach";229 button.innerHTML = peachIconSvg();230 231 232 233 234 button.style.cssText = `235 position: fixed; z-index: 2147483646;236 width: 26px; height: 26px; cursor: pointer;237 pointer-events: auto; border: none; padding: 3px;238 display: inline-flex; align-items: center; justify-content: center;239 background: transparent; border-radius: 999px;240 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;241 opacity: 0;242 `;243 button.addEventListener("mouseenter", () => {244 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;245 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;246 button.style.transform = "scale(1.08)";247 });248 button.addEventListener("mouseleave", () => {249 button.style.background = "transparent";250 button.style.boxShadow = "none";251 button.style.transform = "none";252 });253 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });254 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });255256 document.body.appendChild(button);257258 function updatePosition() {259 const rect = field.getBoundingClientRect();260 if (rect.width === 0 || rect.height === 0) {261 button.style.opacity = "0"; button.style.pointerEvents = "none";262 return;263 }264 const sz = 26;265 const inset = Math.max(5, Math.min(10, rect.height * 0.22));266 const top = rect.top + rect.height / 2 - sz / 2;267 268 269 const minLeft = rect.left + 4;270 const left = Math.max(minLeft, rect.right - sz - inset);271 272 if (rect.width < 40) {273 button.style.opacity = "0"; button.style.pointerEvents = "none";274 return;275 }276 button.style.top = `${top}px`;277 button.style.left = `${left}px`;278 button.style.opacity = "1"; button.style.pointerEvents = "auto";279 }280 updatePosition();281 requestAnimationFrame(() => { button.style.opacity = "1"; });282 const stopObserving = observeAnchorPosition(field, updatePosition);283 return () => { stopObserving(); button.remove(); };284}285286287288289290291292const SELECTOR_STYLES = `293 :host { all: initial; }294 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }295296 .peach-inline-popup[data-theme="dark"] {297 --peach-bg: #1a1410;298 --peach-bg-card: #221c16;299 --peach-text: #f5f0ea;300 --peach-text-dim: #8a7a6a;301 --peach-text-muted: #4a3a2a;302 --peach-border: rgba(255,255,255,0.07);303 --peach-input-bg: rgba(255,255,255,0.05);304 --peach-input-border: rgba(255,255,255,0.08);305 }306 .peach-inline-popup[data-theme="light"] {307 --peach-bg: #fdf8f4;308 --peach-bg-card: #f5ede4;309 --peach-text: #1c1310;310 --peach-text-dim: #6b5a4a;311 --peach-text-muted: #a89080;312 --peach-border: rgba(0,0,0,0.08);313 --peach-input-bg: rgba(0,0,0,0.04);314 --peach-input-border: rgba(0,0,0,0.10);315 }316317 .peach-inline-popup {318 background: var(--peach-bg);319 border: 1px solid var(--peach-border-accent, var(--peach-border));320 border-radius: 14px;321 overflow: hidden;322 box-shadow:323 0 8px 40px var(--peach-glow, transparent),324 0 2px 12px rgba(0,0,0,0.5);325 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;326 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;327 }328 /* Light theme: shadow against a light page is too strong otherwise — drop both329 the accent glow and the dark drop-shadow opacity to keep the popup elegant. */330 .peach-inline-popup[data-theme="light"] {331 box-shadow:332 0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),333 0 2px 12px rgba(0,0,0,0.12);334 }335 .peach-inline-popup.is-exiting {336 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;337 pointer-events: none;338 }339340 @keyframes peach-enter {341 0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }342 65% { opacity: 1; filter: blur(0); }343 100% { opacity: 1; transform: translateY(0) scale(1); }344 }345 @keyframes peach-exit {346 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }347 100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }348 }349 @keyframes peach-slide-in {350 from { opacity: 0; transform: translateX(12px); }351 to { opacity: 1; transform: translateX(0); }352 }353 @keyframes peach-slide-back {354 from { opacity: 0; transform: translateX(-12px); }355 to { opacity: 1; transform: translateX(0); }356 }357 .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }358 .peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }359360 /* Domain header */361 .domain-header {362 padding: 9px 13px 7px;363 border-bottom: 1px solid var(--peach-border);364 display: flex; align-items: center; justify-content: space-between;365 }366 .domain-eyebrow {367 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;368 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;369 }370 .domain-name {371 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);372 font-family: "IBM Plex Mono", monospace;373 }374375 /* Search */376 .search-wrap {377 padding: 9px 12px 7px;378 border-bottom: 1px solid var(--peach-border);379 }380 .search {381 width: 100%; background: var(--peach-input-bg);382 border: 1px solid var(--peach-input-border);383 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;384 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;385 transition: border-color 120ms, box-shadow 120ms;386 }387 .search:focus {388 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);389 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);390 }391 .search::placeholder { color: var(--peach-text-muted); }392393 /* Results */394 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }395 .results::-webkit-scrollbar { display: none; }396397 /* Entry rows */398 .entry {399 display: flex; align-items: center; padding: 8px 12px;400 cursor: pointer; border-bottom: 1px solid var(--peach-border);401 transition: background 70ms; position: relative; outline: none;402 }403 .entry:last-child { border-bottom: none; }404 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }405 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }406 .entry.focused::before {407 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;408 width: 2px; border-radius: 2px;409 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);410 }411412 .entry-icon {413 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;414 background: var(--peach-input-bg); border: 1px solid var(--peach-border);415 display: flex; align-items: center; justify-content: center;416 overflow: hidden; margin-right: 10px;417 }418 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }419 .entry-icon.is-new {420 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);421 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);422 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);423 }424425 .entry-info { flex: 1; min-width: 0; }426 .entry-title {427 font-size: 12.5px; color: var(--peach-text);428 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;429 line-height: 1.3;430 }431 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }432 .entry-sub {433 font-size: 10px; color: var(--peach-text-dim);434 font-family: "IBM Plex Mono", monospace;435 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;436 margin-top: 1px;437 }438439 .totp-badge {440 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;441 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);442 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);443 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;444 }445446 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }447448 .empty-hint {449 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);450 font-family: "IBM Plex Mono", monospace;451 }452453 /* Create form */454 .create-form-wrap {455 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;456 }457 .create-head {458 display: flex; align-items: center; gap: 8px;459 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);460 }461 .back-btn {462 background: transparent; border: none; cursor: pointer;463 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;464 display: flex; align-items: center; line-height: 0;465 transition: color 100ms, background 100ms;466 }467 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }468 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }469 .create-body { padding: 12px; display: grid; gap: 9px; }470 .f-group { display: grid; gap: 3px; }471 .f-label {472 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;473 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;474 }475 .f-input {476 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);477 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;478 font-size: 11px; font-family: "IBM Plex Mono", monospace;479 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;480 }481 .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); }482 .f-input::placeholder { color: var(--peach-text-muted); }483 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }484 .f-row { display: flex; gap: 6px; }485 .f-row .f-input { flex: 1; min-width: 0; }486 .gen-btn {487 padding: 0 10px; border-radius: 7px; border: none;488 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);489 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);490 font-size: 10px; font-family: "IBM Plex Mono", monospace;491 cursor: pointer; white-space: nowrap; flex-shrink: 0;492 display: flex; align-items: center; gap: 4px;493 transition: background 100ms;494 }495 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }496 .save-btn {497 width: 100%; padding: 8px; border-radius: 8px; border: none;498 background: var(--peach-accent); color: #fff; font-size: 12px;499 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;500 transition: background 120ms, box-shadow 120ms, transform 80ms;501 letter-spacing: 0.01em;502 }503 .save-btn:hover {504 background: color-mix(in srgb, var(--peach-accent) 82%, #000);505 box-shadow: 0 4px 20px var(--peach-glow, transparent);506 }507 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }508 .url-hint {509 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);510 font-family: "IBM Plex Mono", monospace; font-style: italic;511 }512513 /* Success */514 .success-wrap {515 display: flex; flex-direction: column; align-items: center;516 padding: 24px 16px 20px; gap: 8px;517 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;518 }519 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }520 .success-ring {521 width: 40px; height: 40px; border-radius: 50%;522 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);523 display: flex; align-items: center; justify-content: center;524 }525 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }526 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }527528 /* Silver platter (State 1 — single match) */529 .silver-platter {530 padding: 14px 14px 12px;531 display: flex; flex-direction: column; gap: 6px;532 }533 .sp-title {534 font-size: 14px; font-weight: 500; color: var(--peach-text);535 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;536 line-height: 1.25;537 }538 .sp-username {539 font-size: 12px; color: var(--peach-text-dim);540 font-family: "IBM Plex Mono", monospace;541 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;542 margin-bottom: 6px;543 }544 .sp-fill-btn {545 width: 100%; padding: 11px; border-radius: 9px; border: none;546 background: var(--peach-accent); color: #fff;547 font-size: 13px; font-weight: 500; cursor: pointer;548 font-family: Inter, sans-serif; letter-spacing: 0.01em;549 display: flex; align-items: center; justify-content: center; min-height: 38px;550 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;551 }552 .sp-fill-btn:hover {553 filter: brightness(1.1);554 box-shadow: 0 4px 20px var(--peach-glow, transparent);555 transform: translateY(-1px);556 }557 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }558 .sp-fill-btn.filled {559 background: #16a34a; transform: none; filter: none;560 box-shadow: 0 4px 16px rgba(22,163,74,0.35);561 cursor: default;562 }563 .sp-fill-btn:disabled { cursor: default; }564 .sp-actions {565 display: flex; gap: 4px; justify-content: space-between; align-items: center;566 margin-top: 4px;567 }568 .sp-action-btn {569 flex: 1; height: 28px; border: none; border-radius: 6px;570 background: transparent; color: var(--peach-text-dim);571 font-size: 12px; cursor: pointer;572 transition: background 120ms ease, color 120ms ease;573 font-family: Inter, sans-serif; letter-spacing: 0.04em;574 display: inline-flex; align-items: center; justify-content: center; gap: 4px;575 }576 .sp-action-btn:hover:not(:disabled) {577 background: color-mix(in srgb, var(--peach-text) 5%, transparent);578 color: var(--peach-text);579 }580 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }581 .sp-action-btn.flashed {582 color: var(--peach-accent);583 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);584 }585586 /* TOTP tray — shown below the silver-platter secondary actions */587 .sp-tray {588 margin-top: 8px;589 padding: 10px 12px;590 border-radius: 9px;591 background: var(--peach-input-bg);592 border: 1px solid var(--peach-input-border);593 display: flex; align-items: center; gap: 12px;594 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;595 }596 .sp-tray-label {597 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;598 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;599 flex-shrink: 0;600 }601 .sp-totp-code {602 font-size: 18px; font-weight: 500;603 color: var(--peach-text);604 font-family: "IBM Plex Mono", monospace;605 letter-spacing: 0.12em;606 flex: 1;607 text-align: center;608 }609 .sp-totp-ring {610 width: 22px; height: 22px; flex-shrink: 0;611 }612 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }613 .sp-totp-ring .ring-fill {614 stroke: var(--peach-accent);615 transition: stroke-dashoffset 1s linear;616 transform: rotate(-90deg);617 transform-origin: center;618 }619 .sp-totp-copy {620 background: transparent; border: none; cursor: pointer;621 padding: 4px 8px; border-radius: 5px;622 color: var(--peach-text-dim); font-size: 11px;623 font-family: Inter, sans-serif;624 transition: background 120ms, color 120ms;625 }626 .sp-totp-copy:hover {627 background: color-mix(in srgb, var(--peach-text) 5%, transparent);628 color: var(--peach-text);629 }630631 /* Card silver platter (State 7) — reuses .sp-* primary action classes */632 .card-platter {633 padding: 12px 14px 10px;634 display: flex; flex-direction: column; gap: 4px;635 }636 .cp-network {637 font-size: 10px; font-weight: 600;638 color: var(--peach-accent);639 letter-spacing: 0.08em; text-transform: uppercase;640 font-family: Inter, sans-serif;641 }642 .cp-nickname {643 font-size: 14px; font-weight: 500; color: var(--peach-text);644 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;645 line-height: 1.2;646 }647 .cp-last4 {648 font-size: 12px; color: var(--peach-text-dim);649 font-family: "IBM Plex Mono", monospace;650 letter-spacing: 0.04em;651 margin-top: 2px;652 }653 .cp-meta {654 font-size: 11px; color: var(--peach-text-dim);655 font-family: "IBM Plex Mono", monospace;656 margin-top: 2px;657 display: flex; gap: 10px; align-items: center;658 }659 .cp-meta-sep { opacity: 0.5; }660 .cp-fill-wrap { margin-top: 10px; }661662 /* Locked state */663 .locked-wrap {664 display: flex; flex-direction: column; align-items: stretch;665 padding: 14px 13px 13px; gap: 9px;666 }667 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }668 .locked-icon {669 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;670 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);671 display: flex; align-items: center; justify-content: center;672 }673 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }674 .locked-input {675 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);676 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;677 font-size: 11px; font-family: "IBM Plex Mono", monospace;678 outline: none; transition: border-color 120ms, box-shadow 120ms;679 }680 .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); }681 .locked-input::placeholder { color: var(--peach-text-muted); }682 .locked-btn {683 width: 100%; padding: 8px; border-radius: 8px; border: none;684 background: var(--peach-accent); color: #fff; font-size: 12px;685 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;686 transition: background 120ms;687 letter-spacing: 0.01em;688 box-shadow: 0 2px 16px var(--peach-glow, transparent);689 animation: peach-pulse-glow 2.4s ease-in-out infinite;690 }691 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }692 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }693 .locked-error {694 font-size: 10px; color: rgba(220,80,60,0.85);695 font-family: "IBM Plex Mono", monospace;696 }697698 @keyframes peach-pulse-glow {699 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }700 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),701 0 0 48px var(--peach-glow, transparent); }702 }703`;704705export function showAutofillSelector(706 anchorEl: HTMLInputElement,707 entries: SelectorEntry[],708 onSelect: (id: string) => void,709 options: SelectorOptions = {}710): () => void {711 const host = document.createElement("div");712 host.style.cssText = "position: absolute; z-index: 2147483647;";713 host.style.setProperty("--peach-accent", accentHex);714 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));715 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));716 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));717 const shadow = host.attachShadow({ mode: "closed" });718719 const style = shadow.appendChild(document.createElement("style"));720 style.textContent = SELECTOR_STYLES;721722 const rect = anchorEl.getBoundingClientRect();723 host.style.top = `${rect.bottom + window.scrollY + 5}px`;724 host.style.left = `${rect.left + window.scrollX}px`;725 726 727 host.style.width = `${Math.max(rect.width, 320)}px`;728729 const container = shadow.appendChild(document.createElement("div"));730 container.className = "peach-inline-popup";731 container.setAttribute("data-theme", popupTheme);732733 const searchableEntries = options.searchableEntries ?? [];734 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();735736 let query = "";737 let focusedIndex = -1;738 let currentMode: "list" | "create" | "success" | "silver-platter" = "list";739 let searchInput: HTMLInputElement | null = null;740 741 742 const cleanupHooks: Array<() => void> = [];743744 745 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";746 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");747 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";748749 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {750 const seen = new Set<string>();751 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });752 }753754 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {755 const q = value.trim().toLowerCase();756 const deduped = uniqueEntries(source);757 if (!q) return deduped.slice(0, 8);758 return deduped.filter(e =>759 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)760 ).slice(0, 12);761 }762763 function activeEntries(): SelectorEntry[] {764 765 if (query.trim()) {766 const merged = uniqueEntries([...entries, ...searchableEntries]);767 return filterEntries(merged, query);768 }769 770 if (entries.length > 0) return entries;771 return filterEntries(searchableEntries, "").slice(0, 8);772 }773774 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {775 const icon = document.createElement("div");776 icon.className = `entry-icon${isNew ? " is-new" : ""}`;777 if (isNew) {778 icon.innerHTML = plusSvgStr();779 } else {780 781 782 783 void url;784 icon.innerHTML = keySvgStr();785 }786 return icon;787 }788789 function renderList() {790 container.innerHTML = "";791 focusedIndex = -1;792 searchInput = null;793794 const current = activeEntries();795796 797 const domHdr = document.createElement("div");798 domHdr.className = "domain-header";799 const eyebrow = document.createElement("div");800 eyebrow.className = "domain-eyebrow";801 eyebrow.textContent = options.isSignup802 ? "Sign up as"803 : entries.length > 0 ? "Saved for this site" : "No saved login";804 const domName = document.createElement("div");805 domName.className = "domain-name";806 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");807 domHdr.append(eyebrow, domName);808 container.appendChild(domHdr);809810 811 const sw = document.createElement("div"); sw.className = "search-wrap";812 searchInput = document.createElement("input");813 searchInput.className = "search";814 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";815 searchInput.value = query;816 searchInput.addEventListener("input", () => {817 query = searchInput?.value ?? "";818 renderList();819 searchInput?.focus();820 searchInput?.setSelectionRange(query.length, query.length);821 });822 sw.appendChild(searchInput);823 container.appendChild(sw);824825 826 const results = document.createElement("div");827 results.className = "results";828 container.appendChild(results);829830 if (current.length === 0 && entries.length > 0) {831 832 }833834 for (let i = 0; i < current.length; i++) {835 const e = current[i]!;836 const entryEl = document.createElement("div");837 entryEl.className = "entry";838 entryEl.dataset.id = e.id;839 entryEl.dataset.index = String(i);840 entryEl.setAttribute("tabindex", "-1");841842 entryEl.appendChild(makeEntryIcon(e.url));843844 const info = document.createElement("div"); info.className = "entry-info";845 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;846 const subEl = document.createElement("div"); subEl.className = "entry-sub";847 const domain = e.url ? getDomainFromUrl(e.url) : "";848 subEl.textContent = domain || e.username || "";849 info.append(titleEl, subEl);850 entryEl.appendChild(info);851852 if (e.hasTotp) {853 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";854 entryEl.appendChild(badge);855 }856857 entryEl.addEventListener("click", () => {858 if (options.isSignup && options.onFillFields) {859 860 options.onFillFields(e.username ?? "", "");861 dismiss();862 } else {863 onSelect(e.id);864 dismiss();865 }866 });867 results.appendChild(entryEl);868 }869870 871 if (current.length > 0) {872 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));873 }874875 876 const newEl = document.createElement("div");877 newEl.className = "entry";878 newEl.setAttribute("tabindex", "-1");879 newEl.appendChild(makeEntryIcon(undefined, true));880881 const newInfo = document.createElement("div"); newInfo.className = "entry-info";882 const newTitle = document.createElement("div");883 newTitle.className = "entry-title is-new";884 newTitle.textContent = `New entry for ${sTitle}`;885 const newSub = document.createElement("div");886 newSub.className = "entry-sub";887 newSub.textContent = sOrigin;888 newInfo.append(newTitle, newSub);889 newEl.appendChild(newInfo);890 newEl.addEventListener("click", showCreateForm);891 results.appendChild(newEl);892893 894 if (searchInput) {895 requestAnimationFrame(() => searchInput?.focus());896 }897 }898899 function setFocused(idx: number) {900 const all = container.querySelectorAll<HTMLElement>(".entry");901 all.forEach((el, i) => el.classList.toggle("focused", i === idx));902 focusedIndex = idx;903 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });904 }905906 async function showCreateForm() {907 currentMode = "create";908 container.innerHTML = "";909910 911 912 913 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;914 if (!statusRes?.unlocked) {915 renderLocked();916 return;917 }918919 920 921 const sitePolicy = detectPasswordPolicy();922923 const wrap = document.createElement("div");924 wrap.className = "create-form-wrap";925926 927 const head = document.createElement("div"); head.className = "create-head";928 const backBtn = document.createElement("button"); backBtn.className = "back-btn";929 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>`;930 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });931 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";932 head.append(backBtn, lbl);933934 935 const body = document.createElement("div"); body.className = "create-body";936937 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {938 const g = document.createElement("div"); g.className = "f-group";939 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;940 const inp = document.createElement("input"); inp.className = "f-input";941 g.append(l, inp); return { g, inp };942 }943944 945 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";946 body.appendChild(tg.g);947948 949 const ug = fGroup("website");950 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";951 if (loginSubdomain) {952 const h = document.createElement("div"); h.className = "url-hint";953 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);954 }955 body.appendChild(ug.g);956957 958 const xg = fGroup("username / email");959 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";960 xg.inp.setAttribute("autocomplete", "off");961 body.appendChild(xg.g);962963 964 const pg = fGroup("password");965 const pwInp = document.createElement("input"); pwInp.className = "f-input";966 pwInp.type = "text"; pwInp.value = prePassword || generatePasswordForSite(sitePolicy);967 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';968 pwInp.style.letterSpacing = "0.04em";969 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");970 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";971 genBtn.innerHTML = genSvgStr() + " regen";972 genBtn.addEventListener("click", () => { pwInp.value = generatePasswordForSite(sitePolicy); });973 const pwRow = document.createElement("div"); pwRow.className = "f-row";974 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);975 body.appendChild(pg.g);976977 978 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";979 saveBtn.textContent = "Save entry";980 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";981 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }982 saveBtn.addEventListener("click", async () => {983 const title = tg.inp.value.trim();984 const pw = pwInp.value.trim();985 const username = xg.inp.value.trim();986 if (!title) {987 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();988 showFormError("Entry needs a title");989 return;990 }991 if (!pw) {992 pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus();993 showFormError("Entry needs a password");994 return;995 }996 errEl.style.display = "none";997 saveBtn.disabled = true; saveBtn.textContent = "Saving…";998 try {999 const res = await chrome.runtime.sendMessage({1000 type: "ADD_ENTRY",1001 payload: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" },1002 });1003 if ((res as { ok?: boolean }).ok) {1004 1005 1006 1007 1008 1009 1010 options.onFillFields?.(username, pw);1011 showSuccess(title);1012 } else {1013 const errMsg = (res as { error?: string }).error === "vault locked"1014 ? "Vault locked — open Peach to save"1015 : "Could not save entry";1016 showFormError(errMsg);1017 saveBtn.disabled = false; saveBtn.textContent = "Save entry";1018 }1019 } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }1020 });1021 body.appendChild(saveBtn);1022 body.appendChild(errEl);10231024 wrap.append(head, body);1025 container.appendChild(wrap);1026 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });1027 }10281029 function showSuccess(title: string) {1030 currentMode = "success";1031 container.innerHTML = "";1032 const wrap = document.createElement("div"); wrap.className = "success-wrap";1033 const ring = document.createElement("div"); ring.className = "success-ring";1034 ring.innerHTML = checkSvgStr();1035 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;1036 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;1037 wrap.append(ring, t, s);1038 container.appendChild(wrap);1039 setTimeout(() => dismiss(), 1600);1040 }10411042 function flashActionBtn(btn: HTMLButtonElement) {1043 btn.classList.add("flashed");1044 setTimeout(() => btn.classList.remove("flashed"), 700);1045 }10461047 function renderSilverPlatter(entry: SelectorEntry) {1048 container.innerHTML = "";1049 searchInput = null;1050 focusedIndex = -1;1051 currentMode = "silver-platter";10521053 const domHdr = document.createElement("div");1054 domHdr.className = "domain-header";1055 const eyebrow = document.createElement("div");1056 eyebrow.className = "domain-eyebrow";1057 eyebrow.textContent = "Saved for this site";1058 const domName = document.createElement("div");1059 domName.className = "domain-name";1060 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1061 domHdr.append(eyebrow, domName);1062 container.appendChild(domHdr);10631064 const platter = document.createElement("div");1065 platter.className = "silver-platter";10661067 const titleEl = document.createElement("div");1068 titleEl.className = "sp-title";1069 titleEl.textContent = entry.title;1070 platter.appendChild(titleEl);10711072 if (entry.username) {1073 const userEl = document.createElement("div");1074 userEl.className = "sp-username";1075 userEl.textContent = entry.username;1076 platter.appendChild(userEl);1077 }10781079 const fillBtn = document.createElement("button");1080 fillBtn.className = "sp-fill-btn";1081 fillBtn.type = "button";1082 fillBtn.textContent = "Fill credentials";1083 fillBtn.addEventListener("click", () => {1084 if (fillBtn.disabled) return;1085 fillBtn.disabled = true;1086 fillBtn.classList.add("filled");1087 fillBtn.innerHTML = checkSvgStr();1088 1089 1090 1091 1092 onSelect(entry.id);1093 setTimeout(() => dismiss(), 400);1094 });1095 platter.appendChild(fillBtn);10961097 const actions = document.createElement("div");1098 actions.className = "sp-actions";10991100 const copyU = document.createElement("button");1101 copyU.className = "sp-action-btn";1102 copyU.type = "button";1103 copyU.title = "Copy username";1104 copyU.textContent = "copy ⓤ";1105 copyU.disabled = !entry.username;1106 copyU.addEventListener("click", async (e) => {1107 e.stopPropagation();1108 if (!entry.username) return;1109 let alreadyWritten = false;1110 try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1111 1112 1113 1114 await chrome.runtime.sendMessage({1115 type: "COPY",1116 payload: { text: entry.username, label: "Username", alreadyWritten },1117 }).catch(() => {});1118 flashActionBtn(copyU);1119 });1120 actions.appendChild(copyU);11211122 const copyP = document.createElement("button");1123 copyP.className = "sp-action-btn";1124 copyP.type = "button";1125 copyP.title = "Copy password";1126 copyP.textContent = "copy ⓟ";1127 copyP.addEventListener("click", async (e) => {1128 e.stopPropagation();1129 const resp = await chrome.runtime.sendMessage({1130 type: "GET_ENTRY_FOR_FILL",1131 payload: { id: entry.id },1132 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;1133 if (!resp?.ok || !resp.data?.password) return;1134 let alreadyWritten = false;1135 try { await navigator.clipboard.writeText(resp.data.password); alreadyWritten = true; } catch {}1136 await chrome.runtime.sendMessage({1137 type: "COPY",1138 payload: { text: resp.data.password, label: "Password", alreadyWritten },1139 }).catch(() => {});1140 flashActionBtn(copyP);1141 });1142 actions.appendChild(copyP);11431144 let trayEl: HTMLDivElement | null = null;1145 let trayInterval: ReturnType<typeof setInterval> | null = null;11461147 function closeTray() {1148 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }1149 trayEl?.remove();1150 trayEl = null;1151 }1152 1153 1154 cleanupHooks.push(closeTray);11551156 async function openTOTPTray() {1157 1158 1159 1160 const resp = await chrome.runtime.sendMessage({1161 type: "GET_ENTRY_FOR_FILL",1162 payload: { id: entry.id },1163 }).catch(() => null) as { ok: boolean; data?: { totp?: string } } | null;1164 const secret = resp?.ok ? resp.data?.totp : undefined;1165 if (!secret) return;11661167 trayEl = document.createElement("div");1168 trayEl.className = "sp-tray";11691170 const label = document.createElement("span");1171 label.className = "sp-tray-label";1172 label.textContent = "2FA";1173 trayEl.appendChild(label);11741175 const codeEl = document.createElement("div");1176 codeEl.className = "sp-totp-code";1177 trayEl.appendChild(codeEl);11781179 const ringNs = "http://www.w3.org/2000/svg";1180 const ring = document.createElementNS(ringNs, "svg");1181 ring.setAttribute("class", "sp-totp-ring");1182 ring.setAttribute("viewBox", "0 0 24 24");1183 const trackC = document.createElementNS(ringNs, "circle");1184 trackC.setAttribute("class", "ring-track");1185 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");1186 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");1187 const fillC = document.createElementNS(ringNs, "circle");1188 fillC.setAttribute("class", "ring-fill");1189 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");1190 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");1191 fillC.setAttribute("stroke-linecap", "round");1192 fillC.setAttribute("stroke-dasharray", "62.83");1193 ring.append(trackC, fillC);1194 trayEl.appendChild(ring);11951196 const copyBtn = document.createElement("button");1197 copyBtn.className = "sp-totp-copy";1198 copyBtn.type = "button";1199 copyBtn.textContent = "Copy";1200 trayEl.appendChild(copyBtn);12011202 function refresh() {1203 const { code, remaining } = generateTOTP(secret!);1204 1205 codeEl.textContent = code.length === 61206 ? `${code.slice(0, 3)} ${code.slice(3)}`1207 : code;1208 1209 1210 const fraction = (30 - remaining) / 30;1211 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));1212 }1213 refresh();1214 trayInterval = setInterval(refresh, 1000);12151216 copyBtn.addEventListener("click", async (e) => {1217 e.stopPropagation();1218 const { code } = generateTOTP(secret!);1219 let alreadyWritten = false;1220 try { await navigator.clipboard.writeText(code); alreadyWritten = true; } catch {}1221 await chrome.runtime.sendMessage({1222 type: "COPY",1223 payload: { text: code, label: "TOTP", alreadyWritten },1224 }).catch(() => {});1225 copyBtn.textContent = "Copied";1226 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);1227 });12281229 platter.appendChild(trayEl);1230 }12311232 if (entry.hasTotp) {1233 const moreBtn = document.createElement("button");1234 moreBtn.className = "sp-action-btn";1235 moreBtn.type = "button";1236 moreBtn.title = "Show 2FA code";1237 moreBtn.textContent = "···";1238 moreBtn.addEventListener("click", (e) => {1239 e.stopPropagation();1240 if (trayEl) {1241 closeTray();1242 moreBtn.classList.remove("flashed");1243 } else {1244 moreBtn.classList.add("flashed");1245 void openTOTPTray();1246 }1247 });1248 actions.appendChild(moreBtn);1249 }12501251 const closeBtn = document.createElement("button");1252 closeBtn.className = "sp-action-btn";1253 closeBtn.type = "button";1254 closeBtn.title = "Dismiss";1255 closeBtn.textContent = "✕";1256 closeBtn.addEventListener("click", (e) => {1257 e.stopPropagation();1258 closeTray();1259 dismiss(true);1260 });1261 actions.appendChild(closeBtn);12621263 platter.appendChild(actions);1264 container.appendChild(platter);1265 }12661267 function renderLocked() {1268 container.innerHTML = "";12691270 1271 const domHdr = document.createElement("div");1272 domHdr.className = "domain-header";1273 const eyebrow = document.createElement("div");1274 eyebrow.className = "domain-eyebrow";1275 eyebrow.textContent = "vault locked";1276 const domName = document.createElement("div");1277 domName.className = "domain-name";1278 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1279 domHdr.append(eyebrow, domName);1280 container.appendChild(domHdr);12811282 const wrap = document.createElement("div");1283 wrap.className = "locked-wrap";12841285 1286 const head = document.createElement("div"); head.className = "locked-head";1287 const icon = document.createElement("div"); icon.className = "locked-icon";1288 icon.innerHTML = keySvgStr();1289 const title = document.createElement("div"); title.className = "locked-title";1290 title.textContent = "unlock to autofill";1291 head.append(icon, title);12921293 1294 const pwInp = document.createElement("input");1295 pwInp.className = "locked-input";1296 pwInp.type = "password";1297 pwInp.placeholder = "master password";1298 pwInp.setAttribute("autocomplete", "current-password");12991300 1301 const errEl = document.createElement("div");1302 errEl.className = "locked-error";1303 errEl.style.display = "none";13041305 1306 const unlockBtn = document.createElement("button");1307 unlockBtn.className = "locked-btn";1308 unlockBtn.type = "button";1309 unlockBtn.textContent = "unlock";13101311 async function tryUnlock() {1312 const pw = pwInp.value.trim();1313 if (!pw) { pwInp.focus(); return; }1314 unlockBtn.disabled = true;1315 unlockBtn.textContent = "unlocking…";1316 errEl.style.display = "none";1317 try {1318 const res = await chrome.runtime.sendMessage({1319 type: "UNLOCK",1320 payload: { password: pw },1321 });1322 if ((res as { ok?: boolean }).ok) {1323 dismiss();1324 options.onUnlocked?.();1325 } else {1326 errEl.textContent = "incorrect password";1327 errEl.style.display = "block";1328 pwInp.value = "";1329 pwInp.focus();1330 unlockBtn.disabled = false;1331 unlockBtn.textContent = "unlock";1332 }1333 } catch {1334 errEl.textContent = "could not reach Peach";1335 errEl.style.display = "block";1336 unlockBtn.disabled = false;1337 unlockBtn.textContent = "unlock";1338 }1339 }13401341 pwInp.addEventListener("keydown", (e) => {1342 e.stopPropagation();1343 if (e.key === "Enter") void tryUnlock();1344 });1345 pwInp.addEventListener("keyup", (e) => e.stopPropagation());1346 pwInp.addEventListener("keypress", (e) => e.stopPropagation());1347 unlockBtn.addEventListener("click", () => void tryUnlock());13481349 wrap.append(head, pwInp, errEl, unlockBtn);1350 container.appendChild(wrap);13511352 1353 1354 1355 1356 1357 1358 const captureKeydown = (e: KeyboardEvent) => {1359 if (!host.isConnected) {1360 document.removeEventListener("keydown", captureKeydown, true);1361 return;1362 }1363 1364 if (e.ctrlKey || e.metaKey || e.altKey || e.key === "Tab") return;1365 1366 e.stopImmediatePropagation();1367 if (e.key === "Escape") { dismiss(true); return; }1368 if (e.key === "Enter") { void tryUnlock(); return; }1369 1370 1371 if (document.activeElement !== host) {1372 pwInp.focus({ preventScroll: true });1373 }1374 };1375 captureKeydownRef = captureKeydown;1376 document.addEventListener("keydown", captureKeydown, true);13771378 1379 1380 pwInp.focus({ preventScroll: true });1381 requestAnimationFrame(() => { if (host.isConnected) pwInp.focus({ preventScroll: true }); });1382 }13831384 function handleKeyDown(e: KeyboardEvent) {1385 if (options.locked) {1386 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1387 return;1388 }1389 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter") {1390 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1391 return;1392 }1393 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");1394 const count = allEntryEls.length;1395 if (document.activeElement === searchInput) {1396 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }1397 if (count === 0) return;1398 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;1399 }1400 if (count === 0) return;1401 switch (e.key) {1402 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;1403 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;1404 case "Enter": {1405 if (focusedIndex >= 0) {1406 const el = allEntryEls[focusedIndex] as HTMLElement;1407 const id = el.dataset.id;1408 if (id) { onSelect(id); dismiss(); }1409 else if (el.classList.contains("is-new") || !id) {1410 1411 showCreateForm();1412 }1413 }1414 break;1415 }1416 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;1417 }1418 }14191420 document.addEventListener("keydown", handleKeyDown);14211422 document.body.appendChild(host);14231424 function updatePosition() {1425 const r = anchorEl.getBoundingClientRect();1426 host.style.top = `${r.bottom + window.scrollY + 5}px`;1427 host.style.left = `${r.left + window.scrollX}px`;1428 host.style.width = `${Math.max(r.width, 260)}px`;1429 }1430 1431 1432 1433 1434 1435 1436 1437 let captureKeydownRef: ((e: KeyboardEvent) => void) | null = null;1438 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);14391440 if (options.locked) {1441 renderLocked();1442 } else if (entries.length === 1 && !options.isSignup) {1443 1444 1445 1446 1447 renderSilverPlatter(entries[0]!);1448 } else {1449 renderList();1450 }14511452 let exitScheduled = false;1453 function dismiss(userAction = false) {1454 1455 1456 1457 if (exitScheduled) return;1458 exitScheduled = true;1459 1460 1461 1462 stopObserving();1463 document.removeEventListener("click", outsideClick, { capture: true });1464 document.removeEventListener("keydown", handleKeyDown);1465 if (captureKeydownRef) {1466 document.removeEventListener("keydown", captureKeydownRef, true);1467 captureKeydownRef = null;1468 }1469 for (const cleanup of cleanupHooks) {1470 try { cleanup(); } catch { }1471 }1472 if (userAction) options.onDismiss?.();1473 container.classList.add("is-exiting");1474 setTimeout(() => host.remove(), 130);1475 }14761477 function outsideClick(ev: MouseEvent) {1478 const path = ev.composedPath();1479 1480 1481 1482 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);1483 }1484 1485 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);14861487 return dismiss;1488}14891490export interface CardSelectorEntry {1491 id: string;1492 title: string;1493 cardholderName?: string;1494 last4?: string;1495 expMonth?: string;1496 expYear?: string;1497 network?: import("../shared/types").CardNetwork;1498}14991500interface CardSelectorOptions {1501 domain?: string;1502 1503 onFill: (entry: import("../shared/types").VaultEntry) => void;1504 1505 onDismiss?: () => void;1506}150715081509151015111512151315141515export function showCardSelector(1516 anchorEl: HTMLInputElement,1517 cards: CardSelectorEntry[],1518 options: CardSelectorOptions1519): () => void {1520 if (cards.length === 0) return () => {};1521 const card = cards[0]!;15221523 const host = document.createElement("div");1524 host.style.cssText = "position: absolute; z-index: 2147483647;";1525 host.style.setProperty("--peach-accent", accentHex);1526 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));1527 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));1528 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1529 const shadow = host.attachShadow({ mode: "closed" });15301531 const style = shadow.appendChild(document.createElement("style"));1532 style.textContent = SELECTOR_STYLES;15331534 const rect = anchorEl.getBoundingClientRect();1535 host.style.top = `${rect.bottom + window.scrollY + 5}px`;1536 host.style.left = `${rect.left + window.scrollX}px`;1537 host.style.width = `${Math.max(rect.width, 320)}px`;15381539 const container = shadow.appendChild(document.createElement("div"));1540 container.className = "peach-inline-popup";1541 container.setAttribute("data-theme", popupTheme);15421543 1544 const domHdr = document.createElement("div");1545 domHdr.className = "domain-header";1546 const eyebrow = document.createElement("div");1547 eyebrow.className = "domain-eyebrow";1548 eyebrow.textContent = "Pay with Peach";1549 const domName = document.createElement("div");1550 domName.className = "domain-name";1551 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1552 domHdr.append(eyebrow, domName);1553 container.appendChild(domHdr);15541555 const platter = document.createElement("div");1556 platter.className = "card-platter";15571558 if (card.network && card.network !== "unknown") {1559 const net = document.createElement("div");1560 net.className = "cp-network";1561 net.textContent = card.network;1562 platter.appendChild(net);1563 }15641565 const nick = document.createElement("div");1566 nick.className = "cp-nickname";1567 nick.textContent = card.title;1568 platter.appendChild(nick);15691570 if (card.last4) {1571 const l4 = document.createElement("div");1572 l4.className = "cp-last4";1573 l4.textContent = `•••• ${card.last4}`;1574 platter.appendChild(l4);1575 }15761577 const meta = document.createElement("div");1578 meta.className = "cp-meta";1579 if (card.cardholderName) {1580 const name = document.createElement("span");1581 name.textContent = card.cardholderName;1582 meta.appendChild(name);1583 }1584 if (card.expMonth && card.expYear) {1585 if (card.cardholderName) {1586 const sep = document.createElement("span");1587 sep.className = "cp-meta-sep";1588 sep.textContent = "·";1589 meta.appendChild(sep);1590 }1591 const exp = document.createElement("span");1592 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;1593 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;1594 meta.appendChild(exp);1595 }1596 if (meta.childElementCount > 0) platter.appendChild(meta);15971598 const fillWrap = document.createElement("div");1599 fillWrap.className = "cp-fill-wrap";1600 const fillBtn = document.createElement("button");1601 fillBtn.className = "sp-fill-btn";1602 fillBtn.type = "button";1603 fillBtn.textContent = "Fill card details";1604 fillBtn.addEventListener("click", async () => {1605 if (fillBtn.disabled) return;1606 fillBtn.disabled = true;1607 fillBtn.classList.add("filled");1608 fillBtn.innerHTML = checkSvgStr();1609 const resp = await chrome.runtime.sendMessage({1610 type: "GET_ENTRY_FOR_FILL",1611 payload: { id: card.id },1612 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;1613 if (resp?.ok && resp.data) options.onFill(resp.data);1614 setTimeout(() => dismiss(), 400);1615 });1616 fillWrap.appendChild(fillBtn);1617 platter.appendChild(fillWrap);16181619 1620 const actions = document.createElement("div");1621 actions.className = "sp-actions";16221623 function flashBtn(b: HTMLButtonElement) {1624 b.classList.add("flashed");1625 setTimeout(() => b.classList.remove("flashed"), 700);1626 }16271628 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {1629 const resp = await chrome.runtime.sendMessage({1630 type: "GET_ENTRY_FOR_FILL",1631 payload: { id: card.id },1632 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;1633 return resp?.ok && resp.data ? resp.data : null;1634 }16351636 const copyNum = document.createElement("button");1637 copyNum.className = "sp-action-btn";1638 copyNum.type = "button";1639 copyNum.title = "Copy card number";1640 copyNum.textContent = "copy #";1641 copyNum.addEventListener("click", async (e) => {1642 e.stopPropagation();1643 const full = await fetchFullCard();1644 if (!full?.cardNumber) return;1645 let alreadyWritten = false;1646 try { await navigator.clipboard.writeText(full.cardNumber); alreadyWritten = true; } catch {}1647 await chrome.runtime.sendMessage({1648 type: "COPY",1649 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },1650 }).catch(() => {});1651 flashBtn(copyNum);1652 });1653 actions.appendChild(copyNum);16541655 const copyCvc = document.createElement("button");1656 copyCvc.className = "sp-action-btn";1657 copyCvc.type = "button";1658 copyCvc.title = "Copy CVC";1659 copyCvc.textContent = "copy CVC";1660 copyCvc.addEventListener("click", async (e) => {1661 e.stopPropagation();1662 const full = await fetchFullCard();1663 if (!full?.cvc) return;1664 let alreadyWritten = false;1665 try { await navigator.clipboard.writeText(full.cvc); alreadyWritten = true; } catch {}1666 await chrome.runtime.sendMessage({1667 type: "COPY",1668 payload: { text: full.cvc, label: "CVC", alreadyWritten },1669 }).catch(() => {});1670 flashBtn(copyCvc);1671 });1672 actions.appendChild(copyCvc);16731674 const closeBtn = document.createElement("button");1675 closeBtn.className = "sp-action-btn";1676 closeBtn.type = "button";1677 closeBtn.title = "Dismiss";1678 closeBtn.textContent = "✕";1679 closeBtn.addEventListener("click", (e) => {1680 e.stopPropagation();1681 dismiss(true);1682 });1683 actions.appendChild(closeBtn);16841685 platter.appendChild(actions);1686 container.appendChild(platter);16871688 document.body.appendChild(host);1689 function updatePosition() {1690 const r = anchorEl.getBoundingClientRect();1691 host.style.top = `${r.bottom + window.scrollY + 5}px`;1692 host.style.left = `${r.left + window.scrollX}px`;1693 host.style.width = `${Math.max(r.width, 320)}px`;1694 }1695 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);16961697 function onKeyDown(e: KeyboardEvent) {1698 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }1699 }1700 document.addEventListener("keydown", onKeyDown);17011702 let exitScheduled = false;1703 function dismiss(userAction = false) {1704 if (exitScheduled) return;1705 exitScheduled = true;1706 stopObserving();1707 document.removeEventListener("click", outsideClick, { capture: true });1708 document.removeEventListener("keydown", onKeyDown);1709 if (userAction) options.onDismiss?.();1710 container.classList.add("is-exiting");1711 setTimeout(() => host.remove(), 130);1712 }17131714 function outsideClick(ev: MouseEvent) {1715 const path = ev.composedPath();1716 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);1717 }1718 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);17191720 return dismiss;1721}