123456789101112import { ACCENT_COLORS, type PasswordPolicy, type PasswordHistoryItem, type SavedIdentity } from "../shared/types";13import { generatePassword } from "../shared/generate";14import { detectPasswordPolicy } from "./policy";15import {16 filterSavedIdentities,17 identityFilterLabel,18 resolveCustomIdentityKind,19 type IdentityFilterMode,20 type IdentityKind,21} from "./identity-helper";22232425function timeSince(when: number): string {26 const secs = Math.max(0, Math.floor((Date.now() - when) / 1000));27 if (secs < 60) return "just now";28 const mins = Math.floor(secs / 60);29 if (mins < 60) return `${mins}m ago`;30 const hours = Math.floor(mins / 60);31 if (hours < 24) return `${hours}h ago`;32 const days = Math.floor(hours / 24);33 if (days < 30) return `${days}d ago`;34 const months = Math.floor(days / 30);35 if (months < 12) return `${months}mo ago`;36 return `${Math.floor(months / 12)}y ago`;37}3839let accentHex = "#E8956D";40let popupTheme: "dark" | "light" = "dark";41chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string; theme?: string } } | undefined) => {42 const data = resp?.data;43 const name = data?.accentColor ?? "peach";44 accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";45 popupTheme = data?.theme === "light" ? "light" : "dark";46});4748495051function withAlpha(hex: string, alpha: string): string {52 53 54 55 return /^#[0-9a-f]{6}$/i.test(hex) ? hex + alpha : hex;56}5758export interface SelectorEntry {59 id: string;60 title: string;61 username?: string;62 email?: string;63 url?: string;64 hasTotp?: boolean;65 66 hasPasskey?: boolean;67}6869interface SelectorOptions {70 domain?: string;71 searchableEntries?: SelectorEntry[];72 73 isSignup?: boolean;74 75767778 identityKind?: IdentityKind;79 80818283 savedIdentities?: SavedIdentity[];84 8586878889 onFillFields?: (username: string, password: string, email?: string) => void;90 919293949596 hasEmailField?: boolean;97 9899100101102 locked?: boolean;103 104105106107 onUnlocked?: () => void;108 109110111112113 onDismiss?: () => void;114}115116function getDomainFromUrl(url: string): string {117 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }118}119120function keySvgStr(): string {121 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>`;122}123124function plusSvgStr(): string {125 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>`;126}127128function searchSvgStr(): string {129 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"><circle cx="5.25" cy="5.25" r="3.5"/><path d="M7.9 7.9l2.35 2.35"/></svg>`;130}131132function backSvgStr(): string {133 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.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 2.5L3.5 6l4 3.5"/></svg>`;134}135136function peachIconSvg(): string {137 138 139 140 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 240 240" fill="none" aria-hidden="true"><path d="M 124 64 C 134 32 168 16 200 22 C 198 56 168 72 130 66 Z" fill="#92AC65"/><path d="M 130 64 C 152 46 178 30 196 24" stroke="#5E7A42" stroke-width="2.6" fill="none" stroke-linecap="round"/><path d="M 120 60 C 75 45 28 75 22 130 C 18 185 65 222 125 222 C 185 222 222 185 220 130 C 218 78 170 45 120 60 Z" fill="#FFB082"/><path d="M 120 60 C 75 45 28 75 22 130 C 18 185 65 222 125 222 C 105 195 75 150 100 90 C 105 75 112 65 120 60 Z" fill="#E5805C"/><ellipse cx="178" cy="108" rx="20" ry="11" fill="#FFD0A8" opacity="0.7" transform="rotate(-26 178 108)"/></svg>`;141}142143function checkSvgStr(): string {144 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="#5E7A42" stroke-width="1.5" stroke-linecap="round"><path d="M4 10.5L8 14.5L16 6.5"/></svg>`;145}146147function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {148 const { hostname, origin } = location;149 const parts = hostname.split(".");150 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",151 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",152 "oidc", "federation", "saml", "cas", "ldap"];153 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());154 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;155 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);156 return { origin, title, loginSubdomain };157}158159160161162163164export function showRememberSiteToast(165 pageDomain: string,166 entryId: string,167): void {168 const host = document.createElement("div");169 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";170 host.style.setProperty("--peach-accent", accentHex);171 const shadow = host.attachShadow({ mode: "closed" });172173 const style = document.createElement("style");174 style.textContent = `175 .bar {176 display:flex;align-items:center;gap:10px;padding:10px 14px;177 background:#131109;border:1px solid rgba(240,232,216,0.09);178 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);179 font-family:Inter,sans-serif;white-space:nowrap;180 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;181 }182 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }183 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }184 .yes {185 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;186 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;187 }188 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }189 .no {190 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;191 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;192 }193 `;194 shadow.appendChild(style);195196 const bar = document.createElement("div"); bar.className = "bar";197 const txt = document.createElement("span"); txt.className = "txt";198 txt.textContent = `Remember ${pageDomain} for this entry?`;199 const yesBtn = document.createElement("button"); yesBtn.className = "yes";200 yesBtn.textContent = "Remember";201 yesBtn.addEventListener("click", async () => {202 await chrome.runtime.sendMessage({203 type: "ADD_ADDITIONAL_URL",204 payload: { id: entryId, url: location.origin },205 });206 host.remove();207 });208 const noBtn = document.createElement("button"); noBtn.className = "no";209 noBtn.textContent = "Not now";210 noBtn.addEventListener("click", () => host.remove());211 bar.append(txt, yesBtn, noBtn);212 shadow.appendChild(bar);213 document.body.appendChild(host);214 setTimeout(() => host.remove(), 9000);215}216217interface SmartGenerator {218 root: HTMLElement;219 getPassword: () => string;220 221 copy: () => Promise<void>;222}223224interface SmartGeneratorOptions {225 226 policy: PasswordPolicy;227 228229230231232 initialPassword?: string;233 234 onChange?: (pw: string) => void;235}236237238239240241242243244function createSmartGenerator(opts: SmartGeneratorOptions): SmartGenerator {245 const policy = opts.policy ?? {};246247 248 let length = policy.maxLength ?? 64;249 if (policy.minLength) length = Math.max(length, policy.minLength);250 if (length < 8) length = 8;251 if (length > 128) length = 128;252253 let upper = true;254 let lower = true;255 let numbers = true;256 let symbols = !policy.forbidsSymbols;257 if (policy.requiresSymbols) symbols = true;258 const forbiddenChars = policy.forbiddenChars ?? "";259260 let password = "";261262 const root = document.createElement("div");263 root.className = "pwgen-block";264265 const displayRow = root.appendChild(document.createElement("div"));266 displayRow.className = "pwgen-display-row";267 const output = displayRow.appendChild(document.createElement("input")) as HTMLInputElement;268 output.type = "text";269 output.className = "pwgen-output";270 output.setAttribute("autocomplete", "off");271 output.setAttribute("spellcheck", "false");272 output.setAttribute("autocorrect", "off");273 output.setAttribute("autocapitalize", "off");274275 const regenBtn = displayRow.appendChild(document.createElement("button"));276 regenBtn.className = "pwgen-icon-btn";277 regenBtn.type = "button";278 regenBtn.setAttribute("aria-label", "Regenerate password");279 regenBtn.title = "Regenerate";280 regenBtn.innerHTML = `<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><path d="M11 2.5A5.5 5.5 0 1 0 12.5 7"/><path d="M9.5 2.5H11V1"/></svg>`;281282 const copyBtn = displayRow.appendChild(document.createElement("button"));283 copyBtn.className = "pwgen-icon-btn";284 copyBtn.type = "button";285 copyBtn.setAttribute("aria-label", "Copy password");286 copyBtn.title = "Copy";287 copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="8" height="9" rx="1.5"/><path d="M10 4V2.5A1.5 1.5 0 0 0 8.5 1h-6A1.5 1.5 0 0 0 1 2.5v7A1.5 1.5 0 0 0 2.5 11H4"/></svg>`;288289 const controls = root.appendChild(document.createElement("div"));290 controls.className = "pwgen-controls-row";291 const slider = controls.appendChild(document.createElement("input"));292 slider.type = "range";293 slider.className = "pwgen-slider";294 slider.min = "8";295 slider.max = "128";296 slider.value = String(length);297 if (policy.minLength) slider.min = String(Math.max(8, policy.minLength));298 if (policy.maxLength) slider.max = String(Math.min(128, policy.maxLength));299 const lenNum = controls.appendChild(document.createElement("span"));300 lenNum.className = "pwgen-len-num pwgen-len-num--edit";301 lenNum.textContent = String(length);302 lenNum.addEventListener("click", () => {303 const li = document.createElement("input");304 li.type = "text"; li.inputMode = "numeric";305 li.className = "pwgen-len-input"; li.value = String(length);306 lenNum.replaceWith(li);307 requestAnimationFrame(() => li.select());308 function commitLenEdit() {309 const n = parseInt(li.value, 10);310 if (!isNaN(n)) {311 length = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));312 slider.value = String(length);313 regenerate();314 }315 lenNum.textContent = String(length);316 li.replaceWith(lenNum);317 }318 li.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commitLenEdit(); } else if (e.key === "Escape") { li.replaceWith(lenNum); } });319 li.addEventListener("blur", commitLenEdit);320 });321322 const pills = root.appendChild(document.createElement("div"));323 pills.className = "pwgen-pills";324325 function makePill(label: string, on: boolean, onToggle: (next: boolean) => void): HTMLButtonElement {326 const b = document.createElement("button");327 b.type = "button";328 b.className = `pwgen-pill ${on ? "is-on" : ""}`;329 b.textContent = label;330 b.addEventListener("click", () => {331 const next = !b.classList.contains("is-on");332 b.classList.toggle("is-on", next);333 onToggle(next);334 });335 return b;336 }337 pills.appendChild(makePill("A-Z", upper, (next) => { upper = next; regenerate(); }));338 pills.appendChild(makePill("a-z", lower, (next) => { lower = next; regenerate(); }));339 pills.appendChild(makePill("0-9", numbers, (next) => { numbers = next; regenerate(); }));340 pills.appendChild(makePill("!@#", symbols, (next) => {341 342 symbols = next;343 regenerate();344 }));345346 347 348 349 const hintParts: string[] = [];350 if (policy.minLength) hintParts.push(`min ${policy.minLength}`);351 if (policy.maxLength) hintParts.push(`max ${policy.maxLength}`);352 if (policy.requiresSymbols) hintParts.push("requires symbols");353 if (policy.forbidsSymbols) hintParts.push("no symbols");354 if (policy.forbiddenChars) hintParts.push(`excludes ${policy.forbiddenChars.slice(0, 6)}${policy.forbiddenChars.length > 6 ? "…" : ""}`);355 if (hintParts.length > 0) {356 const hint = root.appendChild(document.createElement("div"));357 hint.className = "pwgen-policy-hint";358 const accent = document.createElement("span");359 accent.className = "pwgen-hint-accent";360 accent.textContent = "site rules · ";361 const text = document.createTextNode(hintParts.join(" · "));362 hint.appendChild(accent);363 hint.appendChild(text);364 }365366 function regenerate() {367 368 369 password = generatePassword({370 length,371 includeUppercase: upper,372 includeLowercase: lower,373 includeNumbers: numbers,374 includeSymbols: symbols,375 forbiddenChars,376 });377 output.value = password;378 opts.onChange?.(password);379 }380381 output.addEventListener("input", () => {382 password = output.value;383 length = password.length;384 slider.value = String(Math.max(8, Math.min(128, length)));385 lenNum.textContent = String(length);386 opts.onChange?.(password);387 });388389 slider.addEventListener("input", () => {390 length = parseInt(slider.value, 10);391 lenNum.textContent = String(length);392 regenerate();393 });394 regenBtn.addEventListener("click", regenerate);395396 let copyResetTimer: ReturnType<typeof setTimeout> | null = null;397 async function copy(): Promise<void> {398 try {399 await navigator.clipboard.writeText(password);400 copyBtn.classList.add("is-copied");401 if (copyResetTimer) clearTimeout(copyResetTimer);402 copyResetTimer = setTimeout(() => copyBtn.classList.remove("is-copied"), 1400);403 } catch {404 405 }406 }407 copyBtn.addEventListener("click", () => { void copy(); });408409 if (opts.initialPassword) {410 password = opts.initialPassword;411 output.value = password;412 opts.onChange?.(password);413 } else {414 regenerate();415 }416417 return {418 root,419 getPassword: () => password,420 copy,421 };422}423424export function observeAnchorPosition(425 anchorEl: HTMLElement,426 onUpdate: () => void427): () => void {428 let rafId = 0;429 let burstUntil = 0;430431 const runUpdate = () => {432 rafId = 0;433 onUpdate();434 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);435 };436 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };437 const scheduleBurst = (ms = 220) => {438 burstUntil = Math.max(burstUntil, performance.now() + ms);439 scheduleUpdate();440 };441442 const resizeObserver = new ResizeObserver(() => scheduleBurst());443 resizeObserver.observe(anchorEl);444 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);445446 const mutationObserver = new MutationObserver(() => scheduleBurst());447 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });448 if (anchorEl.parentElement) {449 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });450 }451452 const onScroll = () => scheduleUpdate();453 window.addEventListener("scroll", onScroll, true);454 window.addEventListener("resize", onScroll);455 window.visualViewport?.addEventListener("resize", onScroll);456 window.visualViewport?.addEventListener("scroll", onScroll);457 anchorEl.addEventListener("focus", () => scheduleBurst());458 anchorEl.addEventListener("input", () => scheduleBurst());459460 scheduleBurst(260);461462 return () => {463 if (rafId) cancelAnimationFrame(rafId);464 resizeObserver.disconnect();465 mutationObserver.disconnect();466 window.removeEventListener("scroll", onScroll, true);467 window.removeEventListener("resize", onScroll);468 window.visualViewport?.removeEventListener("resize", onScroll);469 window.visualViewport?.removeEventListener("scroll", onScroll);470 };471}472473export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {474 const button = document.createElement("button");475 button.type = "button";476 button.setAttribute("aria-label", "Peach autofill");477 button.title = "Fill with Peach";478 button.innerHTML = peachIconSvg();479 480 481 482 483 button.style.cssText = `484 position: fixed; z-index: 2147483646;485 width: 26px; height: 26px; cursor: pointer;486 pointer-events: auto; border: none; padding: 3px;487 display: inline-flex; align-items: center; justify-content: center;488 background: transparent; border-radius: 999px;489 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;490 opacity: 0;491 `;492 button.addEventListener("mouseenter", () => {493 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;494 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;495 button.style.transform = "scale(1.08)";496 });497 button.addEventListener("mouseleave", () => {498 button.style.background = "transparent";499 button.style.boxShadow = "none";500 button.style.transform = "none";501 });502 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });503 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });504505 document.body.appendChild(button);506507 function updatePosition() {508 const rect = field.getBoundingClientRect();509 if (rect.width === 0 || rect.height === 0) {510 button.style.opacity = "0"; button.style.pointerEvents = "none";511 return;512 }513 const sz = 26;514 const inset = Math.max(5, Math.min(10, rect.height * 0.22));515 const top = rect.top + rect.height / 2 - sz / 2;516 517 518 const minLeft = rect.left + 4;519 const left = Math.max(minLeft, rect.right - sz - inset);520 521 if (rect.width < 40) {522 button.style.opacity = "0"; button.style.pointerEvents = "none";523 return;524 }525 button.style.top = `${top}px`;526 button.style.left = `${left}px`;527 button.style.opacity = "1"; button.style.pointerEvents = "auto";528 }529 updatePosition();530 requestAnimationFrame(() => { button.style.opacity = "1"; });531 const stopObserving = observeAnchorPosition(field, updatePosition);532 return () => { stopObserving(); button.remove(); };533}534535536537538539540541const SELECTOR_STYLES = `542 :host { all: initial; }543 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }544545 .peach-inline-popup[data-theme="dark"] {546 --peach-bg: #1a1410;547 --peach-bg-card: #221c16;548 --peach-text: #f5f0ea;549 --peach-text-dim: #8a7a6a;550 --peach-text-muted: #4a3a2a;551 --peach-border: rgba(255,255,255,0.07);552 --peach-input-bg: rgba(255,255,255,0.05);553 --peach-input-border: rgba(255,255,255,0.08);554 }555 .peach-inline-popup[data-theme="light"] {556 --peach-bg: #fdf8f4;557 --peach-bg-card: #f5ede4;558 --peach-text: #1c1310;559 --peach-text-dim: #6b5a4a;560 --peach-text-muted: #a89080;561 --peach-border: rgba(0,0,0,0.08);562 --peach-input-bg: rgba(0,0,0,0.04);563 --peach-input-border: rgba(0,0,0,0.10);564 }565566 .peach-inline-popup {567 background: var(--peach-bg);568 border: 1px solid var(--peach-border-accent, var(--peach-border));569 border-radius: 14px;570 overflow: hidden;571 box-shadow:572 0 8px 40px var(--peach-glow, transparent),573 0 2px 12px rgba(0,0,0,0.5);574 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;575 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;576 transition: height 220ms cubic-bezier(0.16,1,0.3,1);577 }578 /* Light theme: shadow against a light page is too strong otherwise — drop both579 the accent glow and the dark drop-shadow opacity to keep the popup elegant. */580 .peach-inline-popup[data-theme="light"] {581 box-shadow:582 0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),583 0 2px 12px rgba(0,0,0,0.12);584 }585 .peach-inline-popup.is-exiting {586 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;587 pointer-events: none;588 }589590 @keyframes peach-enter {591 0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }592 65% { opacity: 1; filter: blur(0); }593 100% { opacity: 1; transform: translateY(0) scale(1); }594 }595 @keyframes peach-exit {596 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }597 100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }598 }599 @keyframes peach-slide-in {600 from { opacity: 0; transform: translateX(12px); }601 to { opacity: 1; transform: translateX(0); }602 }603 @keyframes peach-slide-back {604 from { opacity: 0; transform: translateX(-12px); }605 to { opacity: 1; transform: translateX(0); }606 }607 .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }608 .peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }609610 /* Domain header */611 .domain-header {612 padding: 9px 13px 7px;613 border-bottom: 1px solid var(--peach-border);614 display: flex; align-items: center; justify-content: space-between;615 gap: 8px;616 }617 .domain-header-text {618 display: flex; flex-direction: column; gap: 2px;619 min-width: 0; flex: 1;620 }621 .domain-eyebrow {622 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;623 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;624 }625 .domain-name {626 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);627 font-family: "IBM Plex Mono", monospace;628 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;629 }630 .domain-header-actions {631 display: inline-flex; align-items: center; gap: 5px;632 flex-shrink: 0;633 }634 .domain-add-btn,635 .domain-search-btn {636 flex-shrink: 0;637 width: 22px; height: 22px;638 display: inline-flex; align-items: center; justify-content: center;639 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);640 border: 1px solid color-mix(in srgb, var(--peach-accent) 28%, transparent);641 border-radius: 6px;642 color: var(--peach-accent);643 cursor: pointer; padding: 0;644 transition: background 120ms, border-color 120ms, transform 80ms;645 font-family: "IBM Plex Mono", monospace;646 font-size: 14px; line-height: 1;647 }648 .domain-add-btn:hover,649 .domain-search-btn:hover {650 background: color-mix(in srgb, var(--peach-accent) 22%, transparent);651 border-color: color-mix(in srgb, var(--peach-accent) 48%, transparent);652 transform: scale(1.06);653 }654 .domain-add-btn:active,655 .domain-search-btn:active { transform: scale(0.96); }656657 /* Search */658 .search-wrap {659 padding: 9px 12px 7px;660 border-bottom: 1px solid var(--peach-border);661 }662 .search {663 width: 100%; background: var(--peach-input-bg);664 border: 1px solid var(--peach-input-border);665 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;666 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;667 transition: border-color 120ms, box-shadow 120ms;668 }669 .search:focus {670 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);671 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);672 }673 .search::placeholder { color: var(--peach-text-muted); }674675 /* Results */676 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }677 .results::-webkit-scrollbar { display: none; }678679 /* Entry rows */680 .entry {681 display: flex; align-items: center; padding: 8px 12px;682 cursor: pointer; border-bottom: 1px solid var(--peach-border);683 transition: background 70ms; position: relative; outline: none;684 }685 .entry:last-child { border-bottom: none; }686 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }687 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }688 .entry.focused::before {689 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;690 width: 2px; border-radius: 2px;691 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);692 }693694 .entry-icon {695 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;696 background: var(--peach-input-bg); border: 1px solid var(--peach-border);697 display: flex; align-items: center; justify-content: center;698 overflow: hidden; margin-right: 10px;699 }700 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }701 .entry-icon.is-new {702 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);703 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);704 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);705 }706707 .entry-info { flex: 1; min-width: 0; }708 .entry-title {709 font-size: 12.5px; color: var(--peach-text);710 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;711 line-height: 1.3;712 }713 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }714 .entry-sub {715 font-size: 10px; color: var(--peach-text-dim);716 font-family: "IBM Plex Mono", monospace;717 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;718 margin-top: 1px;719 }720721 .totp-badge {722 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;723 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);724 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);725 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;726 }727 .passkey-badge {728 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;729 border-radius: 4px;730 background: color-mix(in srgb, var(--peach-accent) 14%, transparent);731 color: color-mix(in srgb, var(--peach-accent) 85%, transparent);732 border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);733 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;734 }735736 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }737738 .empty-hint {739 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);740 font-family: "IBM Plex Mono", monospace;741 }742743 /* Create form */744 .create-form-wrap {745 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;746 }747 .create-head {748 display: flex; align-items: center; gap: 8px;749 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);750 }751 .back-btn {752 background: transparent; border: none; cursor: pointer;753 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;754 display: flex; align-items: center; line-height: 0;755 transition: color 100ms, background 100ms;756 }757 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }758 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }759 /* minmax(0, 1fr) caps the grid track at the popup width — without it,760 long monospace content (e.g. a 60-char generated password) would761 auto-size the track wider than the popup and overflow the right edge. */762 .create-body { padding: 12px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; }763 .f-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: 3px; }764 .f-label {765 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;766 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;767 }768 .f-input {769 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);770 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;771 font-size: 11px; font-family: "IBM Plex Mono", monospace;772 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;773 }774 .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); }775 .f-input::placeholder { color: var(--peach-text-muted); }776 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }777 .f-row { display: flex; gap: 6px; }778 .f-row .f-input { flex: 1; min-width: 0; }779 .gen-btn {780 padding: 0 10px; border-radius: 7px; border: none;781 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);782 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);783 font-size: 10px; font-family: "IBM Plex Mono", monospace;784 cursor: pointer; white-space: nowrap; flex-shrink: 0;785 display: flex; align-items: center; gap: 4px;786 transition: background 100ms;787 }788 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }789 .gen-toggle-link {790 background: none; border: none; padding: 2px 0;791 color: color-mix(in srgb, var(--peach-accent) 60%, transparent);792 font-size: 10px; font-family: "IBM Plex Mono", monospace;793 cursor: pointer; text-align: left; transition: color 100ms;794 }795 .gen-toggle-link:hover { color: var(--peach-accent); }796 .pw-history {797 display: grid; gap: 4px; margin-top: 4px;798 max-height: 150px; overflow-y: auto; scrollbar-width: thin;799 }800 .pw-history-row {801 display: flex; align-items: center; justify-content: space-between; gap: 6px;802 padding: 5px 7px; border-radius: 7px;803 background: color-mix(in srgb, var(--peach-accent) 6%, transparent);804 border: 1px solid color-mix(in srgb, var(--peach-accent) 10%, transparent);805 }806 .pw-history-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }807 .pw-history-pw {808 font-family: "IBM Plex Mono", monospace; font-size: 11px;809 color: var(--peach-t1, #eee); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;810 }811 .pw-history-age {812 font-family: "IBM Plex Mono", monospace; font-size: 9px;813 color: color-mix(in srgb, var(--peach-accent) 55%, transparent); letter-spacing: 0.03em;814 }815 .pw-history-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }816 .pw-history-btn {817 background: none; border: none; padding: 2px 5px; border-radius: 5px;818 color: color-mix(in srgb, var(--peach-accent) 65%, transparent);819 font-size: 9px; font-family: "IBM Plex Mono", monospace; cursor: pointer;820 transition: color 100ms, background 100ms;821 }822 .pw-history-btn:hover {823 color: var(--peach-accent);824 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);825 }826 .save-btn {827 width: 100%; padding: 8px; border-radius: 8px; border: none;828 background: var(--peach-accent); color: #fff; font-size: 12px;829 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;830 transition: background 120ms, box-shadow 120ms, transform 80ms;831 letter-spacing: 0.01em;832 }833 .save-btn:hover {834 background: color-mix(in srgb, var(--peach-accent) 82%, #000);835 box-shadow: 0 4px 20px var(--peach-glow, transparent);836 }837 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }838 .url-hint {839 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);840 font-family: "IBM Plex Mono", monospace; font-style: italic;841 }842843 /* Success */844 .success-wrap {845 display: flex; flex-direction: column; align-items: center;846 padding: 24px 16px 20px; gap: 8px;847 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;848 }849 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }850 .success-ring {851 width: 40px; height: 40px; border-radius: 50%;852 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);853 display: flex; align-items: center; justify-content: center;854 }855 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }856 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }857858 /* Silver platter (State 1 — single match) */859 .silver-platter {860 padding: 14px 14px 12px;861 display: flex; flex-direction: column; gap: 6px;862 }863 .sp-title {864 font-size: 14px; font-weight: 500; color: var(--peach-text);865 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;866 line-height: 1.25;867 }868 .sp-identity-row {869 display: flex; align-items: baseline; gap: 5px; margin-bottom: 3px;870 }871 .sp-identity-label {872 font-size: 9px; font-weight: 600; letter-spacing: 0.05em;873 text-transform: uppercase; color: var(--peach-text-muted); flex-shrink: 0;874 }875 .sp-username {876 font-size: 12px; color: var(--peach-text-dim);877 font-family: "IBM Plex Mono", monospace;878 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;879 margin-bottom: 0;880 }881 .sp-identity-row + .sp-fill-btn, .sp-username + .sp-fill-btn { margin-top: 6px; }882 .sp-fill-btn {883 width: 100%; padding: 11px; border-radius: 9px; border: none;884 background: var(--peach-accent); color: #fff;885 font-size: 13px; font-weight: 500; cursor: pointer;886 font-family: Inter, sans-serif; letter-spacing: 0.01em;887 display: flex; align-items: center; justify-content: center; min-height: 38px;888 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;889 }890 .sp-fill-btn:hover {891 filter: brightness(1.1);892 box-shadow: 0 4px 20px var(--peach-glow, transparent);893 transform: translateY(-1px);894 }895 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }896 .sp-fill-btn.filled {897 background: #16a34a; transform: none; filter: none;898 box-shadow: 0 4px 16px rgba(22,163,74,0.35);899 cursor: default;900 }901 .sp-fill-btn:disabled { cursor: default; }902 .sp-actions-group {903 margin-top: 6px;904 display: flex; flex-direction: column; gap: 4px;905 }906 .sp-actions-label {907 font-size: 8px; letter-spacing: 0.18em; text-transform: uppercase;908 color: var(--peach-text-muted);909 font-family: "IBM Plex Mono", monospace;910 text-align: center;911 }912 .sp-actions {913 display: flex; gap: 4px; justify-content: space-between; align-items: center;914 }915 .sp-action-btn {916 flex: 1; height: 28px; border: none; border-radius: 6px;917 background: transparent; color: var(--peach-text-dim);918 font-size: 12px; cursor: pointer;919 transition: background 120ms ease, color 120ms ease;920 font-family: Inter, sans-serif; letter-spacing: 0.04em;921 display: inline-flex; align-items: center; justify-content: center; gap: 4px;922 white-space: nowrap; overflow: hidden;923 }924 .sp-action-btn:hover:not(:disabled) {925 background: color-mix(in srgb, var(--peach-text) 5%, transparent);926 color: var(--peach-text);927 }928 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }929 .sp-action-btn.flashed {930 color: var(--peach-accent);931 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);932 }933934 /* TOTP tray — shown below the silver-platter secondary actions */935 .sp-tray {936 margin-top: 8px;937 padding: 10px 12px;938 border-radius: 9px;939 background: var(--peach-input-bg);940 border: 1px solid var(--peach-input-border);941 display: flex; align-items: center; gap: 12px;942 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;943 }944 .sp-tray-label {945 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;946 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;947 flex-shrink: 0;948 }949 .sp-totp-code {950 font-size: 18px; font-weight: 500;951 color: var(--peach-text);952 font-family: "IBM Plex Mono", monospace;953 letter-spacing: 0.12em;954 flex: 1;955 text-align: center;956 }957 .sp-totp-ring {958 width: 22px; height: 22px; flex-shrink: 0;959 }960 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }961 .sp-totp-ring .ring-fill {962 stroke: var(--peach-accent);963 transition: stroke-dashoffset 1s linear;964 transform: rotate(-90deg);965 transform-origin: center;966 }967 .sp-totp-copy {968 background: transparent; border: none; cursor: pointer;969 padding: 4px 8px; border-radius: 5px;970 color: var(--peach-text-dim); font-size: 11px;971 font-family: Inter, sans-serif;972 transition: background 120ms, color 120ms;973 }974 .sp-totp-copy:hover {975 background: color-mix(in srgb, var(--peach-text) 5%, transparent);976 color: var(--peach-text);977 }978979 /* Card silver platter (State 7) — reuses .sp-* primary action classes */980 .card-platter {981 padding: 12px 14px 10px;982 display: flex; flex-direction: column; gap: 4px;983 }984 .cp-network {985 font-size: 10px; font-weight: 600;986 color: var(--peach-accent);987 letter-spacing: 0.08em; text-transform: uppercase;988 font-family: Inter, sans-serif;989 }990 .cp-nickname {991 font-size: 14px; font-weight: 500; color: var(--peach-text);992 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;993 line-height: 1.2;994 }995 .cp-last4 {996 font-size: 12px; color: var(--peach-text-dim);997 font-family: "IBM Plex Mono", monospace;998 letter-spacing: 0.04em;999 margin-top: 2px;1000 }1001 .cp-meta {1002 font-size: 11px; color: var(--peach-text-dim);1003 font-family: "IBM Plex Mono", monospace;1004 margin-top: 2px;1005 display: flex; gap: 10px; align-items: center;1006 }1007 .cp-meta-sep { opacity: 0.5; }1008 .cp-fill-wrap { margin-top: 10px; }10091010 /* Locked state */1011 .locked-wrap {1012 display: flex; flex-direction: column; align-items: stretch;1013 padding: 14px 13px 13px; gap: 9px;1014 }1015 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }1016 .locked-icon {1017 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;1018 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1019 display: flex; align-items: center; justify-content: center;1020 }1021 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }1022 .locked-sub { font-size: 10.5px; line-height: 1.4; color: var(--peach-text-muted); text-align: center; margin: 2px 0 2px; }1023 .locked-input {1024 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1025 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;1026 font-size: 11px; font-family: "IBM Plex Mono", monospace;1027 outline: none; transition: border-color 120ms, box-shadow 120ms;1028 }1029 .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); }1030 .locked-input::placeholder { color: var(--peach-text-muted); }1031 .locked-btn {1032 width: 100%; padding: 8px; border-radius: 8px; border: none;1033 background: var(--peach-accent); color: #fff; font-size: 12px;1034 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;1035 transition: background 120ms;1036 letter-spacing: 0.01em;1037 box-shadow: 0 2px 16px var(--peach-glow, transparent);1038 animation: peach-pulse-glow 2.4s ease-in-out infinite;1039 }1040 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }1041 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }1042 .locked-error {1043 font-size: 10px; color: rgba(220,80,60,0.85);1044 font-family: "IBM Plex Mono", monospace;1045 }10461047 @keyframes peach-pulse-glow {1048 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }1049 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),1050 0 0 48px var(--peach-glow, transparent); }1051 }10521053 /* ── State 10: identity helper ─────────────────────────────── */1054 .identity-list {1055 max-height: 192px; overflow-y: auto; padding: 4px 0;1056 scrollbar-width: none;1057 }1058 .identity-list::-webkit-scrollbar { display: none; }1059 .peach-identity-row {1060 display: flex; align-items: center; gap: 10px;1061 padding: 9px 12px; cursor: pointer; border-radius: 8px;1062 transition: background 100ms ease;1063 }1064 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1065 .peach-identity-value {1066 font-family: "IBM Plex Mono", monospace; font-size: 13px;1067 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;1068 }1069 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }1070 .identity-search-wrap {1071 display: flex; flex-direction: column; gap: 6px;1072 }1073 .identity-mode-toggle {1074 display: inline-flex; gap: 4px; flex-wrap: wrap;1075 }1076 .identity-mode-btn {1077 border: 1px solid var(--peach-input-border);1078 background: var(--peach-input-bg);1079 color: var(--peach-text-dim);1080 border-radius: 999px;1081 padding: 4px 9px;1082 cursor: pointer;1083 font-family: "IBM Plex Mono", monospace;1084 font-size: 9px;1085 letter-spacing: 0.03em;1086 transition: background 100ms ease, border-color 100ms ease, color 100ms ease, transform 80ms ease;1087 }1088 .identity-mode-btn:hover {1089 background: color-mix(in srgb, var(--peach-text) 6%, var(--peach-input-bg));1090 color: var(--peach-text);1091 }1092 .identity-mode-btn.is-active {1093 color: var(--peach-accent);1094 border-color: color-mix(in srgb, var(--peach-accent) 30%, var(--peach-input-border));1095 background: color-mix(in srgb, var(--peach-accent) 10%, var(--peach-input-bg));1096 }1097 .identity-mode-btn:active { transform: translateY(1px); }1098 .identity-custom-row {1099 display: flex; flex-direction: column; gap: 2px;1100 padding: 9px 12px; cursor: pointer; border-radius: 8px;1101 transition: background 100ms ease;1102 }1103 .identity-custom-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1104 .identity-custom-row-title {1105 color: var(--peach-text); font-size: 12px; font-family: Inter, sans-serif;1106 }1107 .identity-custom-row-sub {1108 color: var(--peach-text-muted); font-size: 10px; font-family: "IBM Plex Mono", monospace;1109 }1110 .identity-empty {1111 padding: 10px 12px 2px; color: var(--peach-text-dim);1112 font-size: 10px; font-family: "IBM Plex Mono", monospace;1113 }1114 .identity-alt-row {1115 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1116 border-radius: 8px; transition: background 100ms ease;1117 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;1118 }1119 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1120 .identity-escape-row {1121 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1122 border-radius: 8px; transition: background 100ms ease; outline: none;1123 color: var(--peach-accent); font-size: 11px; font-family: Inter, sans-serif;1124 margin-top: 2px;1125 }1126 .identity-escape-row:hover, .identity-escape-row:focus { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }1127 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }1128 .identity-alt-input-wrap.open { display: flex; }1129 .identity-alt-input {1130 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;1131 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);1132 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;1133 outline: none; transition: border-color 120ms;1134 }1135 .identity-alt-input:focus {1136 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);1137 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);1138 }1139 .identity-alt-confirm {1140 height: 32px; padding: 0 12px; border-radius: 7px; border: none;1141 background: var(--peach-accent); color: #fff; font-size: 12px;1142 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;1143 }1144 /* ── Generator view (after identity picked) ────────────────── */1145 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1146 .gen-confirmed {1147 display: flex; align-items: center; gap: 8px; padding: 7px 10px;1148 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1149 border-radius: 8px;1150 }1151 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }1152 .gen-confirmed-val {1153 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1154 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1155 }1156 .gen-label {1157 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;1158 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;1159 }1160 .gen-pw-row { display: flex; gap: 6px; align-items: center; }1161 .gen-pw-val {1162 flex: 1; padding: 8px 10px; border-radius: 7px;1163 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1164 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1165 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1166 }1167 .gen-regen-btn {1168 height: 34px; padding: 0 10px; border-radius: 7px; border: none;1169 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1170 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;1171 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;1172 }1173 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1174 .gen-fill-btn {1175 padding: 10px; border-radius: 8px; border: none;1176 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;1177 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;1178 transition: filter 120ms ease, transform 80ms ease;1179 }1180 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }1181 .gen-fill-btn:active { transform: translateY(0); }11821183 /* ── Smart password generator (parity with main UI compact mode) ─── */1184 .pwgen-block { display: flex; flex-direction: column; gap: 8px; }1185 .pwgen-display-row {1186 display: flex; align-items: center; gap: 6px;1187 padding: 8px 10px; border-radius: 7px;1188 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1189 }1190 .pwgen-output {1191 flex: 1; min-width: 0; width: 0;1192 font-family: "IBM Plex Mono", monospace; font-size: 12px;1193 color: var(--peach-text);1194 background: transparent; border: none; outline: none; padding: 0;1195 }1196 .pwgen-icon-btn {1197 flex-shrink: 0;1198 width: 26px; height: 26px;1199 display: inline-flex; align-items: center; justify-content: center;1200 background: transparent; border: 1px solid var(--peach-input-border);1201 border-radius: 6px; color: var(--peach-text-dim); cursor: pointer;1202 padding: 0; transition: background 100ms, color 100ms, border-color 100ms;1203 }1204 .pwgen-icon-btn:hover {1205 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1206 color: var(--peach-accent);1207 border-color: color-mix(in srgb, var(--peach-accent) 30%, transparent);1208 }1209 .pwgen-icon-btn.is-copied {1210 background: color-mix(in srgb, var(--peach-accent) 18%, transparent);1211 color: var(--peach-accent);1212 border-color: color-mix(in srgb, var(--peach-accent) 45%, transparent);1213 }1214 .pwgen-controls-row {1215 display: flex; align-items: center; gap: 8px;1216 flex-wrap: wrap;1217 }1218 .pwgen-slider {1219 flex: 1; min-width: 110px;1220 accent-color: var(--peach-accent);1221 cursor: pointer; height: 2px; background: transparent;1222 }1223 .pwgen-len-num {1224 font-family: "IBM Plex Mono", monospace; font-size: 11px;1225 color: var(--peach-accent); min-width: 22px; text-align: right;1226 }1227 .pwgen-len-num--edit { cursor: text; border-bottom: 1px dotted color-mix(in srgb, var(--peach-accent) 50%, transparent); }1228 .pwgen-len-input {1229 font-family: "IBM Plex Mono", monospace; font-size: 11px;1230 color: var(--peach-accent); width: 28px; text-align: right;1231 background: transparent; border: none; border-bottom: 1px solid var(--peach-accent);1232 outline: none; padding: 0; min-width: 22px;1233 }1234 @keyframes identity-suggest-in {1235 from { opacity: 0; transform: translateY(-4px); }1236 to { opacity: 1; transform: none; }1237 }1238 .identity-suggest-list { display: flex; flex-direction: column; gap: 1px; margin-top: 3px;1239 animation: identity-suggest-in 180ms cubic-bezier(0.16,1,0.3,1) both; }1240 .identity-filter-row { display: flex; align-items: center; gap: 4px; padding: 2px 0 4px; }1241 .identity-collapse-btn {1242 margin-left: auto; background: transparent; border: none; cursor: pointer;1243 color: var(--peach-text-muted); padding: 2px 4px; display: flex; align-items: center;1244 line-height: 0; border-radius: 3px;1245 transition: color 100ms ease, transform 220ms cubic-bezier(0.16,1,0.3,1);1246 }1247 .identity-collapse-btn:hover { color: var(--peach-text); }1248 .identity-collapse-btn.is-collapsed { transform: rotate(-90deg); }1249 .identity-rows { display: grid; grid-template-rows: 1fr;1250 transition: grid-template-rows 220ms cubic-bezier(0.16,1,0.3,1); }1251 .identity-rows.is-collapsed { grid-template-rows: 0fr; }1252 .identity-rows-inner { overflow-y: auto; min-height: 0; max-height: 148px;1253 scrollbar-width: thin; scrollbar-color: var(--peach-text-muted) transparent; }1254 .identity-rows-inner::-webkit-scrollbar { width: 4px; }1255 .identity-rows-inner::-webkit-scrollbar-track { background: transparent; }1256 .identity-rows-inner::-webkit-scrollbar-thumb { background: var(--peach-text-muted); border-radius: 2px; }1257 .identity-filter-pill {1258 padding: 2px 7px; border-radius: 999px;1259 border: 1px solid var(--peach-input-border);1260 background: transparent; color: var(--peach-text-muted);1261 font-family: "IBM Plex Mono", monospace; font-size: 9px;1262 cursor: pointer; transition: border-color 80ms, background 80ms, color 80ms;1263 }1264 .identity-filter-pill.is-on {1265 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1266 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1267 color: var(--peach-accent);1268 }1269 .identity-suggest-row {1270 display: flex; align-items: center; gap: 5px;1271 width: 100%; padding: 4px 6px; border-radius: 5px; border: none;1272 background: transparent; cursor: pointer; text-align: left;1273 transition: background 80ms;1274 }1275 .identity-suggest-row:hover,1276 .identity-suggest-row:focus { background: color-mix(in srgb, var(--peach-accent) 10%, transparent); outline: none; }1277 .identity-suggest-val {1278 font-family: "IBM Plex Mono", monospace; font-size: 10px; color: var(--peach-text);1279 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;1280 }1281 .identity-suggest-type {1282 font-family: "IBM Plex Mono", monospace; font-size: 8px; font-weight: 600;1283 flex-shrink: 0; padding: 1px 3px; border-radius: 3px;1284 }1285 .identity-suggest-type--email {1286 color: color-mix(in srgb, var(--peach-accent) 70%, transparent);1287 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1288 }1289 .identity-suggest-type--username {1290 color: var(--peach-text-muted);1291 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1292 }1293 .identity-suggest-meta {1294 font-family: "IBM Plex Mono", monospace; font-size: 9px;1295 color: var(--peach-text-muted); flex-shrink: 0;1296 }1297 .pwgen-pills { display: flex; gap: 5px; flex-wrap: wrap; }1298 .pwgen-pill {1299 padding: 3px 8px; border-radius: 999px;1300 border: 1px solid var(--peach-input-border);1301 background: transparent; color: var(--peach-text-muted);1302 font-family: "IBM Plex Mono", monospace; font-size: 9px;1303 letter-spacing: 0.06em; cursor: pointer;1304 transition: all 130ms ease;1305 }1306 .pwgen-pill.is-on {1307 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1308 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1309 color: var(--peach-accent);1310 }1311 .pwgen-pill:not(.is-on):hover {1312 color: var(--peach-text-dim);1313 border-color: var(--peach-text-muted);1314 }1315 .pwgen-policy-hint {1316 font-size: 10px; color: var(--peach-text-muted);1317 font-family: "IBM Plex Mono", monospace; letter-spacing: 0.04em;1318 line-height: 1.3;1319 }1320 .pwgen-policy-hint .pwgen-hint-accent { color: var(--peach-accent); }13211322 /* ── Save offer (after password filled) ───────────────────── */1323 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1324 .save-offer-title {1325 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;1326 }1327 .save-offer-row {1328 display: flex; align-items: center; gap: 8px; padding: 6px 8px;1329 border-radius: 7px; background: var(--peach-input-bg);1330 border: 1px solid var(--peach-input-border);1331 }1332 .save-offer-lbl {1333 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;1334 font-family: "IBM Plex Mono", monospace;1335 }1336 .save-offer-val {1337 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1338 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1339 }1340 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }1341 .save-offer-save {1342 flex: 1; padding: 9px; border-radius: 8px; border: none;1343 background: var(--peach-accent); color: #fff; font-size: 13px;1344 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;1345 }1346 .save-offer-save:hover { filter: brightness(1.08); }1347 .save-offer-skip {1348 flex: 1; padding: 9px; border-radius: 8px; border: none;1349 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1350 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;1351 font-family: Inter, sans-serif; transition: background 100ms ease;1352 }1353 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1354`;13551356export function showAutofillSelector(1357 anchorEl: HTMLInputElement,1358 entries: SelectorEntry[],1359 onSelect: (id: string) => void,1360 options: SelectorOptions = {}1361): () => void {1362 const host = document.createElement("div");1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 const modalAncestor = (1377 anchorEl.closest<HTMLElement>('dialog, [aria-modal="true"], [role="dialog"]') ??1378 (() => {1379 let cur: HTMLElement | null = anchorEl.parentElement;1380 while (cur && cur !== document.body) {1381 if (window.getComputedStyle(cur).position === 'fixed') return cur;1382 cur = cur.parentElement;1383 }1384 return null;1385 })()1386 );1387 host.style.cssText = `position: fixed; z-index: 2147483647;`;1388 host.style.setProperty("--peach-accent", accentHex);1389 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));1390 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));1391 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1392 1393 1394 1395 host.style.willChange = "top, left";1396 1397 1398 1399 1400 1401 const shadow = host.attachShadow({ mode: "closed", delegatesFocus: true });14021403 const style = shadow.appendChild(document.createElement("style"));1404 style.textContent = SELECTOR_STYLES;14051406 const rect = anchorEl.getBoundingClientRect();1407 1408 1409 host.style.top = `${rect.bottom + 5}px`;1410 host.style.left = `${rect.left}px`;1411 1412 1413 1414 1415 const popupWidth = Math.min(Math.max(rect.width, 320), 400);1416 host.style.width = `${popupWidth}px`;14171418 const container = shadow.appendChild(document.createElement("div"));1419 container.className = "peach-inline-popup";1420 container.setAttribute("data-theme", popupTheme);14211422 const searchableEntries = options.searchableEntries ?? [];1423 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();14241425 let query = "";1426 let focusedIndex = -1;1427 let currentMode: "list" | "create" | "success" | "silver-platter" | "identity-helper" | "edit" = "list";1428 let searchInput: HTMLInputElement | null = null;1429 let identityMode: IdentityFilterMode = "both";1430 1431 1432 1433 let silverPlatterOrigin: SelectorEntry | null = null;1434 1435 1436 const cleanupHooks: Array<() => void> = [];14371438 1439 1440 1441 {1442 let prevH = 0;1443 let blocked = false;1444 const heightRo = new ResizeObserver(([entry]) => {1445 if (blocked) return;1446 const h = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;1447 if (prevH > 0 && Math.abs(h - prevH) > 1) {1448 blocked = true;1449 container.style.height = `${prevH}px`;1450 requestAnimationFrame(() => requestAnimationFrame(() => {1451 container.style.height = `${h}px`;1452 prevH = h;1453 const onEnd = (e: TransitionEvent) => {1454 if (e.propertyName !== "height") return;1455 container.style.height = "";1456 blocked = false;1457 container.removeEventListener("transitionend", onEnd);1458 };1459 container.addEventListener("transitionend", onEnd);1460 }));1461 } else {1462 prevH = h;1463 }1464 });1465 heightRo.observe(container);1466 cleanupHooks.push(() => heightRo.disconnect());1467 }14681469 1470 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";1471 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");1472 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";14731474 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {1475 const seen = new Set<string>();1476 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });1477 }14781479 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {1480 const q = value.trim().toLowerCase();1481 const deduped = uniqueEntries(source);1482 if (!q) return deduped.slice(0, 8);1483 return deduped.filter(e =>1484 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)1485 ).slice(0, 12);1486 }14871488 function activeEntries(): SelectorEntry[] {1489 1490 if (query.trim()) {1491 const merged = uniqueEntries([...entries, ...searchableEntries]);1492 return filterEntries(merged, query);1493 }1494 1495 if (entries.length > 0) return entries;1496 return filterEntries(searchableEntries, "").slice(0, 8);1497 }14981499 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {1500 const icon = document.createElement("div");1501 icon.className = `entry-icon${isNew ? " is-new" : ""}`;1502 if (isNew) {1503 icon.innerHTML = plusSvgStr();1504 } else {1505 1506 1507 1508 void url;1509 icon.innerHTML = keySvgStr();1510 }1511 return icon;1512 }15131514 function renderIdentityHelper() {1515 currentMode = "identity-helper";1516 container.innerHTML = "";1517 focusedIndex = -1;1518 searchInput = null;15191520 const fallbackKind: IdentityKind = options.identityKind ?? "username";1521 const identities = filterSavedIdentities(options.savedIdentities ?? [], query, identityMode);1522 const typedQuery = query.trim();1523 const customKind = resolveCustomIdentityKind(typedQuery, identityMode, fallbackKind);15241525 function applyIdentity(identity: string) {1526 if (options.hasEmailField) {1527 if (options.identityKind === "email") {1528 options.onFillFields?.("", "", identity); 1529 renderSecondaryIdentityStep("username", identity);1530 } else {1531 options.onFillFields?.(identity, "", ""); 1532 renderSecondaryIdentityStep("email", identity);1533 }1534 } else {1535 options.onFillFields?.(identity, "");1536 renderGenerator(identity);1537 }1538 }15391540 function applyCustomIdentity(identity: string) {1541 const trimmed = identity.trim();1542 if (!trimmed) return;1543 applyIdentity(trimmed);1544 }15451546 1547 const hdr = container.appendChild(document.createElement("div"));1548 hdr.className = "domain-header";1549 const headText = hdr.appendChild(document.createElement("div"));1550 headText.className = "domain-header-text";1551 const eyebrow = headText.appendChild(document.createElement("div"));1552 eyebrow.className = "domain-eyebrow";1553 eyebrow.textContent = "NEW ACCOUNT";1554 const domName = headText.appendChild(document.createElement("div"));1555 domName.className = "domain-name";1556 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");15571558 const actions = hdr.appendChild(document.createElement("div"));1559 actions.className = "identity-mode-toggle";1560 for (const mode of ["both", "email", "username"] as const) {1561 const btn = actions.appendChild(document.createElement("button"));1562 btn.type = "button";1563 btn.className = `identity-mode-btn${identityMode === mode ? " is-active" : ""}`;1564 btn.textContent = identityFilterLabel(mode);1565 btn.setAttribute("aria-pressed", String(identityMode === mode));1566 btn.addEventListener("click", () => {1567 identityMode = mode;1568 renderIdentityHelper();1569 });1570 }15711572 const searchWrap = container.appendChild(document.createElement("div"));1573 searchWrap.className = "search-wrap identity-search-wrap";1574 const search = searchWrap.appendChild(document.createElement("input"));1575 search.className = "search";1576 search.placeholder = "search identities…";1577 search.value = query;1578 search.addEventListener("input", () => {1579 query = search.value;1580 renderIdentityHelper();1581 searchInput?.focus();1582 searchInput?.setSelectionRange(query.length, query.length);1583 });1584 search.addEventListener("keydown", (e) => {1585 if (e.key === "Escape") {1586 e.preventDefault();1587 dismiss(true);1588 return;1589 }1590 if (e.key !== "Enter") return;1591 const exact = identities.find((id) => id.value.toLowerCase() === typedQuery.toLowerCase());1592 if (exact) {1593 e.preventDefault();1594 applyIdentity(exact.value);1595 return;1596 }1597 if (!typedQuery || identities.length > 0) return;1598 e.preventDefault();1599 applyCustomIdentity(typedQuery);1600 });1601 const searchTools = searchWrap.appendChild(document.createElement("div"));1602 searchTools.className = "identity-mode-toggle";1603 const sortHint = searchTools.appendChild(document.createElement("div"));1604 sortHint.className = "identity-empty";1605 sortHint.style.padding = "0";1606 sortHint.textContent = "sorted by recent use";1607 searchInput = search;1608 const customLabel = identityMode === "both"1609 ? customKind1610 : identityMode;16111612 1613 const list = container.appendChild(document.createElement("div"));1614 list.className = "identity-list";1615 1616 1617 1618 1619 const updateListFade = () => {1620 const overflowing = list.scrollHeight > list.clientHeight + 1;1621 const atBottom = list.scrollTop + list.clientHeight >= list.scrollHeight - 2;1622 const mask = overflowing && !atBottom1623 ? "linear-gradient(to bottom, black calc(100% - 24px), transparent 100%)"1624 : "none";1625 list.style.setProperty("mask-image", mask);1626 list.style.setProperty("-webkit-mask-image", mask);1627 };1628 list.addEventListener("scroll", updateListFade);1629 if (typedQuery && identities.length === 0) {1630 const empty = list.appendChild(document.createElement("div"));1631 empty.className = "identity-empty";1632 empty.textContent = identityMode === "both"1633 ? `No match for “${typedQuery}”.`1634 : `No ${identityFilterLabel(identityMode).toLowerCase()} match for “${typedQuery}”.`;1635 const custom = list.appendChild(document.createElement("div"));1636 custom.className = "identity-custom-row";1637 const customTitle = custom.appendChild(document.createElement("div"));1638 customTitle.className = "identity-custom-row-title";1639 customTitle.textContent = `Use “${typedQuery}” as a new ${customLabel}`;1640 const customSub = custom.appendChild(document.createElement("div"));1641 customSub.className = "identity-custom-row-sub";1642 customSub.textContent = "Press Enter or click to continue.";1643 custom.addEventListener("click", () => applyCustomIdentity(typedQuery));1644 }1645 for (const id of identities) {1646 const row = list.appendChild(document.createElement("div"));1647 row.className = "peach-identity-row";1648 row.setAttribute("tabindex", "-1");1649 const val = row.appendChild(document.createElement("div"));1650 val.className = "peach-identity-value";1651 val.textContent = id.value;1652 const meta = row.appendChild(document.createElement("div"));1653 meta.className = "peach-identity-meta";1654 meta.textContent = `used ${id.useCount}×`;1655 row.addEventListener("click", () => {1656 applyIdentity(id.value);1657 });1658 }16591660 updateListFade();16611662 1663 if (identities.length > 0) {1664 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1665 }16661667 1668 const altRow = container.appendChild(document.createElement("div"));1669 altRow.className = "identity-alt-row";1670 altRow.textContent = `+ Use a different ${customLabel}…`;16711672 const altInputWrap = container.appendChild(document.createElement("div"));1673 altInputWrap.className = "identity-alt-input-wrap";1674 const altInput = altInputWrap.appendChild(document.createElement("input"));1675 altInput.className = "identity-alt-input";1676 altInput.type = "text";1677 altInput.placeholder = customLabel === "email" ? "[email protected]" : "your username";1678 altInput.setAttribute("autocomplete", "off");1679 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1680 altConfirm.className = "identity-alt-confirm";1681 altConfirm.type = "button";1682 altConfirm.textContent = "✓";16831684 function fillCustomIdentity() {1685 const v = altInput.value.trim();1686 if (!v) return;1687 applyCustomIdentity(v);1688 }16891690 altRow.addEventListener("click", () => {1691 altRow.style.display = "none";1692 altInputWrap.classList.add("open");1693 altInput.value = typedQuery || altInput.value;1694 altInput.focus();1695 altInput.setSelectionRange(altInput.value.length, altInput.value.length);1696 });1697 altConfirm.addEventListener("click", fillCustomIdentity);1698 altInput.addEventListener("keydown", (e) => {1699 if (e.key === "Enter") { e.preventDefault(); fillCustomIdentity(); }1700 });17011702 1703 1704 1705 1706 1707 if ((options.searchableEntries?.length ?? 0) > 0) {1708 const escapeRow = container.appendChild(document.createElement("div"));1709 escapeRow.className = "identity-escape-row";1710 escapeRow.textContent = "I already have an account →";1711 escapeRow.setAttribute("tabindex", "0");1712 escapeRow.addEventListener("click", () => renderList());1713 escapeRow.addEventListener("keydown", (e) => {1714 if (e.key === "Enter" || e.key === " ") { e.preventDefault(); renderList(); }1715 });1716 }17171718 requestAnimationFrame(() => {1719 search.focus();1720 search.setSelectionRange(query.length, query.length);1721 });1722 }17231724 17251726172717281729 function renderSecondaryIdentityStep(stepKind: "email" | "username", firstValue: string) {1730 currentMode = "identity-helper";1731 container.innerHTML = "";17321733 const wrap = container.appendChild(document.createElement("div"));1734 wrap.className = "gen-view";17351736 1737 const hdrRow = wrap.appendChild(document.createElement("div"));1738 hdrRow.className = "domain-header";1739 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1740 eyebrow2.className = "domain-eyebrow";1741 eyebrow2.textContent = "NEW ACCOUNT";1742 const domName2 = hdrRow.appendChild(document.createElement("div"));1743 domName2.className = "domain-name";1744 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");17451746 1747 const conf = wrap.appendChild(document.createElement("div"));1748 conf.className = "gen-confirmed";1749 const confCheck = conf.appendChild(document.createElement("span"));1750 confCheck.className = "gen-confirmed-check";1751 confCheck.textContent = "✓";1752 const confVal = conf.appendChild(document.createElement("div"));1753 confVal.className = "gen-confirmed-val";1754 confVal.textContent = firstValue;17551756 const stepLbl = wrap.appendChild(document.createElement("div"));1757 stepLbl.className = "gen-label";1758 stepLbl.textContent = stepKind === "email" ? "Now choose your email" : "Now choose your username";17591760 1761 const identities = (options.savedIdentities ?? [])1762 .filter(s => s.type === stepKind)1763 .sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0))1764 .slice(0, 6);17651766 const list = wrap.appendChild(document.createElement("div"));1767 list.className = "identity-list";1768 for (const id of identities) {1769 const row = list.appendChild(document.createElement("div"));1770 row.className = "peach-identity-row";1771 row.setAttribute("tabindex", "-1");1772 const val = row.appendChild(document.createElement("div"));1773 val.className = "peach-identity-value";1774 val.textContent = id.value;1775 const meta = row.appendChild(document.createElement("div"));1776 meta.className = "peach-identity-meta";1777 meta.textContent = `used ${id.useCount}×`;1778 row.addEventListener("click", () => applySecondIdentity(id.value));1779 }17801781 1782 const altInputWrap = wrap.appendChild(document.createElement("div"));1783 altInputWrap.className = "identity-alt-input-wrap open";1784 const altInput = altInputWrap.appendChild(document.createElement("input"));1785 altInput.className = "identity-alt-input";1786 altInput.type = stepKind === "email" ? "email" : "text";1787 altInput.placeholder = stepKind === "email" ? "[email protected]" : "your username";1788 altInput.setAttribute("autocomplete", "off");1789 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1790 altConfirm.className = "identity-alt-confirm";1791 altConfirm.type = "button";1792 altConfirm.textContent = "→";17931794 function applySecondIdentity(value: string) {1795 const trimmed = value.trim();1796 if (!trimmed) return;1797 1798 const [email, username] = stepKind === "email"1799 ? [trimmed, firstValue]1800 : [firstValue, trimmed];1801 if (stepKind === "email") {1802 options.onFillFields?.("", "", trimmed); 1803 } else {1804 options.onFillFields?.(trimmed, "", ""); 1805 }1806 renderGenerator(email, username);1807 }18081809 altConfirm.addEventListener("click", () => applySecondIdentity(altInput.value));1810 altInput.addEventListener("keydown", (e) => {1811 if (e.key === "Enter") { e.preventDefault(); applySecondIdentity(altInput.value); }1812 });18131814 1815 const skipRow = wrap.appendChild(document.createElement("div"));1816 skipRow.className = "identity-escape-row";1817 skipRow.textContent = `Skip ${stepKind} →`;1818 skipRow.setAttribute("tabindex", "0");1819 skipRow.addEventListener("click", () => {1820 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1821 renderGenerator(email, username);1822 });1823 skipRow.addEventListener("keydown", (e) => {1824 if (e.key === "Enter" || e.key === " ") {1825 e.preventDefault();1826 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1827 renderGenerator(email, username);1828 }1829 });18301831 requestAnimationFrame(() => { altInput.focus(); });1832 }18331834 function renderGenerator(filledEmail: string, filledUsername?: string) {1835 container.innerHTML = "";1836 const sitePolicy = detectPasswordPolicy();18371838 const wrap = container.appendChild(document.createElement("div"));1839 wrap.className = "gen-view";18401841 1842 const hdrRow = wrap.appendChild(document.createElement("div"));1843 hdrRow.className = "domain-header";1844 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1845 eyebrow2.className = "domain-eyebrow";1846 eyebrow2.textContent = "NEW ACCOUNT";1847 const domName2 = hdrRow.appendChild(document.createElement("div"));1848 domName2.className = "domain-name";1849 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");18501851 1852 function confirmedRow(value: string) {1853 const conf = wrap.appendChild(document.createElement("div"));1854 conf.className = "gen-confirmed";1855 const confCheck = conf.appendChild(document.createElement("span"));1856 confCheck.className = "gen-confirmed-check";1857 confCheck.textContent = "✓";1858 const confVal = conf.appendChild(document.createElement("div"));1859 confVal.className = "gen-confirmed-val";1860 confVal.textContent = value;1861 }1862 if (filledEmail) confirmedRow(filledEmail);1863 if (filledUsername && filledUsername !== filledEmail) confirmedRow(filledUsername);18641865 const lbl = wrap.appendChild(document.createElement("div"));1866 lbl.className = "gen-label";1867 lbl.textContent = "Generate a password";18681869 let currentPw = "";1870 const smartGen = createSmartGenerator({1871 policy: sitePolicy,1872 onChange: (pw) => { currentPw = pw; },1873 });1874 wrap.appendChild(smartGen.root);18751876 const fillBtn = wrap.appendChild(document.createElement("button"));1877 fillBtn.className = "gen-fill-btn";1878 fillBtn.type = "button";1879 fillBtn.textContent = "Copy & fill";1880 fillBtn.addEventListener("click", async () => {1881 if (!currentPw) return;1882 fillBtn.disabled = true;1883 fillBtn.textContent = "Saving…";1884 1885 options.onFillFields?.("", currentPw, "");1886 1887 1888 const saveUsername = filledUsername || filledEmail;1889 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;1890 try {1891 const res = await chrome.runtime.sendMessage({1892 type: "ADD_ENTRY",1893 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: currentPw, type: "login" },1894 });1895 if ((res as { ok?: boolean }).ok) {1896 showSuccess(sTitle);1897 } else {1898 renderSaveOffer(filledEmail, currentPw, filledUsername);1899 }1900 } catch {1901 renderSaveOffer(filledEmail, currentPw, filledUsername);1902 }1903 });1904 }19051906 function renderSaveOffer(filledEmail: string, filledPassword: string, filledUsername?: string) {1907 container.innerHTML = "";1908 const domain = options.domain ?? location.hostname.replace(/^www\./, "");19091910 const wrap = container.appendChild(document.createElement("div"));1911 wrap.className = "save-offer";19121913 const title = wrap.appendChild(document.createElement("div"));1914 title.className = "save-offer-title";1915 title.textContent = "Save this login to Peach?";19161917 function detailRow(label: string, value: string) {1918 const row = wrap.appendChild(document.createElement("div"));1919 row.className = "save-offer-row";1920 const lbl = row.appendChild(document.createElement("div"));1921 lbl.className = "save-offer-lbl";1922 lbl.textContent = label;1923 const val = row.appendChild(document.createElement("div"));1924 val.className = "save-offer-val";1925 val.textContent = value;1926 }19271928 detailRow("site", domain);1929 1930 if (filledUsername) {1931 detailRow("username", filledUsername);1932 if (filledEmail) detailRow("email", filledEmail);1933 } else {1934 if (filledEmail) detailRow("email", filledEmail);1935 }1936 detailRow("password", "••••••••••••••••");19371938 const btns = wrap.appendChild(document.createElement("div"));1939 btns.className = "save-offer-btns";19401941 const saveBtn = btns.appendChild(document.createElement("button"));1942 saveBtn.className = "save-offer-save";1943 saveBtn.type = "button";1944 saveBtn.textContent = "Save";1945 saveBtn.addEventListener("click", async () => {1946 saveBtn.disabled = true;1947 saveBtn.textContent = "Saving…";1948 try {1949 1950 1951 const saveUsername = filledUsername || filledEmail;1952 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;1953 const res = await chrome.runtime.sendMessage({1954 type: "ADD_ENTRY",1955 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: filledPassword, type: "login" },1956 });1957 if ((res as { ok?: boolean }).ok) {1958 showSuccess(sTitle);1959 } else {1960 saveBtn.disabled = false;1961 saveBtn.textContent = "Save";1962 }1963 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }1964 });19651966 const skipBtn = btns.appendChild(document.createElement("button"));1967 skipBtn.className = "save-offer-skip";1968 skipBtn.type = "button";1969 skipBtn.textContent = "Not now";1970 skipBtn.addEventListener("click", () => { dismiss(true); });1971 }19721973 function renderList() {1974 container.innerHTML = "";1975 focusedIndex = -1;1976 searchInput = null;1977 currentMode = "list";19781979 const current = activeEntries();19801981 1982 1983 1984 const domHdr = document.createElement("div");1985 domHdr.className = "domain-header";1986 const headText = document.createElement("div");1987 headText.className = "domain-header-text";1988 const eyebrow = document.createElement("div");1989 eyebrow.className = "domain-eyebrow";1990 eyebrow.textContent = options.isSignup1991 ? "Sign up as"1992 : entries.length > 0 ? "Saved for this site" : "No saved login";1993 const domName = document.createElement("div");1994 domName.className = "domain-name";1995 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1996 headText.append(eyebrow, domName);1997 domHdr.appendChild(headText);19981999 if (!options.isSignup) {2000 const actions = document.createElement("div");2001 actions.className = "domain-header-actions";20022003 2004 2005 2006 if (silverPlatterOrigin) {2007 const backBtn = document.createElement("button");2008 backBtn.className = "domain-search-btn";2009 backBtn.type = "button";2010 backBtn.setAttribute("aria-label", "Back to suggested entry");2011 backBtn.title = "Back to suggested entry";2012 backBtn.innerHTML = backSvgStr();2013 backBtn.addEventListener("click", (e) => {2014 e.stopPropagation();2015 if (silverPlatterOrigin) renderSilverPlatter(silverPlatterOrigin);2016 });2017 actions.appendChild(backBtn);2018 }20192020 const addBtn = document.createElement("button");2021 addBtn.className = "domain-add-btn";2022 addBtn.type = "button";2023 addBtn.setAttribute("aria-label", "Create new entry");2024 addBtn.title = "Create new entry";2025 addBtn.textContent = "+";2026 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2027 actions.appendChild(addBtn);20282029 domHdr.appendChild(actions);2030 }20312032 container.appendChild(domHdr);20332034 2035 const sw = document.createElement("div"); sw.className = "search-wrap";2036 searchInput = document.createElement("input");2037 searchInput.className = "search";2038 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";2039 searchInput.value = query;2040 searchInput.addEventListener("input", () => {2041 query = searchInput?.value ?? "";2042 renderList();2043 searchInput?.focus();2044 searchInput?.setSelectionRange(query.length, query.length);2045 });2046 sw.appendChild(searchInput);2047 container.appendChild(sw);20482049 2050 const results = document.createElement("div");2051 results.className = "results";2052 container.appendChild(results);20532054 if (current.length === 0 && entries.length > 0) {2055 2056 }20572058 for (let i = 0; i < current.length; i++) {2059 const e = current[i]!;2060 const entryEl = document.createElement("div");2061 entryEl.className = "entry";2062 entryEl.dataset.id = e.id;2063 entryEl.dataset.index = String(i);2064 entryEl.setAttribute("tabindex", "-1");20652066 entryEl.appendChild(makeEntryIcon(e.url));20672068 const info = document.createElement("div"); info.className = "entry-info";2069 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;2070 const subEl = document.createElement("div"); subEl.className = "entry-sub";2071 const domain = e.url ? getDomainFromUrl(e.url) : "";2072 subEl.textContent = domain || e.username || "";2073 info.append(titleEl, subEl);2074 entryEl.appendChild(info);20752076 if (e.hasTotp) {2077 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";2078 entryEl.appendChild(badge);2079 }2080 if (e.hasPasskey) {2081 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";2082 entryEl.appendChild(badge);2083 }20842085 entryEl.addEventListener("click", () => {2086 if (options.isSignup && options.onFillFields) {2087 2088 options.onFillFields(e.username ?? "", "", e.email);2089 dismiss();2090 } else {2091 onSelect(e.id);2092 dismiss();2093 }2094 });2095 results.appendChild(entryEl);2096 }20972098 2099 if (current.length > 0) {2100 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));2101 }21022103 2104 const newEl = document.createElement("div");2105 newEl.className = "entry";2106 newEl.setAttribute("tabindex", "-1");2107 newEl.appendChild(makeEntryIcon(undefined, true));21082109 const newInfo = document.createElement("div"); newInfo.className = "entry-info";2110 const newTitle = document.createElement("div");2111 newTitle.className = "entry-title is-new";2112 newTitle.textContent = `New entry for ${sTitle}`;2113 const newSub = document.createElement("div");2114 newSub.className = "entry-sub";2115 newSub.textContent = sOrigin;2116 newInfo.append(newTitle, newSub);2117 newEl.appendChild(newInfo);2118 newEl.addEventListener("click", showCreateForm);2119 results.appendChild(newEl);21202121 2122 if (searchInput) {2123 requestAnimationFrame(() => {2124 searchInput?.focus();2125 2126 2127 searchInput?.setSelectionRange(query.length, query.length);2128 });2129 }2130 }21312132 function setFocused(idx: number) {2133 const all = container.querySelectorAll<HTMLElement>(".entry");2134 all.forEach((el, i) => el.classList.toggle("focused", i === idx));2135 focusedIndex = idx;2136 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });2137 }21382139 function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[], fieldType: "username" | "email"): void {2140 const typed = identities.filter(s => s.type === fieldType);2141 if (!typed.length) return;2142 let collapsed = false;2143 let suppressNextFocus = false;21442145 const list = g.appendChild(document.createElement("div"));2146 list.className = "identity-suggest-list";2147 list.style.display = "none";21482149 2150 const collapseRow = list.appendChild(document.createElement("div"));2151 collapseRow.className = "identity-filter-row";2152 collapseRow.style.justifyContent = "flex-end";2153 const collapseBtn = collapseRow.appendChild(document.createElement("button"));2154 collapseBtn.type = "button";2155 collapseBtn.className = "identity-collapse-btn";2156 collapseBtn.innerHTML = `<svg width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1L5 5L9 1"/></svg>`;2157 collapseBtn.addEventListener("mousedown", (e) => {2158 e.preventDefault();2159 collapsed = !collapsed;2160 collapseBtn.classList.toggle("is-collapsed", collapsed);2161 rowsWrapper.classList.toggle("is-collapsed", collapsed);2162 });21632164 const rowsWrapper = list.appendChild(document.createElement("div"));2165 rowsWrapper.className = "identity-rows";2166 const rows = rowsWrapper.appendChild(document.createElement("div"));2167 rows.className = "identity-rows-inner";21682169 2170 2171 2172 const updateRowsFade = () => {2173 const overflowing = rows.scrollHeight > rows.clientHeight + 1;2174 const atBottom = rows.scrollTop + rows.clientHeight >= rows.scrollHeight - 2;2175 const mask = overflowing && !atBottom2176 ? "linear-gradient(to bottom, black calc(100% - 18px), transparent 100%)"2177 : "none";2178 rows.style.setProperty("mask-image", mask);2179 rows.style.setProperty("-webkit-mask-image", mask);2180 };2181 rows.addEventListener("scroll", updateRowsFade);21822183 2184 function hideList() {2185 list.style.display = "none";2186 document.removeEventListener("mousedown", onDocMousedown, { capture: true });2187 }21882189 2190 2191 2192 2193 2194 function onDocMousedown(e: MouseEvent) {2195 if (e.composedPath().includes(host)) return;2196 hideList();2197 }21982199 function pick(value: string) {2200 inp.value = value;2201 hideList();2202 suppressNextFocus = true;2203 inp.focus();2204 2205 2206 2207 setTimeout(() => { suppressNextFocus = false; }, 120);2208 }22092210 function render() {2211 const q = inp.value.toLowerCase();2212 const filtered = typed.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 8);2213 rows.innerHTML = "";2214 if (!filtered.length) { hideList(); return; }2215 for (const s of filtered) {2216 const row = rows.appendChild(document.createElement("button"));2217 row.type = "button"; row.className = "identity-suggest-row";2218 const val = row.appendChild(document.createElement("span"));2219 val.className = "identity-suggest-val"; val.textContent = s.value;2220 const meta = row.appendChild(document.createElement("span"));2221 meta.className = "identity-suggest-meta"; meta.textContent = `${s.useCount}×`;2222 row.addEventListener("mousedown", (e) => { e.preventDefault(); pick(s.value); });2223 row.addEventListener("keydown", (e) => {2224 if (e.key === "ArrowDown") {2225 e.preventDefault();2226 (row.nextElementSibling as HTMLElement | null)?.focus();2227 } else if (e.key === "ArrowUp") {2228 e.preventDefault();2229 const prev = row.previousElementSibling as HTMLElement | null;2230 if (prev) prev.focus();2231 else inp.focus();2232 } else if (e.key === "Enter" || e.key === " ") {2233 e.preventDefault();2234 pick(s.value);2235 } else if (e.key === "Escape") {2236 e.preventDefault();2237 hideList();2238 suppressNextFocus = true;2239 inp.focus();2240 }2241 });2242 2243 2244 2245 row.addEventListener("blur", (e) => {2246 const rt = (e as FocusEvent).relatedTarget as Node | null;2247 if (rt && (rt === inp || list.contains(rt))) return;2248 setTimeout(() => { hideList(); }, 150);2249 });2250 }2251 list.style.display = "";2252 updateRowsFade();2253 2254 2255 2256 document.addEventListener("mousedown", onDocMousedown, { capture: true });2257 }22582259 inp.addEventListener("focus", () => {2260 if (suppressNextFocus) { suppressNextFocus = false; return; }2261 render();2262 });2263 inp.addEventListener("input", render);2264 inp.addEventListener("keydown", (e) => {2265 if (list.style.display === "none") return;2266 if (e.key === "ArrowDown" && !collapsed) {2267 e.preventDefault();2268 (rows.firstElementChild as HTMLElement | null)?.focus();2269 } else if (e.key === "Escape") {2270 e.preventDefault();2271 hideList();2272 } else if (e.key === "Tab") {2273 2274 hideList();2275 }2276 });2277 2278 2279 2280 2281 22822283 2284 2285 cleanupHooks.push(hideList);2286 }22872288 async function showCreateForm() {2289 currentMode = "create";2290 container.innerHTML = "";22912292 2293 2294 2295 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;2296 if (!statusRes?.unlocked) {2297 renderLocked();2298 return;2299 }23002301 2302 2303 const sitePolicy = detectPasswordPolicy();23042305 2306 2307 let formIdentities: SavedIdentity[] = options.savedIdentities ?? [];2308 if (!formIdentities.length) {2309 try {2310 const idRes = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as { ok: boolean; data?: SavedIdentity[] } | null;2311 if (idRes?.ok && Array.isArray(idRes.data)) formIdentities = idRes.data;2312 } catch { }2313 }23142315 const wrap = document.createElement("div");2316 wrap.className = "create-form-wrap";23172318 2319 const head = document.createElement("div"); head.className = "create-head";2320 const backBtn = document.createElement("button"); backBtn.className = "back-btn";2321 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>`;2322 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });2323 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";2324 head.append(backBtn, lbl);23252326 2327 const body = document.createElement("div"); body.className = "create-body";23282329 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {2330 const g = document.createElement("div"); g.className = "f-group";2331 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;2332 const inp = document.createElement("input"); inp.className = "f-input";2333 g.append(l, inp); return { g, inp };2334 }23352336 2337 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";2338 body.appendChild(tg.g);23392340 2341 const ug = fGroup("website");2342 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";2343 if (loginSubdomain) {2344 const h = document.createElement("div"); h.className = "url-hint";2345 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);2346 }2347 body.appendChild(ug.g);23482349 2350 const xg = fGroup("username");2351 xg.inp.value = preUsername && !preUsername.includes("@") ? preUsername : "";2352 xg.inp.placeholder = "johndoe";2353 xg.inp.setAttribute("autocomplete", "off");2354 if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities, "username");2355 body.appendChild(xg.g);23562357 2358 const eg = fGroup("email");2359 (eg.inp as HTMLInputElement).type = "email";2360 eg.inp.value = preUsername && preUsername.includes("@") ? preUsername : "";2361 eg.inp.placeholder = "[email protected]";2362 eg.inp.setAttribute("autocomplete", "off");2363 if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities, "email");2364 body.appendChild(eg.g);23652366 2367 2368 2369 const pg = fGroup("password");2370 pg.inp.remove(); 2371 let currentPassword = prePassword || "";2372 const smartGen = createSmartGenerator({2373 policy: sitePolicy,2374 initialPassword: prePassword || undefined,2375 onChange: (pw) => { currentPassword = pw; },2376 });2377 pg.g.appendChild(smartGen.root);2378 body.appendChild(pg.g);23792380 2381 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";2382 saveBtn.textContent = "Save entry";2383 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";2384 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }2385 saveBtn.addEventListener("click", async () => {2386 const title = tg.inp.value.trim();2387 const pw = currentPassword.trim();2388 const username = xg.inp.value.trim();2389 const email = eg.inp.value.trim();2390 if (!title) {2391 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();2392 showFormError("Entry needs a title");2393 return;2394 }2395 if (!pw) {2396 showFormError("Entry needs a password — tap regen to make one");2397 return;2398 }2399 errEl.style.display = "none";2400 saveBtn.disabled = true; saveBtn.textContent = "Saving…";2401 try {2402 const res = await chrome.runtime.sendMessage({2403 type: "ADD_ENTRY",2404 payload: { title, url: ug.inp.value.trim(), username, email: email || undefined, password: pw, type: "login" },2405 });2406 if ((res as { ok?: boolean }).ok) {2407 2408 2409 options.onFillFields?.(username || email, pw);2410 showSuccess(title);2411 } else {2412 const err = (res as { error?: string })?.error;2413 const errMsg = err === "vault_locked"2414 ? "Vault locked — open Peach to save"2415 : err === "STORAGE_QUOTA_EXCEEDED"2416 ? "Out of storage — open Peach → Settings to free space"2417 : err2418 ? `Could not save: ${err}`2419 : "Could not save entry";2420 showFormError(errMsg);2421 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2422 }2423 } catch (e) {2424 showFormError(e instanceof Error && e.message ? `Could not reach Peach: ${e.message}` : "Could not reach Peach");2425 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2426 }2427 });2428 body.appendChild(saveBtn);2429 body.appendChild(errEl);24302431 wrap.append(head, body);2432 container.appendChild(wrap);2433 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });2434 }24352436 function showSuccess(title: string) {2437 currentMode = "success";2438 container.innerHTML = "";2439 const wrap = document.createElement("div"); wrap.className = "success-wrap";2440 const ring = document.createElement("div"); ring.className = "success-ring";2441 ring.innerHTML = checkSvgStr();2442 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;2443 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;2444 wrap.append(ring, t, s);2445 container.appendChild(wrap);2446 setTimeout(() => dismiss(), 1600);2447 }24482449 function flashActionBtn(btn: HTMLButtonElement) {2450 btn.classList.add("flashed");2451 setTimeout(() => btn.classList.remove("flashed"), 700);2452 }24532454 2455 2456 function flashCopyError(btn: HTMLButtonElement) {2457 const original = btn.textContent ?? "";2458 btn.textContent = "Blocked";2459 btn.classList.add("flashed");2460 setTimeout(() => {2461 btn.classList.remove("flashed");2462 if (btn.isConnected) btn.textContent = original;2463 }, 1200);2464 }24652466 function renderSilverPlatter(entry: SelectorEntry) {2467 container.innerHTML = "";2468 searchInput = null;2469 focusedIndex = -1;2470 currentMode = "silver-platter";2471 silverPlatterOrigin = entry;24722473 const domHdr = document.createElement("div");2474 domHdr.className = "domain-header";2475 const headText = document.createElement("div");2476 headText.className = "domain-header-text";2477 const eyebrow = document.createElement("div");2478 eyebrow.className = "domain-eyebrow";2479 eyebrow.textContent = "Saved for this site";2480 const domName = document.createElement("div");2481 domName.className = "domain-name";2482 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2483 headText.append(eyebrow, domName);2484 domHdr.appendChild(headText);2485 if (!options.isSignup) {2486 const actions = document.createElement("div");2487 actions.className = "domain-header-actions";24882489 2490 2491 2492 const searchBtn = document.createElement("button");2493 searchBtn.className = "domain-search-btn";2494 searchBtn.type = "button";2495 searchBtn.setAttribute("aria-label", "Search for a different entry");2496 searchBtn.title = "Search for a different entry";2497 searchBtn.innerHTML = searchSvgStr();2498 searchBtn.addEventListener("click", (e) => {2499 e.stopPropagation();2500 query = "";2501 renderList();2502 });2503 actions.appendChild(searchBtn);25042505 const addBtn = document.createElement("button");2506 addBtn.className = "domain-add-btn";2507 addBtn.type = "button";2508 addBtn.setAttribute("aria-label", "Create new entry");2509 addBtn.title = "Create new entry";2510 addBtn.textContent = "+";2511 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2512 actions.appendChild(addBtn);25132514 domHdr.appendChild(actions);2515 }2516 container.appendChild(domHdr);25172518 const platter = document.createElement("div");2519 platter.className = "silver-platter";25202521 const titleEl = document.createElement("div");2522 titleEl.className = "sp-title";2523 titleEl.textContent = entry.title;2524 platter.appendChild(titleEl);25252526 if (entry.email) {2527 const emailEl = document.createElement("div");2528 emailEl.className = "sp-identity-row";2529 const emailLbl = document.createElement("span");2530 emailLbl.className = "sp-identity-label";2531 emailLbl.textContent = "email";2532 const emailVal = document.createElement("span");2533 emailVal.className = "sp-username";2534 emailVal.textContent = entry.email;2535 emailEl.append(emailLbl, emailVal);2536 platter.appendChild(emailEl);2537 }25382539 if (entry.username) {2540 const userEl = document.createElement("div");2541 userEl.className = "sp-identity-row";2542 const userLbl = document.createElement("span");2543 userLbl.className = "sp-identity-label";2544 userLbl.textContent = entry.username.includes("@") ? "email" : "username";2545 const userVal = document.createElement("span");2546 userVal.className = "sp-username";2547 userVal.textContent = entry.username;2548 userEl.append(userLbl, userVal);2549 platter.appendChild(userEl);2550 }25512552 const fillBtn = document.createElement("button");2553 fillBtn.className = "sp-fill-btn";2554 fillBtn.type = "button";2555 fillBtn.textContent = "Fill credentials";2556 fillBtn.addEventListener("click", () => {2557 if (fillBtn.disabled) return;2558 fillBtn.disabled = true;2559 fillBtn.classList.add("filled");2560 fillBtn.innerHTML = checkSvgStr();2561 2562 2563 2564 2565 onSelect(entry.id);2566 setTimeout(() => dismiss(), 400);2567 });2568 platter.appendChild(fillBtn);25692570 const actionsGroup = document.createElement("div");2571 actionsGroup.className = "sp-actions-group";2572 const actionsLabel = document.createElement("div");2573 actionsLabel.className = "sp-actions-label";2574 actionsLabel.textContent = "Copy";2575 actionsGroup.appendChild(actionsLabel);25762577 const actions = document.createElement("div");2578 actions.className = "sp-actions";2579 actionsGroup.appendChild(actions);25802581 if (entry.email) {2582 const copyEmail = document.createElement("button");2583 copyEmail.className = "sp-action-btn";2584 copyEmail.type = "button";2585 copyEmail.title = "Copy email";2586 copyEmail.textContent = "Email";2587 copyEmail.addEventListener("click", async (e) => {2588 e.stopPropagation();2589 let alreadyWritten = false;2590 try {2591 await navigator.clipboard.writeText(entry.email!);2592 alreadyWritten = true;2593 } catch (err) {2594 console.warn("[Peach] clipboard write denied:", err);2595 }2596 const resp = await chrome.runtime.sendMessage({2597 type: "COPY",2598 payload: { text: entry.email, label: "Email", alreadyWritten },2599 }).catch(() => null) as { ok?: boolean } | null;2600 if (alreadyWritten || resp?.ok) flashActionBtn(copyEmail);2601 else flashCopyError(copyEmail);2602 });2603 actions.appendChild(copyEmail);2604 }26052606 const copyU = document.createElement("button");2607 copyU.className = "sp-action-btn";2608 copyU.type = "button";2609 const usernameIsEmail = entry.username?.includes("@") ?? false;2610 copyU.title = usernameIsEmail ? "Copy email" : "Copy username";2611 copyU.textContent = usernameIsEmail ? "Email" : "Username";2612 copyU.disabled = !entry.username;2613 copyU.addEventListener("click", async (e) => {2614 e.stopPropagation();2615 if (!entry.username) return;2616 let alreadyWritten = false;2617 try {2618 await navigator.clipboard.writeText(entry.username);2619 alreadyWritten = true;2620 } catch (err) {2621 console.warn("[Peach] clipboard write denied:", err);2622 }2623 const resp = await chrome.runtime.sendMessage({2624 type: "COPY",2625 payload: { text: entry.username, label: entry.username.includes("@") ? "Email" : "Username", alreadyWritten },2626 }).catch(() => null) as { ok?: boolean } | null;2627 if (alreadyWritten || resp?.ok) flashActionBtn(copyU);2628 else flashCopyError(copyU);2629 });2630 actions.appendChild(copyU);26312632 const copyP = document.createElement("button");2633 copyP.className = "sp-action-btn";2634 copyP.type = "button";2635 copyP.title = "Copy password";2636 copyP.textContent = "Password";2637 copyP.addEventListener("click", async (e) => {2638 e.stopPropagation();2639 const resp = await chrome.runtime.sendMessage({2640 type: "GET_ENTRY_FOR_FILL",2641 payload: { id: entry.id },2642 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;2643 if (!resp?.ok || !resp.data?.password) return;2644 let alreadyWritten = false;2645 try {2646 await navigator.clipboard.writeText(resp.data.password);2647 alreadyWritten = true;2648 } catch (err) {2649 console.warn("[Peach] clipboard write denied:", err);2650 }2651 const copyResp = await chrome.runtime.sendMessage({2652 type: "COPY",2653 payload: { text: resp.data.password, label: "Password", alreadyWritten },2654 }).catch(() => null) as { ok?: boolean } | null;2655 if (alreadyWritten || copyResp?.ok) flashActionBtn(copyP);2656 else flashCopyError(copyP);2657 });2658 actions.appendChild(copyP);26592660 let trayEl: HTMLDivElement | null = null;2661 let trayInterval: ReturnType<typeof setInterval> | null = null;26622663 function closeTray() {2664 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }2665 trayEl?.remove();2666 trayEl = null;2667 }2668 2669 2670 cleanupHooks.push(closeTray);26712672 async function openTOTPTray() {2673 2674 2675 2676 2677 async function fetchCode(): Promise<{ code: string; remaining: number } | null> {2678 const r = await chrome.runtime.sendMessage({2679 type: "GET_ENTRY_FOR_FILL",2680 payload: { id: entry.id },2681 }).catch(() => null) as { ok: boolean; data?: { totpCode?: string; totpRemaining?: number } } | null;2682 if (!r?.ok || !r.data?.totpCode) return null;2683 return { code: r.data.totpCode, remaining: r.data.totpRemaining ?? 30 };2684 }2685 let current = await fetchCode();2686 if (!current) return;26872688 trayEl = document.createElement("div");2689 trayEl.className = "sp-tray";26902691 const label = document.createElement("span");2692 label.className = "sp-tray-label";2693 label.textContent = "2FA";2694 trayEl.appendChild(label);26952696 const codeEl = document.createElement("div");2697 codeEl.className = "sp-totp-code";2698 trayEl.appendChild(codeEl);26992700 const ringNs = "http://www.w3.org/2000/svg";2701 const ring = document.createElementNS(ringNs, "svg");2702 ring.setAttribute("class", "sp-totp-ring");2703 ring.setAttribute("viewBox", "0 0 24 24");2704 const trackC = document.createElementNS(ringNs, "circle");2705 trackC.setAttribute("class", "ring-track");2706 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");2707 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");2708 const fillC = document.createElementNS(ringNs, "circle");2709 fillC.setAttribute("class", "ring-fill");2710 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");2711 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");2712 fillC.setAttribute("stroke-linecap", "round");2713 fillC.setAttribute("stroke-dasharray", "62.83");2714 ring.append(trackC, fillC);2715 trayEl.appendChild(ring);27162717 const copyBtn = document.createElement("button");2718 copyBtn.className = "sp-totp-copy";2719 copyBtn.type = "button";2720 copyBtn.textContent = "Copy";2721 trayEl.appendChild(copyBtn);27222723 function render() {2724 const { code, remaining } = current!;2725 2726 codeEl.textContent = code.length === 62727 ? `${code.slice(0, 3)} ${code.slice(3)}`2728 : code;2729 2730 2731 const fraction = (30 - remaining) / 30;2732 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));2733 }2734 render();2735 trayInterval = setInterval(async () => {2736 if (!current) return;2737 current = { code: current.code, remaining: current.remaining - 1 };2738 2739 if (current.remaining <= 0) {2740 const next = await fetchCode();2741 current = next ?? { code: current.code, remaining: 30 };2742 }2743 render();2744 }, 1000);27452746 copyBtn.addEventListener("click", async (e) => {2747 e.stopPropagation();2748 const code = current?.code;2749 if (!code) return;2750 let alreadyWritten = false;2751 try {2752 await navigator.clipboard.writeText(code);2753 alreadyWritten = true;2754 } catch (err) {2755 console.warn("[Peach] clipboard write denied:", err);2756 }2757 const resp = await chrome.runtime.sendMessage({2758 type: "COPY",2759 payload: { text: code, label: "TOTP", alreadyWritten },2760 }).catch(() => null) as { ok?: boolean } | null;2761 copyBtn.textContent = alreadyWritten || resp?.ok ? "Copied" : "Blocked";2762 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);2763 });27642765 platter.appendChild(trayEl);2766 }27672768 const editBtn = document.createElement("button");2769 editBtn.className = "sp-action-btn sp-action-edit";2770 editBtn.type = "button";2771 editBtn.title = "Edit entry";2772 editBtn.textContent = "Edit";2773 editBtn.addEventListener("click", (e) => {2774 e.stopPropagation();2775 closeTray();2776 void renderInlineEdit(entry);2777 });2778 actions.appendChild(editBtn);27792780 2781 2782 2783 if (entry.hasTotp) void openTOTPTray();27842785 platter.appendChild(actionsGroup);2786 container.appendChild(platter);2787 }27882789 279027912792279327942795 async function renderInlineEdit(summary: SelectorEntry) {2796 currentMode = "edit";2797 container.innerHTML = "";2798 const wrap = document.createElement("div");2799 wrap.className = "create-form-wrap";28002801 const head = document.createElement("div");2802 head.className = "create-head";2803 const back = document.createElement("button");2804 back.className = "back-btn";2805 back.type = "button";2806 back.title = "Cancel";2807 back.innerHTML = `<svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><path d="M7 2L3 5.5L7 9"/></svg>`;2808 back.addEventListener("click", () => renderSilverPlatter(summary));2809 head.appendChild(back);2810 const headLbl = document.createElement("div");2811 headLbl.className = "create-label";2812 headLbl.textContent = "Edit entry";2813 head.appendChild(headLbl);2814 wrap.appendChild(head);28152816 const body = document.createElement("div");2817 body.className = "create-body";28182819 2820 const loading = document.createElement("div");2821 loading.className = "empty-hint";2822 loading.textContent = "loading…";2823 body.appendChild(loading);2824 wrap.appendChild(body);2825 container.appendChild(wrap);28262827 const [res, idRes] = await Promise.all([2828 2829 2830 chrome.runtime.sendMessage({2831 type: "GET_ENTRY_FOR_EDIT",2832 payload: { id: summary.id },2833 }).catch(() => null),2834 chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null),2835 ]);2836 const entryRes = res as { ok: boolean; data?: { title?: string; username?: string; email?: string; password?: string; url?: string; totp?: string; notes?: string; passwordHistory?: PasswordHistoryItem[] } } | null;2837 const editIdentities: SavedIdentity[] = (idRes as { ok?: boolean; data?: SavedIdentity[] } | null)?.data ?? [];28382839 if (!entryRes || !entryRes.ok || !entryRes.data) {2840 loading.textContent = "couldn't load this entry. try again from the popup.";2841 return;2842 }2843 body.innerHTML = "";28442845 function field(label: string, value: string, type: "text" | "password" = "text"): { g: HTMLDivElement; inp: HTMLInputElement } {2846 const group = document.createElement("div");2847 group.className = "f-group";2848 const lbl = document.createElement("div");2849 lbl.className = "f-label";2850 lbl.textContent = label;2851 const input = document.createElement("input");2852 input.className = "f-input";2853 input.type = type;2854 input.value = value;2855 input.autocomplete = "off";2856 group.append(lbl, input);2857 body.appendChild(group);2858 return { g: group, inp: input };2859 }28602861 const titleInput = field("title", entryRes.data.title ?? "").inp;2862 const urlInput = field("url", entryRes.data.url ?? "").inp;2863 urlInput.classList.add("url-f");2864 const usernameLabel = (entryRes.data.username ?? "").includes("@") ? "email" : "username";2865 const { g: usernameGroup, inp: usernameInput } = field(usernameLabel, entryRes.data.username ?? "");2866 if (editIdentities.length) attachIdentitySuggestions(usernameGroup, usernameInput, editIdentities, "username");2867 const { g: emailGroup, inp: emailInput } = field("email", entryRes.data.email ?? "");2868 if (editIdentities.length) attachIdentitySuggestions(emailGroup, emailInput, editIdentities, "email");28692870 2871 const pwGroup = document.createElement("div");2872 pwGroup.className = "f-group";2873 const pwLbl = document.createElement("div");2874 pwLbl.className = "f-label";2875 pwLbl.textContent = "password";2876 const pwRow = document.createElement("div");2877 pwRow.className = "f-row";2878 const pwInput = document.createElement("input");2879 pwInput.className = "f-input";2880 pwInput.type = "password";2881 pwInput.value = entryRes.data.password ?? "";2882 pwInput.autocomplete = "new-password";2883 const revealBtn = document.createElement("button");2884 revealBtn.className = "gen-btn";2885 revealBtn.type = "button";2886 revealBtn.textContent = "show";2887 revealBtn.addEventListener("click", () => {2888 const reveal = pwInput.type === "password";2889 pwInput.type = reveal ? "text" : "password";2890 revealBtn.textContent = reveal ? "hide" : "show";2891 });2892 pwRow.append(pwInput, revealBtn);2893 const editSmartGen = createSmartGenerator({ policy: detectPasswordPolicy(), onChange: (pw) => { pwInput.value = pw; } });2894 editSmartGen.root.style.display = "none";2895 let editGenVisible = false;2896 const genToggleBtn = document.createElement("button");2897 genToggleBtn.className = "gen-toggle-link";2898 genToggleBtn.type = "button";2899 genToggleBtn.textContent = "⚙ generate";2900 genToggleBtn.addEventListener("click", () => {2901 editGenVisible = !editGenVisible;2902 pwRow.style.display = editGenVisible ? "none" : "";2903 editSmartGen.root.style.display = editGenVisible ? "" : "none";2904 genToggleBtn.textContent = editGenVisible ? "☰ hide generator" : "⚙ generate";2905 });2906 pwGroup.append(pwLbl, pwRow, genToggleBtn, editSmartGen.root);29072908 2909 2910 2911 const history = entryRes.data.passwordHistory ?? [];2912 if (history.length > 0) {2913 const histList = document.createElement("div");2914 histList.className = "pw-history";2915 histList.style.display = "none";2916 let histVisible = false;29172918 const histToggle = document.createElement("button");2919 histToggle.className = "gen-toggle-link";2920 histToggle.type = "button";2921 histToggle.textContent = `↺ history (${history.length})`;2922 histToggle.addEventListener("click", () => {2923 histVisible = !histVisible;2924 histList.style.display = histVisible ? "" : "none";2925 histToggle.textContent = histVisible ? "↺ hide history" : `↺ history (${history.length})`;2926 });29272928 for (const item of history) {2929 const row = document.createElement("div");2930 row.className = "pw-history-row";2931 const main = document.createElement("div");2932 main.className = "pw-history-main";2933 const pwText = document.createElement("div");2934 pwText.className = "pw-history-pw";2935 let revealed = false;2936 const mask = "••••••••••••";2937 pwText.textContent = mask;2938 const age = document.createElement("div");2939 age.className = "pw-history-age";2940 age.textContent = timeSince(item.changedAt);2941 main.append(pwText, age);29422943 const acts = document.createElement("div");2944 acts.className = "pw-history-actions";2945 const revealBtn2 = document.createElement("button");2946 revealBtn2.className = "pw-history-btn";2947 revealBtn2.type = "button";2948 revealBtn2.textContent = "show";2949 revealBtn2.addEventListener("click", () => {2950 revealed = !revealed;2951 pwText.textContent = revealed ? item.password : mask;2952 revealBtn2.textContent = revealed ? "hide" : "show";2953 });2954 const copyBtn2 = document.createElement("button");2955 copyBtn2.className = "pw-history-btn";2956 copyBtn2.type = "button";2957 copyBtn2.textContent = "copy";2958 copyBtn2.addEventListener("click", async () => {2959 let alreadyWritten = false;2960 try {2961 await navigator.clipboard.writeText(item.password);2962 alreadyWritten = true;2963 } catch (err) {2964 console.warn("[Peach] clipboard write denied:", err);2965 }2966 await chrome.runtime.sendMessage({2967 type: "COPY",2968 payload: { text: item.password, label: "Password", alreadyWritten },2969 }).catch(() => null);2970 copyBtn2.textContent = "copied";2971 setTimeout(() => { if (copyBtn2.isConnected) copyBtn2.textContent = "copy"; }, 1200);2972 });2973 const useBtn = document.createElement("button");2974 useBtn.className = "pw-history-btn";2975 useBtn.type = "button";2976 useBtn.textContent = "use";2977 useBtn.title = "Use this password again";2978 useBtn.addEventListener("click", () => {2979 pwInput.value = item.password;2980 pwInput.type = "text";2981 revealBtn.textContent = "hide";2982 if (editGenVisible) {2983 editGenVisible = false;2984 pwRow.style.display = "";2985 editSmartGen.root.style.display = "none";2986 genToggleBtn.textContent = "⚙ generate";2987 }2988 histVisible = false;2989 histList.style.display = "none";2990 histToggle.textContent = `↺ history (${history.length})`;2991 });2992 acts.append(revealBtn2, copyBtn2, useBtn);2993 row.append(main, acts);2994 histList.appendChild(row);2995 }29962997 pwGroup.append(histToggle, histList);2998 }29993000 body.appendChild(pwGroup);30013002 const totpInput = field("2fa secret", entryRes.data.totp ?? "").inp;30033004 3005 const notesGroup = document.createElement("div");3006 notesGroup.className = "f-group";3007 const notesLbl = document.createElement("div");3008 notesLbl.className = "f-label";3009 notesLbl.textContent = "notes";3010 const notesInput = document.createElement("textarea");3011 notesInput.className = "f-input";3012 notesInput.rows = 3;3013 notesInput.value = entryRes.data.notes ?? "";3014 notesInput.style.fontFamily = "Inter, sans-serif";3015 notesInput.style.resize = "vertical";3016 notesGroup.append(notesLbl, notesInput);3017 body.appendChild(notesGroup);30183019 3020 const actionsRow = document.createElement("div");3021 actionsRow.className = "f-row";3022 const saveBtn = document.createElement("button");3023 saveBtn.className = "save-btn";3024 saveBtn.type = "button";3025 saveBtn.textContent = "Save changes";3026 saveBtn.style.flex = "1";3027 const cancelBtn = document.createElement("button");3028 cancelBtn.className = "gen-btn";3029 cancelBtn.type = "button";3030 cancelBtn.textContent = "cancel";3031 cancelBtn.addEventListener("click", () => renderSilverPlatter(summary));3032 actionsRow.append(saveBtn, cancelBtn);3033 body.appendChild(actionsRow);30343035 saveBtn.addEventListener("click", async () => {3036 saveBtn.disabled = true;3037 const originalLabel = saveBtn.textContent;3038 saveBtn.textContent = "Saving…";3039 const patch: Record<string, unknown> = {3040 title: titleInput.value.trim(),3041 url: urlInput.value.trim(),3042 username: usernameInput.value.trim(),3043 email: emailInput.value.trim() || undefined,3044 password: pwInput.value,3045 totp: totpInput.value.trim(),3046 notes: notesInput.value,3047 };3048 const updateRes = (await chrome.runtime.sendMessage({3049 type: "UPDATE_ENTRY",3050 payload: { id: summary.id, patch },3051 }).catch(() => null)) as { ok: boolean; error?: string } | null;3052 if (!updateRes?.ok) {3053 saveBtn.disabled = false;3054 saveBtn.textContent = originalLabel;3055 const err = document.createElement("div");3056 err.className = "empty-hint";3057 err.style.color = "color-mix(in srgb, #e55 70%, transparent)";3058 err.textContent = updateRes?.error ?? "Couldn't save. Try the popup.";3059 body.appendChild(err);3060 setTimeout(() => err.remove(), 3000);3061 return;3062 }3063 3064 const refreshed: SelectorEntry = {3065 id: summary.id,3066 title: patch.title as string,3067 username: patch.username as string,3068 email: patch.email as string | undefined,3069 url: patch.url as string,3070 hasTotp: Boolean(patch.totp),3071 hasPasskey: summary.hasPasskey,3072 };3073 renderSilverPlatter(refreshed);3074 });30753076 queueMicrotask(() => titleInput.focus());3077 }30783079 function renderLocked() {3080 container.innerHTML = "";30813082 3083 const domHdr = document.createElement("div");3084 domHdr.className = "domain-header";3085 const eyebrow = document.createElement("div");3086 eyebrow.className = "domain-eyebrow";3087 eyebrow.textContent = "vault locked";3088 const domName = document.createElement("div");3089 domName.className = "domain-name";3090 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");3091 domHdr.append(eyebrow, domName);3092 container.appendChild(domHdr);30933094 const wrap = document.createElement("div");3095 wrap.className = "locked-wrap";30963097 3098 const head = document.createElement("div"); head.className = "locked-head";3099 const icon = document.createElement("div"); icon.className = "locked-icon";3100 icon.innerHTML = keySvgStr();3101 const title = document.createElement("div"); title.className = "locked-title";3102 title.textContent = "unlock to autofill";3103 head.append(icon, title);31043105 3106 3107 3108 3109 3110 3111 3112 3113 const sub = document.createElement("div");3114 sub.className = "locked-sub";3115 sub.textContent = "Unlock in Peach — your master password is never typed on this page.";31163117 const openBtn = document.createElement("button");3118 openBtn.className = "locked-btn";3119 openBtn.type = "button";3120 openBtn.textContent = "Open Peach to unlock";3121 openBtn.addEventListener("click", () => {3122 chrome.runtime.sendMessage({ type: "OPEN_POPUP" }).catch(() => {});3123 openBtn.textContent = "opening Peach…";3124 openBtn.disabled = true;3125 3126 3127 setTimeout(() => {3128 if (openBtn.isConnected) {3129 openBtn.textContent = "Open Peach to unlock";3130 openBtn.disabled = false;3131 }3132 }, 2500);3133 });31343135 wrap.append(head, sub, openBtn);3136 container.appendChild(wrap);3137 }31383139 function handleKeyDown(e: KeyboardEvent) {3140 if (options.locked) {3141 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3142 return;3143 }3144 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter" || currentMode === "identity-helper" || currentMode === "edit") {3145 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3146 return;3147 }3148 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");3149 const count = allEntryEls.length;3150 const activeElement = shadow.activeElement ?? document.activeElement;3151 if (activeElement === searchInput) {3152 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }3153 if (count === 0) return;3154 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;3155 }3156 if (count === 0) return;3157 switch (e.key) {3158 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;3159 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;3160 case "Enter": {3161 if (focusedIndex >= 0) {3162 const el = allEntryEls[focusedIndex] as HTMLElement;3163 const id = el.dataset.id;3164 if (id) { onSelect(id); dismiss(); }3165 else if (el.classList.contains("is-new") || !id) {3166 3167 showCreateForm();3168 }3169 }3170 break;3171 }3172 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;3173 }3174 }31753176 document.addEventListener("keydown", handleKeyDown);31773178 3179 3180 3181 3182 3183 const silverPlatterTypeHandler = (e: KeyboardEvent) => {3184 if (currentMode !== "silver-platter") return;3185 if (e.ctrlKey || e.metaKey || e.altKey) return;3186 if (e.key.length !== 1) return;3187 e.preventDefault();3188 e.stopImmediatePropagation();3189 query = e.key;3190 renderList();3191 };3192 document.addEventListener("keydown", silverPlatterTypeHandler, true);3193 cleanupHooks.push(() => document.removeEventListener("keydown", silverPlatterTypeHandler, true));31943195 const identityHelperTypeHandler = (e: KeyboardEvent) => {3196 if (currentMode !== "identity-helper") return;3197 if (e.ctrlKey || e.metaKey || e.altKey) return;3198 const active = (shadow.activeElement ?? document.activeElement) as Element | null;3199 if (active === searchInput) return;3200 if (3201 active instanceof HTMLInputElement ||3202 active instanceof HTMLTextAreaElement ||3203 (active instanceof HTMLElement && active.isContentEditable)3204 ) {3205 return;3206 }3207 if (e.key.length !== 1) return;3208 e.preventDefault();3209 e.stopImmediatePropagation();3210 query = e.key;3211 renderIdentityHelper();3212 };3213 document.addEventListener("keydown", identityHelperTypeHandler, true);3214 cleanupHooks.push(() => document.removeEventListener("keydown", identityHelperTypeHandler, true));32153216 (modalAncestor ?? document.body).appendChild(host);32173218 3219 3220 3221 3222 3223 3224 3225 function establishesFixedContainingBlock(s: CSSStyleDeclaration): boolean {3226 const has = (p: string) => { const v = s.getPropertyValue(p); return v !== "" && v !== "none"; };3227 return (3228 has("transform") || has("perspective") || has("translate") ||3229 has("rotate") || has("scale") || has("filter") ||3230 has("backdrop-filter") || has("-webkit-backdrop-filter") ||3231 /\b(transform|perspective|filter)\b/.test(s.getPropertyValue("will-change")) ||3232 /\b(paint|layout|strict|content)\b/.test(s.getPropertyValue("contain"))3233 );3234 }3235 function containingBlockOffset(): { dx: number; dy: number } {3236 let el: HTMLElement | null = host.parentElement;3237 while (el && el !== document.documentElement) {3238 const s = getComputedStyle(el);3239 if (establishesFixedContainingBlock(s)) {3240 const r = el.getBoundingClientRect();3241 return {3242 dx: r.left + (parseFloat(s.borderLeftWidth) || 0),3243 dy: r.top + (parseFloat(s.borderTopWidth) || 0),3244 };3245 }3246 el = el.parentElement;3247 }3248 return { dx: 0, dy: 0 };3249 }32503251 function updatePosition() {3252 const r = anchorEl.getBoundingClientRect();3253 const { dx, dy } = containingBlockOffset();3254 host.style.top = `${r.bottom - dy + 5}px`;3255 host.style.left = `${r.left - dx}px`;3256 host.style.width = `${popupWidth}px`;3257 }3258 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);3259 updatePosition();32603261 3262 3263 3264 if (modalAncestor) {3265 modalAncestor.addEventListener("scroll", updatePosition, { passive: true });3266 cleanupHooks.push(() => modalAncestor.removeEventListener("scroll", updatePosition));3267 }32683269 3270 3271 3272 const removalObserver = new MutationObserver(() => {3273 if (!document.contains(anchorEl)) dismiss();3274 });3275 removalObserver.observe(document.body, { childList: true, subtree: true });3276 cleanupHooks.push(() => removalObserver.disconnect());32773278 if (options.locked) {3279 renderLocked();3280 } else if (options.isSignup && options.savedIdentities) {3281 3282 3283 renderIdentityHelper();3284 } else if (entries.length === 1 && !options.isSignup) {3285 3286 3287 renderSilverPlatter(entries[0]!);3288 } else {3289 renderList();3290 }32913292 let exitScheduled = false;3293 function dismiss(userAction = false) {3294 3295 3296 3297 if (exitScheduled) return;3298 exitScheduled = true;3299 3300 3301 3302 stopObserving();3303 document.removeEventListener("click", outsideClick, { capture: true });3304 document.removeEventListener("keydown", handleKeyDown);3305 for (const cleanup of cleanupHooks) {3306 try { cleanup(); } catch { }3307 }3308 if (userAction) options.onDismiss?.();3309 container.classList.add("is-exiting");3310 setTimeout(() => host.remove(), 130);3311 }33123313 function outsideClick(ev: MouseEvent) {3314 const path = ev.composedPath();3315 3316 3317 3318 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3319 }3320 3321 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);33223323 return dismiss;3324}33253326export function showTOTPSelector(3327 anchorEl: HTMLInputElement,3328 entries: SelectorEntry[],3329 onFill: (id: string) => void,3330 options: { domain?: string; onDismiss?: () => void } = {}3331): () => void {3332 const host = document.createElement("div");3333 host.style.cssText = "position: absolute; z-index: 2147483647;";3334 host.style.setProperty("--peach-accent", accentHex);3335 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));3336 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));3337 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));3338 const shadow = host.attachShadow({ mode: "closed" });33393340 const style = shadow.appendChild(document.createElement("style"));3341 style.textContent = SELECTOR_STYLES;33423343 const rect = anchorEl.getBoundingClientRect();3344 host.style.top = `${rect.bottom + window.scrollY + 5}px`;3345 host.style.left = `${rect.left + window.scrollX}px`;3346 const popupWidth = Math.min(Math.max(rect.width, 300), 360);3347 host.style.width = `${popupWidth}px`;33483349 const container = shadow.appendChild(document.createElement("div"));3350 container.className = "peach-inline-popup";3351 container.setAttribute("data-theme", popupTheme);33523353 const domain = options.domain ?? location.hostname.replace(/^www\./, "");3354 const current = entries.filter((entry) => entry.hasTotp);3355 let focusedIndex = current.length === 1 ? 0 : -1;33563357 function updatePosition() {3358 const r = anchorEl.getBoundingClientRect();3359 host.style.top = `${r.bottom + window.scrollY + 5}px`;3360 host.style.left = `${r.left + window.scrollX}px`;3361 host.style.width = `${popupWidth}px`;3362 }33633364 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);33653366 function choose(entry: SelectorEntry) {3367 onFill(entry.id);3368 dismiss();3369 }33703371 function render() {3372 container.innerHTML = "";33733374 const domHdr = document.createElement("div");3375 domHdr.className = "domain-header";3376 const headText = document.createElement("div");3377 headText.className = "domain-header-text";3378 const eyebrow = document.createElement("div");3379 eyebrow.className = "domain-eyebrow";3380 eyebrow.textContent = "2FA code";3381 const domName = document.createElement("div");3382 domName.className = "domain-name";3383 domName.textContent = domain;3384 headText.append(eyebrow, domName);3385 domHdr.appendChild(headText);3386 container.appendChild(domHdr);33873388 if (current.length === 1) {3389 const entry = current[0]!;3390 const platter = document.createElement("div");3391 platter.className = "silver-platter";33923393 const title = document.createElement("div");3394 title.className = "sp-title";3395 title.textContent = entry.title;3396 platter.appendChild(title);33973398 const sub = document.createElement("div");3399 sub.className = "sp-username";3400 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3401 platter.appendChild(sub);34023403 const fill = document.createElement("button");3404 fill.className = "sp-fill-btn";3405 fill.type = "button";3406 fill.textContent = "Fill 2FA code";3407 fill.addEventListener("click", () => choose(entry));3408 platter.appendChild(fill);34093410 container.appendChild(platter);3411 requestAnimationFrame(() => fill.focus({ preventScroll: true }));3412 return;3413 }34143415 const results = document.createElement("div");3416 results.className = "results";3417 container.appendChild(results);34183419 for (let i = 0; i < current.length; i++) {3420 const entry = current[i]!;3421 const row = document.createElement("div");3422 row.className = `entry${i === focusedIndex ? " focused" : ""}`;3423 row.dataset.index = String(i);3424 row.setAttribute("tabindex", "-1");34253426 const icon = document.createElement("div");3427 icon.className = "entry-icon";3428 icon.innerHTML = keySvgStr();3429 row.appendChild(icon);3430 const info = document.createElement("div");3431 info.className = "entry-info";3432 const title = document.createElement("div");3433 title.className = "entry-title";3434 title.textContent = entry.title;3435 const sub = document.createElement("div");3436 sub.className = "entry-sub";3437 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3438 info.append(title, sub);3439 row.appendChild(info);34403441 const badge = document.createElement("span");3442 badge.className = "totp-badge";3443 badge.textContent = "Fill";3444 row.appendChild(badge);3445 row.addEventListener("click", () => choose(entry));3446 results.appendChild(row);3447 }3448 }34493450 function setFocused(index: number) {3451 focusedIndex = index;3452 container.querySelectorAll(".entry").forEach((el, i) => {3453 el.classList.toggle("focused", i === focusedIndex);3454 if (i === focusedIndex) (el as HTMLElement).scrollIntoView({ block: "nearest" });3455 });3456 }34573458 function handleKeyDown(e: KeyboardEvent) {3459 if (e.key === "Escape" || e.key === "Tab") {3460 e.preventDefault();3461 dismiss(true);3462 return;3463 }3464 if (current.length === 0) return;3465 if (e.key === "Enter") {3466 e.preventDefault();3467 choose(current[Math.max(0, focusedIndex)]!);3468 return;3469 }3470 if (current.length === 1) return;3471 if (e.key === "ArrowDown") {3472 e.preventDefault();3473 setFocused(focusedIndex < current.length - 1 ? focusedIndex + 1 : 0);3474 } else if (e.key === "ArrowUp") {3475 e.preventDefault();3476 setFocused(focusedIndex > 0 ? focusedIndex - 1 : current.length - 1);3477 }3478 }34793480 let exitScheduled = false;3481 function dismiss(userAction = false) {3482 if (exitScheduled) return;3483 exitScheduled = true;3484 stopObserving();3485 document.removeEventListener("click", outsideClick, { capture: true });3486 document.removeEventListener("keydown", handleKeyDown);3487 if (userAction) options.onDismiss?.();3488 container.classList.add("is-exiting");3489 setTimeout(() => host.remove(), 130);3490 }34913492 function outsideClick(ev: MouseEvent) {3493 const path = ev.composedPath();3494 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3495 }34963497 document.addEventListener("keydown", handleKeyDown);3498 document.body.appendChild(host);3499 render();3500 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);35013502 return dismiss;3503}35043505export interface CardSelectorEntry {3506 id: string;3507 title: string;3508 cardholderName?: string;3509 last4?: string;3510 expMonth?: string;3511 expYear?: string;3512 network?: import("../shared/types").CardNetwork;3513}35143515interface CardSelectorOptions {3516 domain?: string;3517 3518 onFill: (entry: import("../shared/types").VaultEntry) => void;3519 3520 onDismiss?: () => void;3521}35223523352435253526352735283529export function showPasskeyProGate(): void {3530 const domain = location.hostname.replace(/^www\./, "");35313532 const host = document.createElement("div");3533 host.setAttribute("data-peach-passkey-gate", "1");3534 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";3535 const shadow = host.attachShadow({ mode: "closed" });35363537 const style = document.createElement("style");3538 style.textContent = `3539 :host { all: initial; }3540 .gate-wrap {3541 position: fixed;3542 top: 50%;3543 left: 50%;3544 transform: translate(-50%, -50%);3545 width: 280px;3546 pointer-events: auto;3547 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;3548 }3549 @keyframes g-in {3550 from { opacity:0; transform: translate(-50%,-52%); }3551 to { opacity:1; transform: translate(-50%,-50%); }3552 }3553 .gate-wrap.is-exiting {3554 animation: g-out 130ms ease-in forwards;3555 }3556 @keyframes g-out {3557 to { opacity:0; transform: translate(-50%,-52%); }3558 }3559 .gate {3560 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3561 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3562 border-radius: 12px;3563 box-shadow: 0 12px 48px rgba(0,0,0,0.75);3564 padding: 16px;3565 font-family: Inter, sans-serif;3566 }3567 .gate-header {3568 display: flex;3569 align-items: center;3570 gap: 7px;3571 margin-bottom: 12px;3572 padding-bottom: 10px;3573 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3574 }3575 .gate-domain {3576 font-size: 12px;3577 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3578 font-family: "IBM Plex Mono", monospace;3579 overflow: hidden;3580 text-overflow: ellipsis;3581 white-space: nowrap;3582 }3583 .gate-title {3584 font-size: 14px;3585 font-weight: 500;3586 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3587 margin-bottom: 6px;3588 }3589 .gate-subtitle {3590 font-size: 12px;3591 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};3592 line-height: 1.5;3593 margin-bottom: 14px;3594 }3595 .gate-btn {3596 display: block;3597 width: 100%;3598 padding: 9px 0;3599 border-radius: 8px;3600 border: none;3601 cursor: pointer;3602 font-size: 13px;3603 font-family: Inter, sans-serif;3604 font-weight: 500;3605 transition: opacity 100ms;3606 margin-bottom: 7px;3607 text-align: center;3608 }3609 .gate-btn:last-child { margin-bottom: 0; }3610 .gate-btn--accent {3611 background: ${accentHex};3612 color: #fff;3613 }3614 .gate-btn--accent:hover { opacity: 0.88; }3615 .gate-btn--ghost {3616 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3617 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};3618 }3619 .gate-btn--ghost:hover { opacity: 0.75; }3620 .backdrop {3621 position: fixed;3622 inset: 0;3623 pointer-events: auto;3624 }3625 `;3626 shadow.appendChild(style);36273628 const backdrop = document.createElement("div");3629 backdrop.className = "backdrop";3630 shadow.appendChild(backdrop);36313632 const wrap = document.createElement("div");3633 wrap.className = "gate-wrap";3634 shadow.appendChild(wrap);36353636 const gate = document.createElement("div");3637 gate.className = "gate";3638 wrap.appendChild(gate);36393640 3641 const header = document.createElement("div");3642 header.className = "gate-header";3643 const icon = document.createElement("span");3644 icon.innerHTML = peachIconSvg();3645 const domainEl = document.createElement("span");3646 domainEl.className = "gate-domain";3647 domainEl.textContent = domain;3648 header.append(icon, domainEl);3649 gate.appendChild(header);36503651 const title = document.createElement("div");3652 title.className = "gate-title";3653 title.textContent = "Passkeys are part of Peach Pro.";3654 gate.appendChild(title);36553656 const subtitle = document.createElement("div");3657 subtitle.className = "gate-subtitle";3658 subtitle.textContent = "Store and use passkeys across all your devices, securely.";3659 gate.appendChild(subtitle);36603661 const upgradeBtn = document.createElement("button");3662 upgradeBtn.className = "gate-btn gate-btn--accent";3663 upgradeBtn.type = "button";3664 upgradeBtn.textContent = "Upgrade to Pro";3665 gate.appendChild(upgradeBtn);36663667 const laterBtn = document.createElement("button");3668 laterBtn.className = "gate-btn gate-btn--ghost";3669 laterBtn.type = "button";3670 laterBtn.textContent = "Not right now";3671 gate.appendChild(laterBtn);36723673 function dismiss() {3674 wrap.classList.add("is-exiting");3675 setTimeout(() => host.remove(), 130);3676 }36773678 upgradeBtn.addEventListener("click", () => {3679 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});3680 dismiss();3681 });3682 laterBtn.addEventListener("click", dismiss);3683 backdrop.addEventListener("click", dismiss);36843685 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {3686 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }3687 });36883689 document.body.appendChild(host);3690}369136923693369436953696369736983699export function showPasskeyRegistrationOffer(args: {3700 rpId: string;3701 rpName?: string;3702 userName?: string;3703 userDisplayName?: string;3704}): void {3705 const domain = location.hostname.replace(/^www\./, "");3706 const displayName = args.rpName || domain;37073708 const host = document.createElement("div");3709 host.setAttribute("data-peach-passkey-offer", "1");3710 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";3711 const shadow = host.attachShadow({ mode: "closed" });37123713 const style = document.createElement("style");3714 style.textContent = `3715 :host { all: initial; }3716 .offer-wrap {3717 position: fixed;3718 top: 16px;3719 right: 16px;3720 width: 320px;3721 pointer-events: auto;3722 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;3723 }3724 @keyframes o-in {3725 from { opacity: 0; transform: translateY(-6px); }3726 to { opacity: 1; transform: translateY(0); }3727 }3728 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }3729 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }3730 .offer {3731 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3732 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3733 border-radius: 12px;3734 box-shadow: 0 12px 48px rgba(0,0,0,0.55);3735 padding: 14px 14px 12px;3736 font-family: Inter, sans-serif;3737 position: relative;3738 }3739 .offer-close {3740 position: absolute;3741 top: 8px;3742 right: 8px;3743 width: 18px;3744 height: 18px;3745 border: none;3746 background: transparent;3747 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};3748 cursor: pointer;3749 font-size: 14px;3750 line-height: 1;3751 padding: 0;3752 }3753 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }3754 .offer-header {3755 display: flex;3756 align-items: center;3757 gap: 7px;3758 margin-bottom: 8px;3759 padding-bottom: 8px;3760 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3761 }3762 .offer-domain {3763 font-size: 12px;3764 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3765 font-family: "IBM Plex Mono", monospace;3766 overflow: hidden;3767 text-overflow: ellipsis;3768 white-space: nowrap;3769 }3770 .offer-title {3771 font-size: 13px;3772 font-weight: 500;3773 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3774 margin-bottom: 4px;3775 }3776 .offer-subtitle {3777 font-size: 11px;3778 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};3779 line-height: 1.45;3780 margin-bottom: 12px;3781 }3782 .offer-btn {3783 display: block;3784 width: 100%;3785 padding: 8px 0;3786 border-radius: 7px;3787 border: none;3788 cursor: pointer;3789 font-size: 12px;3790 font-family: Inter, sans-serif;3791 font-weight: 500;3792 transition: opacity 100ms;3793 margin-bottom: 6px;3794 text-align: center;3795 }3796 .offer-btn:last-child { margin-bottom: 0; }3797 .offer-btn--accent { background: ${accentHex}; color: #fff; }3798 .offer-btn--accent:hover { opacity: 0.88; }3799 .offer-btn--ghost {3800 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3801 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};3802 }3803 .offer-btn--ghost:hover { opacity: 0.75; }3804 `;3805 shadow.appendChild(style);38063807 const wrap = document.createElement("div");3808 wrap.className = "offer-wrap";3809 shadow.appendChild(wrap);38103811 const offer = document.createElement("div");3812 offer.className = "offer";3813 wrap.appendChild(offer);38143815 const closeBtn = document.createElement("button");3816 closeBtn.className = "offer-close";3817 closeBtn.type = "button";3818 closeBtn.setAttribute("aria-label", "Dismiss");3819 closeBtn.textContent = "✕";3820 offer.appendChild(closeBtn);38213822 const header = document.createElement("div");3823 header.className = "offer-header";3824 const icon = document.createElement("span");3825 icon.innerHTML = peachIconSvg();3826 const domainEl = document.createElement("span");3827 domainEl.className = "offer-domain";3828 domainEl.textContent = domain;3829 header.append(icon, domainEl);3830 offer.appendChild(header);38313832 const title = document.createElement("div");3833 title.className = "offer-title";3834 title.textContent = "This site supports passkeys.";3835 offer.appendChild(title);38363837 const subtitle = document.createElement("div");3838 subtitle.className = "offer-subtitle";3839 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";3840 offer.appendChild(subtitle);38413842 const saveBtn = document.createElement("button");3843 saveBtn.className = "offer-btn offer-btn--accent";3844 saveBtn.type = "button";3845 saveBtn.textContent = "Save passkey to Peach";3846 offer.appendChild(saveBtn);38473848 const laterBtn = document.createElement("button");3849 laterBtn.className = "offer-btn offer-btn--ghost";3850 laterBtn.type = "button";3851 laterBtn.textContent = "Not now";3852 offer.appendChild(laterBtn);38533854 function dismiss(decline: boolean) {3855 if (decline) {3856 chrome.runtime.sendMessage({3857 type: "PASSKEY_OFFER_DECLINE",3858 payload: { domain },3859 }).catch(() => {});3860 }3861 wrap.classList.add("is-exiting");3862 setTimeout(() => host.remove(), 130);3863 }38643865 saveBtn.addEventListener("click", () => {3866 chrome.runtime.sendMessage({3867 type: "INITIATE_FIREFOX_PASSKEY_CREATE",3868 payload: {3869 rpId: args.rpId,3870 rpName: args.rpName ?? displayName,3871 userName: args.userName ?? "user",3872 userDisplayName: args.userDisplayName,3873 },3874 }).catch(() => {});3875 dismiss(false);3876 });3877 laterBtn.addEventListener("click", () => dismiss(true));3878 closeBtn.addEventListener("click", () => dismiss(true));38793880 document.body.appendChild(host);3881}3882388338843885388638873888388938903891export function showPasskeyAvailable(args: {3892 rpId: string;3893 userName: string;3894 fromBrowser?: "chrome" | "firefox";3895 currentBrowser?: "chrome" | "firefox" | "unknown";3896}): void {3897 const domain = location.hostname.replace(/^www\./, "");3898 const crossBrowser =3899 args.fromBrowser &&3900 args.currentBrowser &&3901 args.currentBrowser !== "unknown" &&3902 args.fromBrowser !== args.currentBrowser;39033904 const host = document.createElement("div");3905 host.setAttribute("data-peach-passkey-available", "1");3906 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";3907 const shadow = host.attachShadow({ mode: "closed" });39083909 const style = document.createElement("style");3910 style.textContent = `3911 :host { all: initial; }3912 .avail-wrap {3913 position: fixed;3914 top: 16px;3915 right: 16px;3916 width: 320px;3917 pointer-events: auto;3918 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;3919 }3920 @keyframes a-in {3921 from { opacity: 0; transform: translateY(-6px); }3922 to { opacity: 1; transform: translateY(0); }3923 }3924 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }3925 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }3926 .avail {3927 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3928 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3929 border-radius: 12px;3930 box-shadow: 0 12px 48px rgba(0,0,0,0.55);3931 padding: 14px 14px 12px;3932 font-family: Inter, sans-serif;3933 position: relative;3934 }3935 .avail-close {3936 position: absolute;3937 top: 8px;3938 right: 8px;3939 width: 18px;3940 height: 18px;3941 border: none;3942 background: transparent;3943 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};3944 cursor: pointer;3945 font-size: 14px;3946 line-height: 1;3947 padding: 0;3948 }3949 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }3950 .avail-header {3951 display: flex;3952 align-items: center;3953 gap: 7px;3954 margin-bottom: 8px;3955 padding-bottom: 8px;3956 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3957 }3958 .avail-domain {3959 font-size: 12px;3960 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3961 font-family: "IBM Plex Mono", monospace;3962 }3963 .avail-label {3964 font-size: 9px;3965 letter-spacing: 0.16em;3966 text-transform: uppercase;3967 color: ${accentHex};3968 margin-bottom: 4px;3969 }3970 .avail-user {3971 font-size: 13px;3972 font-weight: 500;3973 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3974 margin-bottom: 12px;3975 overflow: hidden;3976 text-overflow: ellipsis;3977 white-space: nowrap;3978 }3979 .avail-cross {3980 font-size: 11px;3981 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};3982 line-height: 1.45;3983 margin-bottom: 12px;3984 padding: 8px;3985 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};3986 border-radius: 6px;3987 }3988 .avail-btn {3989 display: block;3990 width: 100%;3991 padding: 8px 0;3992 border-radius: 7px;3993 border: none;3994 cursor: pointer;3995 font-size: 12px;3996 font-family: Inter, sans-serif;3997 font-weight: 500;3998 transition: opacity 100ms;3999 margin-bottom: 6px;4000 text-align: center;4001 }4002 .avail-btn:last-child { margin-bottom: 0; }4003 .avail-btn--accent { background: ${accentHex}; color: #fff; }4004 .avail-btn--accent:hover { opacity: 0.88; }4005 .avail-btn--ghost {4006 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};4007 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};4008 }4009 .avail-btn--ghost:hover { opacity: 0.75; }4010 `;4011 shadow.appendChild(style);40124013 const wrap = document.createElement("div");4014 wrap.className = "avail-wrap";4015 shadow.appendChild(wrap);40164017 const avail = document.createElement("div");4018 avail.className = "avail";4019 wrap.appendChild(avail);40204021 const closeBtn = document.createElement("button");4022 closeBtn.className = "avail-close";4023 closeBtn.type = "button";4024 closeBtn.setAttribute("aria-label", "Dismiss");4025 closeBtn.textContent = "✕";4026 avail.appendChild(closeBtn);40274028 const header = document.createElement("div");4029 header.className = "avail-header";4030 const icon = document.createElement("span");4031 icon.innerHTML = peachIconSvg();4032 const domainEl = document.createElement("span");4033 domainEl.className = "avail-domain";4034 domainEl.textContent = domain;4035 header.append(icon, domainEl);4036 avail.appendChild(header);40374038 const label = document.createElement("div");4039 label.className = "avail-label";4040 label.textContent = "PASSKEY";4041 avail.appendChild(label);40424043 const user = document.createElement("div");4044 user.className = "avail-user";4045 user.textContent = args.userName;4046 avail.appendChild(user);40474048 if (crossBrowser) {4049 const cross = document.createElement("div");4050 cross.className = "avail-cross";4051 cross.textContent =4052 args.fromBrowser === "chrome"4053 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."4054 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";4055 avail.appendChild(cross);4056 }40574058 const signInBtn = document.createElement("button");4059 signInBtn.className = "avail-btn avail-btn--accent";4060 signInBtn.type = "button";4061 signInBtn.textContent = "Sign in with passkey";4062 avail.appendChild(signInBtn);40634064 const passwordBtn = document.createElement("button");4065 passwordBtn.className = "avail-btn avail-btn--ghost";4066 passwordBtn.type = "button";4067 passwordBtn.textContent = "Use password instead";4068 avail.appendChild(passwordBtn);40694070 function dismiss() {4071 wrap.classList.add("is-exiting");4072 setTimeout(() => host.remove(), 130);4073 }40744075 signInBtn.addEventListener("click", () => {4076 4077 4078 4079 4080 4081 dismiss();4082 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});4083 });4084 passwordBtn.addEventListener("click", dismiss);4085 closeBtn.addEventListener("click", dismiss);40864087 document.body.appendChild(host);4088}408940904091409240934094409540964097export function showCardSelector(4098 anchorEl: HTMLInputElement,4099 cards: CardSelectorEntry[],4100 options: CardSelectorOptions4101): () => void {4102 if (cards.length === 0) return () => {};4103 const card = cards[0]!;41044105 const host = document.createElement("div");4106 host.style.cssText = "position: absolute; z-index: 2147483647;";4107 host.style.setProperty("--peach-accent", accentHex);4108 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));4109 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));4110 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));4111 const shadow = host.attachShadow({ mode: "closed" });41124113 const style = shadow.appendChild(document.createElement("style"));4114 style.textContent = SELECTOR_STYLES;41154116 const rect = anchorEl.getBoundingClientRect();4117 host.style.top = `${rect.bottom + window.scrollY + 5}px`;4118 host.style.left = `${rect.left + window.scrollX}px`;4119 host.style.width = `${Math.min(Math.max(rect.width, 320), 400)}px`;41204121 const container = shadow.appendChild(document.createElement("div"));4122 container.className = "peach-inline-popup";4123 container.setAttribute("data-theme", popupTheme);41244125 4126 const domHdr = document.createElement("div");4127 domHdr.className = "domain-header";4128 const eyebrow = document.createElement("div");4129 eyebrow.className = "domain-eyebrow";4130 eyebrow.textContent = "Pay with Peach";4131 const domName = document.createElement("div");4132 domName.className = "domain-name";4133 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");4134 domHdr.append(eyebrow, domName);4135 container.appendChild(domHdr);41364137 const platter = document.createElement("div");4138 platter.className = "card-platter";41394140 if (card.network && card.network !== "unknown") {4141 const net = document.createElement("div");4142 net.className = "cp-network";4143 net.textContent = card.network;4144 platter.appendChild(net);4145 }41464147 const nick = document.createElement("div");4148 nick.className = "cp-nickname";4149 nick.textContent = card.title;4150 platter.appendChild(nick);41514152 if (card.last4) {4153 const l4 = document.createElement("div");4154 l4.className = "cp-last4";4155 l4.textContent = `•••• ${card.last4}`;4156 platter.appendChild(l4);4157 }41584159 const meta = document.createElement("div");4160 meta.className = "cp-meta";4161 if (card.cardholderName) {4162 const name = document.createElement("span");4163 name.textContent = card.cardholderName;4164 meta.appendChild(name);4165 }4166 if (card.expMonth && card.expYear) {4167 if (card.cardholderName) {4168 const sep = document.createElement("span");4169 sep.className = "cp-meta-sep";4170 sep.textContent = "·";4171 meta.appendChild(sep);4172 }4173 const exp = document.createElement("span");4174 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;4175 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;4176 meta.appendChild(exp);4177 }4178 if (meta.childElementCount > 0) platter.appendChild(meta);41794180 const fillWrap = document.createElement("div");4181 fillWrap.className = "cp-fill-wrap";4182 const fillBtn = document.createElement("button");4183 fillBtn.className = "sp-fill-btn";4184 fillBtn.type = "button";4185 fillBtn.textContent = "Fill card details";4186 fillBtn.addEventListener("click", async () => {4187 if (fillBtn.disabled) return;4188 fillBtn.disabled = true;4189 fillBtn.classList.add("filled");4190 fillBtn.innerHTML = checkSvgStr();4191 const resp = await chrome.runtime.sendMessage({4192 type: "GET_ENTRY_FOR_FILL",4193 payload: { id: card.id },4194 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4195 if (resp?.ok && resp.data) options.onFill(resp.data);4196 setTimeout(() => dismiss(), 400);4197 });4198 fillWrap.appendChild(fillBtn);4199 platter.appendChild(fillWrap);42004201 4202 const actions = document.createElement("div");4203 actions.className = "sp-actions";42044205 function flashBtn(b: HTMLButtonElement) {4206 b.classList.add("flashed");4207 setTimeout(() => b.classList.remove("flashed"), 700);4208 }42094210 4211 function flashCopyError(b: HTMLButtonElement) {4212 const original = b.textContent ?? "";4213 b.textContent = "Blocked";4214 b.classList.add("flashed");4215 setTimeout(() => {4216 b.classList.remove("flashed");4217 if (b.isConnected) b.textContent = original;4218 }, 1200);4219 }42204221 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {4222 const resp = await chrome.runtime.sendMessage({4223 type: "GET_ENTRY_FOR_FILL",4224 payload: { id: card.id },4225 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4226 return resp?.ok && resp.data ? resp.data : null;4227 }42284229 const copyNum = document.createElement("button");4230 copyNum.className = "sp-action-btn";4231 copyNum.type = "button";4232 copyNum.title = "Copy card number";4233 copyNum.textContent = "Copy number";4234 copyNum.addEventListener("click", async (e) => {4235 e.stopPropagation();4236 const full = await fetchFullCard();4237 if (!full?.cardNumber) return;4238 let alreadyWritten = false;4239 try {4240 await navigator.clipboard.writeText(full.cardNumber);4241 alreadyWritten = true;4242 } catch (err) {4243 console.warn("[Peach] clipboard write denied:", err);4244 }4245 const resp = await chrome.runtime.sendMessage({4246 type: "COPY",4247 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },4248 }).catch(() => null) as { ok?: boolean } | null;4249 if (alreadyWritten || resp?.ok) flashBtn(copyNum);4250 else flashCopyError(copyNum);4251 });4252 actions.appendChild(copyNum);42534254 const copyCvc = document.createElement("button");4255 copyCvc.className = "sp-action-btn";4256 copyCvc.type = "button";4257 copyCvc.title = "Copy CVC";4258 copyCvc.textContent = "Copy CVC";4259 copyCvc.addEventListener("click", async (e) => {4260 e.stopPropagation();4261 const full = await fetchFullCard();4262 if (!full?.cvc) return;4263 let alreadyWritten = false;4264 try {4265 await navigator.clipboard.writeText(full.cvc);4266 alreadyWritten = true;4267 } catch (err) {4268 console.warn("[Peach] clipboard write denied:", err);4269 }4270 const resp = await chrome.runtime.sendMessage({4271 type: "COPY",4272 payload: { text: full.cvc, label: "CVC", alreadyWritten },4273 }).catch(() => null) as { ok?: boolean } | null;4274 if (alreadyWritten || resp?.ok) flashBtn(copyCvc);4275 else flashCopyError(copyCvc);4276 });4277 actions.appendChild(copyCvc);42784279 const closeBtn = document.createElement("button");4280 closeBtn.className = "sp-action-btn";4281 closeBtn.type = "button";4282 closeBtn.title = "Dismiss";4283 closeBtn.textContent = "✕";4284 closeBtn.addEventListener("click", (e) => {4285 e.stopPropagation();4286 dismiss(true);4287 });4288 actions.appendChild(closeBtn);42894290 platter.appendChild(actions);4291 container.appendChild(platter);42924293 document.body.appendChild(host);4294 function updatePosition() {4295 const r = anchorEl.getBoundingClientRect();4296 host.style.top = `${r.bottom + window.scrollY + 5}px`;4297 host.style.left = `${r.left + window.scrollX}px`;4298 }4299 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);43004301 function onKeyDown(e: KeyboardEvent) {4302 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }4303 }4304 document.addEventListener("keydown", onKeyDown);43054306 let exitScheduled = false;4307 function dismiss(userAction = false) {4308 if (exitScheduled) return;4309 exitScheduled = true;4310 stopObserving();4311 document.removeEventListener("click", outsideClick, { capture: true });4312 document.removeEventListener("keydown", onKeyDown);4313 if (userAction) options.onDismiss?.();4314 container.classList.add("is-exiting");4315 setTimeout(() => host.remove(), 130);4316 }43174318 function outsideClick(ev: MouseEvent) {4319 const path = ev.composedPath();4320 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);4321 }4322 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);43234324 return dismiss;4325}