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 737475767778 loadSearchableEntries?: () => Promise<SelectorEntry[]>;79 80 isSignup?: boolean;81 82838485 identityKind?: IdentityKind;86 87888990 savedIdentities?: SavedIdentity[];91 9293949596 onFillFields?: (username: string, password: string, email?: string) => void;97 9899100101102103 hasEmailField?: boolean;104 105106107108109 locked?: boolean;110 111112113114 onUnlocked?: () => void;115 116117118119120 onDismiss?: () => void;121}122123function getDomainFromUrl(url: string): string {124 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }125}126127function keySvgStr(): string {128 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>`;129}130131function plusSvgStr(): string {132 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>`;133}134135function searchSvgStr(): string {136 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>`;137}138139function backSvgStr(): string {140 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>`;141}142143function peachIconSvg(): string {144 145 146 147 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>`;148}149150function checkSvgStr(): string {151 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>`;152}153154function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {155 const { hostname, origin } = location;156 const parts = hostname.split(".");157 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",158 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",159 "oidc", "federation", "saml", "cas", "ldap"];160 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());161 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;162 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);163 return { origin, title, loginSubdomain };164}165166167168169170171export function showRememberSiteToast(172 pageDomain: string,173 entryId: string,174): void {175 const host = document.createElement("div");176 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";177 host.style.setProperty("--peach-accent", accentHex);178 const shadow = host.attachShadow({ mode: "closed" });179180 const style = document.createElement("style");181 style.textContent = `182 .bar {183 display:flex;align-items:center;gap:10px;padding:10px 14px;184 background:#131109;border:1px solid rgba(240,232,216,0.09);185 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);186 font-family:Inter,sans-serif;white-space:nowrap;187 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;188 }189 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }190 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }191 .yes {192 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;193 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;194 }195 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }196 .no {197 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;198 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;199 }200 `;201 shadow.appendChild(style);202203 const bar = document.createElement("div"); bar.className = "bar";204 const txt = document.createElement("span"); txt.className = "txt";205 txt.textContent = `Remember ${pageDomain} for this entry?`;206 const yesBtn = document.createElement("button"); yesBtn.className = "yes";207 yesBtn.textContent = "Remember";208 yesBtn.addEventListener("click", async () => {209 await chrome.runtime.sendMessage({210 type: "ADD_ADDITIONAL_URL",211 payload: { id: entryId, url: location.origin },212 });213 host.remove();214 });215 const noBtn = document.createElement("button"); noBtn.className = "no";216 noBtn.textContent = "Not now";217 noBtn.addEventListener("click", () => host.remove());218 bar.append(txt, yesBtn, noBtn);219 shadow.appendChild(bar);220 document.body.appendChild(host);221 setTimeout(() => host.remove(), 9000);222}223224225226let totpToastDismissTimer: ReturnType<typeof setTimeout> | null = null;227228229230231232233234235export function showTotpCopyToast(secondsUntilClear: number): void {236 document.getElementById("peach-totp-toast")?.remove();237 if (totpToastDismissTimer) {238 clearTimeout(totpToastDismissTimer);239 totpToastDismissTimer = null;240 }241242 const toast = document.createElement("div");243 toast.id = "peach-totp-toast";244 toast.className = "peach-totp-toast";245 toast.setAttribute("data-peach-toast", "totp-copy");246 toast.setAttribute("aria-live", "polite");247248 249 Object.assign(toast.style, {250 position: "fixed",251 top: "16px",252 right: "16px",253 zIndex: "2147483647",254 padding: "10px 14px",255 borderRadius: "8px",256 background: "rgba(30, 20, 10, 0.92)",257 border: "1px solid rgba(210, 140, 60, 0.3)",258 color: "#e8d5b0",259 fontFamily: '"IBM Plex Mono", monospace',260 fontSize: "11px",261 lineHeight: "1.5",262 boxShadow: "0 4px 24px rgba(0,0,0,0.4)",263 pointerEvents: "none",264 });265266 267 268 toast.innerHTML = `269 <div style="font-style: italic; font-family: 'Cormorant Garamond', serif; font-size: 13px; margin-bottom: 2px;">270 2FA code copied271 </div>272 <div style="opacity: 0.6; font-size: 10px;">273 clears in ${secondsUntilClear}s274 </div>275 `;276277 document.body.appendChild(toast);278279 totpToastDismissTimer = setTimeout(() => {280 totpToastDismissTimer = null;281 toast.style.transition = "opacity 0.3s ease";282 toast.style.opacity = "0";283 setTimeout(() => toast.remove(), 300);284 }, 3000);285}286287interface SmartGenerator {288 root: HTMLElement;289 getPassword: () => string;290 291 copy: () => Promise<void>;292}293294interface SmartGeneratorOptions {295 296 policy: PasswordPolicy;297 298299300301302 initialPassword?: string;303 304 onChange?: (pw: string) => void;305}306307308309310311312313314function createSmartGenerator(opts: SmartGeneratorOptions): SmartGenerator {315 const policy = opts.policy ?? {};316317 318 let length = policy.maxLength ?? 64;319 if (policy.minLength) length = Math.max(length, policy.minLength);320 if (length < 8) length = 8;321 if (length > 128) length = 128;322323 let upper = true;324 let lower = true;325 let numbers = true;326 let symbols = !policy.forbidsSymbols;327 if (policy.requiresSymbols) symbols = true;328 const forbiddenChars = policy.forbiddenChars ?? "";329330 let password = "";331332 const root = document.createElement("div");333 root.className = "pwgen-block";334335 const displayRow = root.appendChild(document.createElement("div"));336 displayRow.className = "pwgen-display-row";337 const output = displayRow.appendChild(document.createElement("input")) as HTMLInputElement;338 output.type = "text";339 output.className = "pwgen-output";340 output.setAttribute("autocomplete", "off");341 output.setAttribute("spellcheck", "false");342 output.setAttribute("autocorrect", "off");343 output.setAttribute("autocapitalize", "off");344345 const regenBtn = displayRow.appendChild(document.createElement("button"));346 regenBtn.className = "pwgen-icon-btn";347 regenBtn.type = "button";348 regenBtn.setAttribute("aria-label", "Regenerate password");349 regenBtn.title = "Regenerate";350 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>`;351352 const copyBtn = displayRow.appendChild(document.createElement("button"));353 copyBtn.className = "pwgen-icon-btn";354 copyBtn.type = "button";355 copyBtn.setAttribute("aria-label", "Copy password");356 copyBtn.title = "Copy";357 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>`;358359 const controls = root.appendChild(document.createElement("div"));360 controls.className = "pwgen-controls-row";361 const slider = controls.appendChild(document.createElement("input"));362 slider.type = "range";363 slider.className = "pwgen-slider";364 slider.min = "8";365 slider.max = "128";366 slider.value = String(length);367 if (policy.minLength) slider.min = String(Math.max(8, policy.minLength));368 if (policy.maxLength) slider.max = String(Math.min(128, policy.maxLength));369 const lenNum = controls.appendChild(document.createElement("span"));370 lenNum.className = "pwgen-len-num pwgen-len-num--edit";371 lenNum.textContent = String(length);372 lenNum.addEventListener("click", () => {373 const li = document.createElement("input");374 li.type = "text"; li.inputMode = "numeric";375 li.className = "pwgen-len-input"; li.value = String(length);376 lenNum.replaceWith(li);377 requestAnimationFrame(() => li.select());378 function commitLenEdit() {379 const n = parseInt(li.value, 10);380 if (!Number.isNaN(n)) {381 length = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));382 slider.value = String(length);383 regenerate();384 }385 lenNum.textContent = String(length);386 li.replaceWith(lenNum);387 }388 li.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commitLenEdit(); } else if (e.key === "Escape") { li.replaceWith(lenNum); } });389 li.addEventListener("blur", commitLenEdit);390 });391392 const pills = root.appendChild(document.createElement("div"));393 pills.className = "pwgen-pills";394395 function makePill(label: string, on: boolean, onToggle: (next: boolean) => void): HTMLButtonElement {396 const b = document.createElement("button");397 b.type = "button";398 b.className = `pwgen-pill ${on ? "is-on" : ""}`;399 b.textContent = label;400 b.addEventListener("click", () => {401 const next = !b.classList.contains("is-on");402 b.classList.toggle("is-on", next);403 onToggle(next);404 });405 return b;406 }407 pills.appendChild(makePill("A-Z", upper, (next) => { upper = next; regenerate(); }));408 pills.appendChild(makePill("a-z", lower, (next) => { lower = next; regenerate(); }));409 pills.appendChild(makePill("0-9", numbers, (next) => { numbers = next; regenerate(); }));410 pills.appendChild(makePill("!@#", symbols, (next) => {411 412 symbols = next;413 regenerate();414 }));415416 417 418 419 const hintParts: string[] = [];420 if (policy.minLength) hintParts.push(`min ${policy.minLength}`);421 if (policy.maxLength) hintParts.push(`max ${policy.maxLength}`);422 if (policy.requiresSymbols) hintParts.push("requires symbols");423 if (policy.forbidsSymbols) hintParts.push("no symbols");424 if (policy.forbiddenChars) hintParts.push(`excludes ${policy.forbiddenChars.slice(0, 6)}${policy.forbiddenChars.length > 6 ? "…" : ""}`);425 if (hintParts.length > 0) {426 const hint = root.appendChild(document.createElement("div"));427 hint.className = "pwgen-policy-hint";428 const accent = document.createElement("span");429 accent.className = "pwgen-hint-accent";430 accent.textContent = "site rules · ";431 const text = document.createTextNode(hintParts.join(" · "));432 hint.appendChild(accent);433 hint.appendChild(text);434 }435436 function regenerate() {437 438 439 password = generatePassword({440 length,441 includeUppercase: upper,442 includeLowercase: lower,443 includeNumbers: numbers,444 includeSymbols: symbols,445 forbiddenChars,446 });447 output.value = password;448 opts.onChange?.(password);449 }450451 output.addEventListener("input", () => {452 password = output.value;453 length = password.length;454 slider.value = String(Math.max(8, Math.min(128, length)));455 lenNum.textContent = String(length);456 opts.onChange?.(password);457 });458459 slider.addEventListener("input", () => {460 length = parseInt(slider.value, 10);461 lenNum.textContent = String(length);462 regenerate();463 });464 regenBtn.addEventListener("click", regenerate);465466 let copyResetTimer: ReturnType<typeof setTimeout> | null = null;467 async function copy(): Promise<void> {468 try {469 await navigator.clipboard.writeText(password);470 copyBtn.classList.add("is-copied");471 if (copyResetTimer) clearTimeout(copyResetTimer);472 copyResetTimer = setTimeout(() => copyBtn.classList.remove("is-copied"), 1400);473 } catch {474 475 }476 }477 copyBtn.addEventListener("click", () => { void copy(); });478479 if (opts.initialPassword) {480 password = opts.initialPassword;481 output.value = password;482 opts.onChange?.(password);483 } else {484 regenerate();485 }486487 return {488 root,489 getPassword: () => password,490 copy,491 };492}493494export function observeAnchorPosition(495 anchorEl: HTMLElement,496 onUpdate: () => void497): () => void {498 let rafId = 0;499 let burstUntil = 0;500501 const runUpdate = () => {502 rafId = 0;503 onUpdate();504 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);505 };506 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };507 const scheduleBurst = (ms = 220) => {508 burstUntil = Math.max(burstUntil, performance.now() + ms);509 scheduleUpdate();510 };511512 const resizeObserver = new ResizeObserver(() => scheduleBurst());513 resizeObserver.observe(anchorEl);514 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);515516 const mutationObserver = new MutationObserver(() => scheduleBurst());517 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });518 if (anchorEl.parentElement) {519 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });520 }521522 const onScroll = () => scheduleUpdate();523 window.addEventListener("scroll", onScroll, true);524 window.addEventListener("resize", onScroll);525 window.visualViewport?.addEventListener("resize", onScroll);526 window.visualViewport?.addEventListener("scroll", onScroll);527 anchorEl.addEventListener("focus", () => scheduleBurst());528 anchorEl.addEventListener("input", () => scheduleBurst());529530 scheduleBurst(260);531532 return () => {533 if (rafId) cancelAnimationFrame(rafId);534 resizeObserver.disconnect();535 mutationObserver.disconnect();536 window.removeEventListener("scroll", onScroll, true);537 window.removeEventListener("resize", onScroll);538 window.visualViewport?.removeEventListener("resize", onScroll);539 window.visualViewport?.removeEventListener("scroll", onScroll);540 };541}542543export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {544 const button = document.createElement("button");545 button.type = "button";546 button.setAttribute("aria-label", "Peach autofill");547 button.title = "Fill with Peach";548 button.innerHTML = peachIconSvg();549 550 551 552 553 button.style.cssText = `554 position: fixed; z-index: 2147483646;555 width: 26px; height: 26px; cursor: pointer;556 pointer-events: auto; border: none; padding: 3px;557 display: inline-flex; align-items: center; justify-content: center;558 background: transparent; border-radius: 999px;559 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;560 opacity: 0;561 `;562 button.addEventListener("mouseenter", () => {563 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;564 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;565 button.style.transform = "scale(1.08)";566 });567 button.addEventListener("mouseleave", () => {568 button.style.background = "transparent";569 button.style.boxShadow = "none";570 button.style.transform = "none";571 });572 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });573 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });574575 document.body.appendChild(button);576577 function updatePosition() {578 const rect = field.getBoundingClientRect();579 if (rect.width === 0 || rect.height === 0) {580 button.style.opacity = "0"; button.style.pointerEvents = "none";581 return;582 }583 const sz = 26;584 const inset = Math.max(5, Math.min(10, rect.height * 0.22));585 const top = rect.top + rect.height / 2 - sz / 2;586 587 588 const minLeft = rect.left + 4;589 const left = Math.max(minLeft, rect.right - sz - inset);590 591 if (rect.width < 40) {592 button.style.opacity = "0"; button.style.pointerEvents = "none";593 return;594 }595 button.style.top = `${top}px`;596 button.style.left = `${left}px`;597 button.style.opacity = "1"; button.style.pointerEvents = "auto";598 }599 updatePosition();600 requestAnimationFrame(() => { button.style.opacity = "1"; });601 const stopObserving = observeAnchorPosition(field, updatePosition);602 return () => { stopObserving(); button.remove(); };603}604605606607608609610611const SELECTOR_STYLES = `612 :host { all: initial; }613 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }614615 .peach-inline-popup[data-theme="dark"] {616 --peach-bg: #1a1410;617 --peach-bg-card: #221c16;618 --peach-text: #f5f0ea;619 --peach-text-dim: #8a7a6a;620 --peach-text-muted: #4a3a2a;621 --peach-border: rgba(255,255,255,0.07);622 --peach-input-bg: rgba(255,255,255,0.05);623 --peach-input-border: rgba(255,255,255,0.08);624 }625 .peach-inline-popup[data-theme="light"] {626 --peach-bg: #fdf8f4;627 --peach-bg-card: #f5ede4;628 --peach-text: #1c1310;629 --peach-text-dim: #6b5a4a;630 --peach-text-muted: #a89080;631 --peach-border: rgba(0,0,0,0.08);632 --peach-input-bg: rgba(0,0,0,0.04);633 --peach-input-border: rgba(0,0,0,0.10);634 }635636 .peach-inline-popup {637 background: var(--peach-bg);638 border: 1px solid var(--peach-border-accent, var(--peach-border));639 border-radius: 14px;640 overflow: hidden;641 box-shadow:642 0 8px 40px var(--peach-glow, transparent),643 0 2px 12px rgba(0,0,0,0.5);644 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;645 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;646 transition: height 220ms cubic-bezier(0.16,1,0.3,1);647 }648 /* Light theme: shadow against a light page is too strong otherwise — drop both649 the accent glow and the dark drop-shadow opacity to keep the popup elegant. */650 .peach-inline-popup[data-theme="light"] {651 box-shadow:652 0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),653 0 2px 12px rgba(0,0,0,0.12);654 }655 .peach-inline-popup.is-exiting {656 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;657 pointer-events: none;658 }659660 @keyframes peach-enter {661 0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }662 65% { opacity: 1; filter: blur(0); }663 100% { opacity: 1; transform: translateY(0) scale(1); }664 }665 @keyframes peach-exit {666 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }667 100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }668 }669 @keyframes peach-slide-in {670 from { opacity: 0; transform: translateX(12px); }671 to { opacity: 1; transform: translateX(0); }672 }673 @keyframes peach-slide-back {674 from { opacity: 0; transform: translateX(-12px); }675 to { opacity: 1; transform: translateX(0); }676 }677 .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }678 .peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }679680 /* Domain header */681 .domain-header {682 padding: 9px 13px 7px;683 border-bottom: 1px solid var(--peach-border);684 display: flex; align-items: center; justify-content: space-between;685 gap: 8px;686 }687 .domain-header-text {688 display: flex; flex-direction: column; gap: 2px;689 min-width: 0; flex: 1;690 }691 .domain-eyebrow {692 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;693 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;694 }695 .domain-name {696 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);697 font-family: "IBM Plex Mono", monospace;698 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;699 }700 .domain-header-actions {701 display: inline-flex; align-items: center; gap: 5px;702 flex-shrink: 0;703 }704 .domain-add-btn,705 .domain-search-btn {706 flex-shrink: 0;707 width: 22px; height: 22px;708 display: inline-flex; align-items: center; justify-content: center;709 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);710 border: 1px solid color-mix(in srgb, var(--peach-accent) 28%, transparent);711 border-radius: 6px;712 color: var(--peach-accent);713 cursor: pointer; padding: 0;714 transition: background 120ms, border-color 120ms, transform 80ms;715 font-family: "IBM Plex Mono", monospace;716 font-size: 14px; line-height: 1;717 }718 .domain-add-btn:hover,719 .domain-search-btn:hover {720 background: color-mix(in srgb, var(--peach-accent) 22%, transparent);721 border-color: color-mix(in srgb, var(--peach-accent) 48%, transparent);722 transform: scale(1.06);723 }724 .domain-add-btn:active,725 .domain-search-btn:active { transform: scale(0.96); }726727 /* Search */728 .search-wrap {729 padding: 9px 12px 7px;730 border-bottom: 1px solid var(--peach-border);731 }732 /* UI-2 / MV-2: brief loading hint while the vault-wide list lazy-loads. */733 .search-loading {734 display: block;735 margin-top: 5px;736 font-size: 10px;737 color: var(--peach-text-dim);738 font-family: "IBM Plex Mono", monospace;739 animation: search-loading-pulse 1s ease-in-out infinite;740 }741 @keyframes search-loading-pulse { 0%,100% { opacity: 0.45; } 50% { opacity: 0.9; } }742 .search {743 width: 100%; background: var(--peach-input-bg);744 border: 1px solid var(--peach-input-border);745 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;746 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;747 transition: border-color 120ms, box-shadow 120ms;748 }749 .search:focus {750 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);751 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);752 }753 .search::placeholder { color: var(--peach-text-muted); }754755 /* Results */756 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }757 .results::-webkit-scrollbar { display: none; }758759 /* Entry rows */760 .entry {761 display: flex; align-items: center; padding: 8px 12px;762 cursor: pointer; border-bottom: 1px solid var(--peach-border);763 transition: background 70ms; position: relative; outline: none;764 }765 .entry:last-child { border-bottom: none; }766 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }767 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }768 .entry.focused::before {769 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;770 width: 2px; border-radius: 2px;771 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);772 }773774 .entry-icon {775 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;776 background: var(--peach-input-bg); border: 1px solid var(--peach-border);777 display: flex; align-items: center; justify-content: center;778 overflow: hidden; margin-right: 10px;779 }780 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }781 .entry-icon.is-new {782 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);783 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);784 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);785 }786787 .entry-info { flex: 1; min-width: 0; }788 .entry-title {789 font-size: 12.5px; color: var(--peach-text);790 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;791 line-height: 1.3;792 }793 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }794 .entry-sub {795 font-size: 10px; color: var(--peach-text-dim);796 font-family: "IBM Plex Mono", monospace;797 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;798 margin-top: 1px;799 }800801 .totp-badge {802 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;803 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);804 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);805 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;806 }807 .passkey-badge {808 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;809 border-radius: 4px;810 background: color-mix(in srgb, var(--peach-accent) 14%, transparent);811 color: color-mix(in srgb, var(--peach-accent) 85%, transparent);812 border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);813 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;814 }815816 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }817818 .empty-hint {819 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);820 font-family: "IBM Plex Mono", monospace;821 }822823 /* Create form */824 .create-form-wrap {825 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;826 }827 .create-head {828 display: flex; align-items: center; gap: 8px;829 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);830 }831 .back-btn {832 background: transparent; border: none; cursor: pointer;833 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;834 display: flex; align-items: center; line-height: 0;835 transition: color 100ms, background 100ms;836 }837 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }838 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }839 /* minmax(0, 1fr) caps the grid track at the popup width — without it,840 long monospace content (e.g. a 60-char generated password) would841 auto-size the track wider than the popup and overflow the right edge. */842 .create-body { padding: 12px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; }843 .f-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: 3px; }844 .f-label {845 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;846 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;847 }848 .f-input {849 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);850 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;851 font-size: 11px; font-family: "IBM Plex Mono", monospace;852 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;853 }854 .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); }855 .f-input::placeholder { color: var(--peach-text-muted); }856 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }857 .f-row { display: flex; gap: 6px; }858 .f-row .f-input { flex: 1; min-width: 0; }859 .gen-btn {860 padding: 0 10px; border-radius: 7px; border: none;861 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);862 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);863 font-size: 10px; font-family: "IBM Plex Mono", monospace;864 cursor: pointer; white-space: nowrap; flex-shrink: 0;865 display: flex; align-items: center; gap: 4px;866 transition: background 100ms;867 }868 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }869 .gen-toggle-link {870 background: none; border: none; padding: 2px 0;871 color: color-mix(in srgb, var(--peach-accent) 60%, transparent);872 font-size: 10px; font-family: "IBM Plex Mono", monospace;873 cursor: pointer; text-align: left; transition: color 100ms;874 }875 .gen-toggle-link:hover { color: var(--peach-accent); }876 .pw-history {877 display: grid; gap: 4px; margin-top: 4px;878 max-height: 150px; overflow-y: auto; scrollbar-width: thin;879 }880 .pw-history-row {881 display: flex; align-items: center; justify-content: space-between; gap: 6px;882 padding: 5px 7px; border-radius: 7px;883 background: color-mix(in srgb, var(--peach-accent) 6%, transparent);884 border: 1px solid color-mix(in srgb, var(--peach-accent) 10%, transparent);885 }886 .pw-history-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }887 .pw-history-pw {888 font-family: "IBM Plex Mono", monospace; font-size: 11px;889 color: var(--peach-t1, #eee); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;890 }891 .pw-history-age {892 font-family: "IBM Plex Mono", monospace; font-size: 9px;893 color: color-mix(in srgb, var(--peach-accent) 55%, transparent); letter-spacing: 0.03em;894 }895 .pw-history-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }896 .pw-history-btn {897 background: none; border: none; padding: 2px 5px; border-radius: 5px;898 color: color-mix(in srgb, var(--peach-accent) 65%, transparent);899 font-size: 9px; font-family: "IBM Plex Mono", monospace; cursor: pointer;900 transition: color 100ms, background 100ms;901 }902 .pw-history-btn:hover {903 color: var(--peach-accent);904 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);905 }906 .pw-history-empty {907 font-family: "IBM Plex Mono", monospace; font-size: 10px; line-height: 1.45;908 color: color-mix(in srgb, var(--peach-accent) 55%, transparent);909 padding: 6px 8px; border-radius: 7px;910 background: color-mix(in srgb, var(--peach-accent) 5%, transparent);911 border: 1px solid color-mix(in srgb, var(--peach-accent) 9%, transparent);912 }913 .save-btn {914 width: 100%; padding: 8px; border-radius: 8px; border: none;915 background: var(--peach-accent); color: #fff; font-size: 12px;916 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;917 transition: background 120ms, box-shadow 120ms, transform 80ms;918 letter-spacing: 0.01em;919 }920 .save-btn:hover {921 background: color-mix(in srgb, var(--peach-accent) 82%, #000);922 box-shadow: 0 4px 20px var(--peach-glow, transparent);923 }924 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }925 .url-hint {926 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);927 font-family: "IBM Plex Mono", monospace; font-style: italic;928 }929930 /* Success */931 .success-wrap {932 display: flex; flex-direction: column; align-items: center;933 padding: 24px 16px 20px; gap: 8px;934 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;935 }936 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }937 .success-ring {938 width: 40px; height: 40px; border-radius: 50%;939 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);940 display: flex; align-items: center; justify-content: center;941 }942 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }943 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }944945 /* Silver platter (State 1 — single match) */946 .silver-platter {947 padding: 14px 14px 12px;948 display: flex; flex-direction: column; gap: 6px;949 }950 .sp-title {951 font-size: 14px; font-weight: 500; color: var(--peach-text);952 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;953 line-height: 1.25;954 }955 .sp-identity-row {956 display: flex; align-items: baseline; gap: 5px; margin-bottom: 3px;957 }958 .sp-identity-label {959 font-size: 9px; font-weight: 600; letter-spacing: 0.05em;960 text-transform: uppercase; color: var(--peach-text-muted); flex-shrink: 0;961 }962 .sp-username {963 font-size: 12px; color: var(--peach-text-dim);964 font-family: "IBM Plex Mono", monospace;965 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;966 margin-bottom: 0;967 }968 .sp-identity-row + .sp-fill-btn, .sp-username + .sp-fill-btn { margin-top: 6px; }969 .sp-fill-btn {970 width: 100%; padding: 11px; border-radius: 9px; border: none;971 background: var(--peach-accent); color: #fff;972 font-size: 13px; font-weight: 500; cursor: pointer;973 font-family: Inter, sans-serif; letter-spacing: 0.01em;974 display: flex; align-items: center; justify-content: center; min-height: 38px;975 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;976 }977 .sp-fill-btn:hover {978 filter: brightness(1.1);979 box-shadow: 0 4px 20px var(--peach-glow, transparent);980 transform: translateY(-1px);981 }982 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }983 .sp-fill-btn.filled {984 background: #16a34a; transform: none; filter: none;985 box-shadow: 0 4px 16px rgba(22,163,74,0.35);986 cursor: default;987 }988 .sp-fill-btn:disabled { cursor: default; }989 .sp-actions-group {990 margin-top: 6px;991 display: flex; flex-direction: column; gap: 4px;992 }993 .sp-actions-label {994 font-size: 8px; letter-spacing: 0.18em; text-transform: uppercase;995 color: var(--peach-text-muted);996 font-family: "IBM Plex Mono", monospace;997 text-align: center;998 }999 .sp-actions {1000 display: flex; gap: 4px; justify-content: space-between; align-items: center;1001 }1002 .sp-action-btn {1003 flex: 1; height: 28px; border: none; border-radius: 6px;1004 background: transparent; color: var(--peach-text-dim);1005 font-size: 12px; cursor: pointer;1006 transition: background 120ms ease, color 120ms ease;1007 font-family: Inter, sans-serif; letter-spacing: 0.04em;1008 display: inline-flex; align-items: center; justify-content: center; gap: 4px;1009 white-space: nowrap; overflow: hidden;1010 }1011 .sp-action-btn:hover:not(:disabled) {1012 background: color-mix(in srgb, var(--peach-text) 5%, transparent);1013 color: var(--peach-text);1014 }1015 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }1016 .sp-action-btn.flashed {1017 color: var(--peach-accent);1018 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1019 }10201021 /* TOTP tray — shown below the silver-platter secondary actions */1022 .sp-tray {1023 margin-top: 8px;1024 padding: 10px 12px;1025 border-radius: 9px;1026 background: var(--peach-input-bg);1027 border: 1px solid var(--peach-input-border);1028 display: flex; align-items: center; gap: 12px;1029 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;1030 }1031 .sp-tray-label {1032 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;1033 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;1034 flex-shrink: 0;1035 }1036 .sp-totp-code {1037 font-size: 18px; font-weight: 500;1038 color: var(--peach-text);1039 font-family: "IBM Plex Mono", monospace;1040 letter-spacing: 0.12em;1041 flex: 1;1042 text-align: center;1043 }1044 .sp-totp-ring {1045 width: 22px; height: 22px; flex-shrink: 0;1046 }1047 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }1048 .sp-totp-ring .ring-fill {1049 stroke: var(--peach-accent);1050 transition: stroke-dashoffset 1s linear;1051 transform: rotate(-90deg);1052 transform-origin: center;1053 }1054 .sp-totp-copy {1055 background: transparent; border: none; cursor: pointer;1056 padding: 4px 8px; border-radius: 5px;1057 color: var(--peach-text-dim); font-size: 11px;1058 font-family: Inter, sans-serif;1059 transition: background 120ms, color 120ms;1060 }1061 .sp-totp-copy:hover {1062 background: color-mix(in srgb, var(--peach-text) 5%, transparent);1063 color: var(--peach-text);1064 }10651066 /* Card silver platter (State 7) — reuses .sp-* primary action classes */1067 .card-platter {1068 padding: 12px 14px 10px;1069 display: flex; flex-direction: column; gap: 4px;1070 }1071 .cp-network {1072 font-size: 10px; font-weight: 600;1073 color: var(--peach-accent);1074 letter-spacing: 0.08em; text-transform: uppercase;1075 font-family: Inter, sans-serif;1076 }1077 .cp-nickname {1078 font-size: 14px; font-weight: 500; color: var(--peach-text);1079 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1080 line-height: 1.2;1081 }1082 .cp-last4 {1083 font-size: 12px; color: var(--peach-text-dim);1084 font-family: "IBM Plex Mono", monospace;1085 letter-spacing: 0.04em;1086 margin-top: 2px;1087 }1088 .cp-meta {1089 font-size: 11px; color: var(--peach-text-dim);1090 font-family: "IBM Plex Mono", monospace;1091 margin-top: 2px;1092 display: flex; gap: 10px; align-items: center;1093 }1094 .cp-meta-sep { opacity: 0.5; }1095 .cp-fill-wrap { margin-top: 10px; }10961097 /* Locked state */1098 .locked-wrap {1099 display: flex; flex-direction: column; align-items: stretch;1100 padding: 14px 13px 13px; gap: 9px;1101 }1102 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }1103 .locked-icon {1104 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;1105 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1106 display: flex; align-items: center; justify-content: center;1107 }1108 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }1109 .locked-sub { font-size: 10.5px; line-height: 1.4; color: var(--peach-text-muted); text-align: center; margin: 2px 0 2px; }1110 .locked-input {1111 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1112 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;1113 font-size: 11px; font-family: "IBM Plex Mono", monospace;1114 outline: none; transition: border-color 120ms, box-shadow 120ms;1115 }1116 .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); }1117 .locked-input::placeholder { color: var(--peach-text-muted); }1118 .locked-btn {1119 width: 100%; padding: 8px; border-radius: 8px; border: none;1120 background: var(--peach-accent); color: #fff; font-size: 12px;1121 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;1122 transition: background 120ms;1123 letter-spacing: 0.01em;1124 box-shadow: 0 2px 16px var(--peach-glow, transparent);1125 animation: peach-pulse-glow 2.4s ease-in-out infinite;1126 }1127 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }1128 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }1129 .locked-error {1130 font-size: 10px; color: rgba(220,80,60,0.85);1131 font-family: "IBM Plex Mono", monospace;1132 }11331134 @keyframes peach-pulse-glow {1135 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }1136 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),1137 0 0 48px var(--peach-glow, transparent); }1138 }11391140 /* ── State 10: identity helper ─────────────────────────────── */1141 .identity-list {1142 max-height: 192px; overflow-y: auto; padding: 4px 0;1143 scrollbar-width: none;1144 }1145 .identity-list::-webkit-scrollbar { display: none; }1146 .peach-identity-row {1147 display: flex; align-items: center; gap: 10px;1148 padding: 9px 12px; cursor: pointer; border-radius: 8px;1149 transition: background 100ms ease;1150 }1151 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1152 .peach-identity-value {1153 font-family: "IBM Plex Mono", monospace; font-size: 13px;1154 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;1155 }1156 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }1157 .identity-search-wrap {1158 display: flex; flex-direction: column; gap: 6px;1159 }1160 .identity-mode-toggle {1161 display: inline-flex; gap: 4px; flex-wrap: wrap;1162 }1163 .identity-mode-btn {1164 border: 1px solid var(--peach-input-border);1165 background: var(--peach-input-bg);1166 color: var(--peach-text-dim);1167 border-radius: 999px;1168 padding: 4px 9px;1169 cursor: pointer;1170 font-family: "IBM Plex Mono", monospace;1171 font-size: 9px;1172 letter-spacing: 0.03em;1173 transition: background 100ms ease, border-color 100ms ease, color 100ms ease, transform 80ms ease;1174 }1175 .identity-mode-btn:hover {1176 background: color-mix(in srgb, var(--peach-text) 6%, var(--peach-input-bg));1177 color: var(--peach-text);1178 }1179 .identity-mode-btn.is-active {1180 color: var(--peach-accent);1181 border-color: color-mix(in srgb, var(--peach-accent) 30%, var(--peach-input-border));1182 background: color-mix(in srgb, var(--peach-accent) 10%, var(--peach-input-bg));1183 }1184 .identity-mode-btn:active { transform: translateY(1px); }1185 .identity-custom-row {1186 display: flex; flex-direction: column; gap: 2px;1187 padding: 9px 12px; cursor: pointer; border-radius: 8px;1188 transition: background 100ms ease;1189 }1190 .identity-custom-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1191 .identity-custom-row-title {1192 color: var(--peach-text); font-size: 12px; font-family: Inter, sans-serif;1193 }1194 .identity-custom-row-sub {1195 color: var(--peach-text-muted); font-size: 10px; font-family: "IBM Plex Mono", monospace;1196 }1197 .identity-empty {1198 padding: 10px 12px 2px; color: var(--peach-text-dim);1199 font-size: 10px; font-family: "IBM Plex Mono", monospace;1200 }1201 .identity-alt-row {1202 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1203 border-radius: 8px; transition: background 100ms ease;1204 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;1205 }1206 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1207 .identity-escape-row {1208 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1209 border-radius: 8px; transition: background 100ms ease; outline: none;1210 color: var(--peach-accent); font-size: 11px; font-family: Inter, sans-serif;1211 margin-top: 2px;1212 }1213 .identity-escape-row:hover, .identity-escape-row:focus { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }1214 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }1215 .identity-alt-input-wrap.open { display: flex; }1216 .identity-alt-input {1217 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;1218 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);1219 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;1220 outline: none; transition: border-color 120ms;1221 }1222 .identity-alt-input:focus {1223 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);1224 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);1225 }1226 .identity-alt-confirm {1227 height: 32px; padding: 0 12px; border-radius: 7px; border: none;1228 background: var(--peach-accent); color: #fff; font-size: 12px;1229 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;1230 }1231 /* ── Generator view (after identity picked) ────────────────── */1232 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1233 .gen-confirmed {1234 display: flex; align-items: center; gap: 8px; padding: 7px 10px;1235 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1236 border-radius: 8px;1237 }1238 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }1239 .gen-confirmed-val {1240 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1241 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1242 }1243 .gen-label {1244 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;1245 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;1246 }1247 .gen-pw-row { display: flex; gap: 6px; align-items: center; }1248 .gen-pw-val {1249 flex: 1; padding: 8px 10px; border-radius: 7px;1250 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1251 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1252 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1253 }1254 .gen-regen-btn {1255 height: 34px; padding: 0 10px; border-radius: 7px; border: none;1256 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1257 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;1258 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;1259 }1260 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1261 .gen-fill-btn {1262 padding: 10px; border-radius: 8px; border: none;1263 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;1264 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;1265 transition: filter 120ms ease, transform 80ms ease;1266 }1267 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }1268 .gen-fill-btn:active { transform: translateY(0); }12691270 /* ── Smart password generator (parity with main UI compact mode) ─── */1271 .pwgen-block { display: flex; flex-direction: column; gap: 8px; }1272 .pwgen-display-row {1273 display: flex; align-items: center; gap: 6px;1274 padding: 8px 10px; border-radius: 7px;1275 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1276 }1277 .pwgen-output {1278 flex: 1; min-width: 0; width: 0;1279 font-family: "IBM Plex Mono", monospace; font-size: 12px;1280 color: var(--peach-text);1281 background: transparent; border: none; outline: none; padding: 0;1282 }1283 .pwgen-icon-btn {1284 flex-shrink: 0;1285 width: 26px; height: 26px;1286 display: inline-flex; align-items: center; justify-content: center;1287 background: transparent; border: 1px solid var(--peach-input-border);1288 border-radius: 6px; color: var(--peach-text-dim); cursor: pointer;1289 padding: 0; transition: background 100ms, color 100ms, border-color 100ms;1290 }1291 .pwgen-icon-btn:hover {1292 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1293 color: var(--peach-accent);1294 border-color: color-mix(in srgb, var(--peach-accent) 30%, transparent);1295 }1296 .pwgen-icon-btn.is-copied {1297 background: color-mix(in srgb, var(--peach-accent) 18%, transparent);1298 color: var(--peach-accent);1299 border-color: color-mix(in srgb, var(--peach-accent) 45%, transparent);1300 }1301 .pwgen-controls-row {1302 display: flex; align-items: center; gap: 8px;1303 flex-wrap: wrap;1304 }1305 .pwgen-slider {1306 flex: 1; min-width: 110px;1307 accent-color: var(--peach-accent);1308 cursor: pointer; height: 2px; background: transparent;1309 }1310 .pwgen-len-num {1311 font-family: "IBM Plex Mono", monospace; font-size: 11px;1312 color: var(--peach-accent); min-width: 22px; text-align: right;1313 }1314 .pwgen-len-num--edit { cursor: text; border-bottom: 1px dotted color-mix(in srgb, var(--peach-accent) 50%, transparent); }1315 .pwgen-len-input {1316 font-family: "IBM Plex Mono", monospace; font-size: 11px;1317 color: var(--peach-accent); width: 28px; text-align: right;1318 background: transparent; border: none; border-bottom: 1px solid var(--peach-accent);1319 outline: none; padding: 0; min-width: 22px;1320 }1321 @keyframes identity-suggest-in {1322 from { opacity: 0; transform: translateY(-4px); }1323 to { opacity: 1; transform: none; }1324 }1325 .identity-suggest-list { display: flex; flex-direction: column; gap: 1px; margin-top: 3px;1326 animation: identity-suggest-in 180ms cubic-bezier(0.16,1,0.3,1) both; }1327 .identity-filter-row { display: flex; align-items: center; gap: 4px; padding: 2px 0 4px; }1328 .identity-collapse-btn {1329 margin-left: auto; background: transparent; border: none; cursor: pointer;1330 color: var(--peach-text-muted); padding: 2px 4px; display: flex; align-items: center;1331 line-height: 0; border-radius: 3px;1332 transition: color 100ms ease, transform 220ms cubic-bezier(0.16,1,0.3,1);1333 }1334 .identity-collapse-btn:hover { color: var(--peach-text); }1335 .identity-collapse-btn.is-collapsed { transform: rotate(-90deg); }1336 .identity-rows { display: grid; grid-template-rows: 1fr;1337 transition: grid-template-rows 220ms cubic-bezier(0.16,1,0.3,1); }1338 .identity-rows.is-collapsed { grid-template-rows: 0fr; }1339 .identity-rows-inner { overflow-y: auto; min-height: 0; max-height: 148px;1340 scrollbar-width: thin; scrollbar-color: var(--peach-text-muted) transparent; }1341 .identity-rows-inner::-webkit-scrollbar { width: 4px; }1342 .identity-rows-inner::-webkit-scrollbar-track { background: transparent; }1343 .identity-rows-inner::-webkit-scrollbar-thumb { background: var(--peach-text-muted); border-radius: 2px; }1344 .identity-filter-pill {1345 padding: 2px 7px; border-radius: 999px;1346 border: 1px solid var(--peach-input-border);1347 background: transparent; color: var(--peach-text-muted);1348 font-family: "IBM Plex Mono", monospace; font-size: 9px;1349 cursor: pointer; transition: border-color 80ms, background 80ms, color 80ms;1350 }1351 .identity-filter-pill.is-on {1352 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1353 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1354 color: var(--peach-accent);1355 }1356 .identity-suggest-row {1357 display: flex; align-items: center; gap: 5px;1358 width: 100%; padding: 4px 6px; border-radius: 5px; border: none;1359 background: transparent; cursor: pointer; text-align: left;1360 transition: background 80ms;1361 }1362 .identity-suggest-row:hover,1363 .identity-suggest-row:focus { background: color-mix(in srgb, var(--peach-accent) 10%, transparent); outline: none; }1364 .identity-suggest-val {1365 font-family: "IBM Plex Mono", monospace; font-size: 10px; color: var(--peach-text);1366 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;1367 }1368 .identity-suggest-type {1369 font-family: "IBM Plex Mono", monospace; font-size: 8px; font-weight: 600;1370 flex-shrink: 0; padding: 1px 3px; border-radius: 3px;1371 }1372 .identity-suggest-type--email {1373 color: color-mix(in srgb, var(--peach-accent) 70%, transparent);1374 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1375 }1376 .identity-suggest-type--username {1377 color: var(--peach-text-muted);1378 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1379 }1380 .identity-suggest-meta {1381 font-family: "IBM Plex Mono", monospace; font-size: 9px;1382 color: var(--peach-text-muted); flex-shrink: 0;1383 }1384 .pwgen-pills { display: flex; gap: 5px; flex-wrap: wrap; }1385 .pwgen-pill {1386 padding: 3px 8px; border-radius: 999px;1387 border: 1px solid var(--peach-input-border);1388 background: transparent; color: var(--peach-text-muted);1389 font-family: "IBM Plex Mono", monospace; font-size: 9px;1390 letter-spacing: 0.06em; cursor: pointer;1391 transition: all 130ms ease;1392 }1393 .pwgen-pill.is-on {1394 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1395 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1396 color: var(--peach-accent);1397 }1398 .pwgen-pill:not(.is-on):hover {1399 color: var(--peach-text-dim);1400 border-color: var(--peach-text-muted);1401 }1402 .pwgen-policy-hint {1403 font-size: 10px; color: var(--peach-text-muted);1404 font-family: "IBM Plex Mono", monospace; letter-spacing: 0.04em;1405 line-height: 1.3;1406 }1407 .pwgen-policy-hint .pwgen-hint-accent { color: var(--peach-accent); }14081409 /* ── Save offer (after password filled) ───────────────────── */1410 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1411 .save-offer-title {1412 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;1413 }1414 .save-offer-row {1415 display: flex; align-items: center; gap: 8px; padding: 6px 8px;1416 border-radius: 7px; background: var(--peach-input-bg);1417 border: 1px solid var(--peach-input-border);1418 }1419 .save-offer-lbl {1420 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;1421 font-family: "IBM Plex Mono", monospace;1422 }1423 .save-offer-val {1424 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1425 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1426 }1427 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }1428 .save-offer-save {1429 flex: 1; padding: 9px; border-radius: 8px; border: none;1430 background: var(--peach-accent); color: #fff; font-size: 13px;1431 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;1432 }1433 .save-offer-save:hover { filter: brightness(1.08); }1434 .save-offer-skip {1435 flex: 1; padding: 9px; border-radius: 8px; border: none;1436 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1437 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;1438 font-family: Inter, sans-serif; transition: background 100ms ease;1439 }1440 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1441`;14421443export function showAutofillSelector(1444 anchorEl: HTMLInputElement,1445 entries: SelectorEntry[],1446 onSelect: (id: string) => void,1447 options: SelectorOptions = {}1448): () => void {1449 const host = document.createElement("div");1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 const modalAncestor = (1464 anchorEl.closest<HTMLElement>('dialog, [aria-modal="true"], [role="dialog"]') ??1465 (() => {1466 let cur: HTMLElement | null = anchorEl.parentElement;1467 while (cur && cur !== document.body) {1468 if (window.getComputedStyle(cur).position === 'fixed') return cur;1469 cur = cur.parentElement;1470 }1471 return null;1472 })()1473 );1474 host.style.cssText = `position: fixed; z-index: 2147483647;`;1475 host.style.setProperty("--peach-accent", accentHex);1476 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));1477 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));1478 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1479 1480 1481 1482 host.style.willChange = "top, left";1483 1484 1485 1486 1487 1488 const shadow = host.attachShadow({ mode: "closed", delegatesFocus: true });14891490 const style = shadow.appendChild(document.createElement("style"));1491 style.textContent = SELECTOR_STYLES;14921493 const rect = anchorEl.getBoundingClientRect();1494 1495 1496 host.style.top = `${rect.bottom + 5}px`;1497 host.style.left = `${rect.left}px`;1498 1499 1500 1501 1502 const popupWidth = Math.min(Math.max(rect.width, 320), 400);1503 host.style.width = `${popupWidth}px`;15041505 const container = shadow.appendChild(document.createElement("div"));1506 container.className = "peach-inline-popup";1507 container.setAttribute("data-theme", popupTheme);15081509 1510 1511 let searchableEntries: SelectorEntry[] = options.searchableEntries ?? [];1512 let searchableLoading = false;1513 let searchableLoaded = !options.loadSearchableEntries;15141515 async function ensureSearchableLoaded(rerender: () => void): Promise<void> {1516 if (searchableLoaded || searchableLoading || !options.loadSearchableEntries) {1517 return;1518 }1519 searchableLoading = true;1520 try {1521 searchableEntries = await options.loadSearchableEntries();1522 } catch {1523 searchableEntries = [];1524 } finally {1525 searchableLoaded = true;1526 searchableLoading = false;1527 rerender();1528 }1529 }1530 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();15311532 let query = "";1533 let focusedIndex = -1;1534 let currentMode: "list" | "create" | "success" | "silver-platter" | "identity-helper" | "edit" = "list";1535 let searchInput: HTMLInputElement | null = null;1536 let identityMode: IdentityFilterMode = "both";1537 1538 1539 1540 let silverPlatterOrigin: SelectorEntry | null = null;1541 1542 1543 const cleanupHooks: Array<() => void> = [];15441545 1546 1547 1548 {1549 let prevH = 0;1550 let blocked = false;1551 const heightRo = new ResizeObserver(([entry]) => {1552 if (blocked) return;1553 const h = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;1554 if (prevH > 0 && Math.abs(h - prevH) > 1) {1555 blocked = true;1556 container.style.height = `${prevH}px`;1557 requestAnimationFrame(() => requestAnimationFrame(() => {1558 container.style.height = `${h}px`;1559 prevH = h;1560 const onEnd = (e: TransitionEvent) => {1561 if (e.propertyName !== "height") return;1562 container.style.height = "";1563 blocked = false;1564 container.removeEventListener("transitionend", onEnd);1565 };1566 container.addEventListener("transitionend", onEnd);1567 }));1568 } else {1569 prevH = h;1570 }1571 });1572 heightRo.observe(container);1573 cleanupHooks.push(() => heightRo.disconnect());1574 }15751576 1577 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";1578 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");1579 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";15801581 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {1582 const seen = new Set<string>();1583 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });1584 }15851586 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {1587 const q = value.trim().toLowerCase();1588 const deduped = uniqueEntries(source);1589 if (!q) return deduped.slice(0, 8);1590 return deduped.filter(e =>1591 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)1592 ).slice(0, 12);1593 }15941595 function activeEntries(): SelectorEntry[] {1596 1597 if (query.trim()) {1598 const merged = uniqueEntries([...entries, ...searchableEntries]);1599 return filterEntries(merged, query);1600 }1601 1602 if (entries.length > 0) return entries;1603 return filterEntries(searchableEntries, "").slice(0, 8);1604 }16051606 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {1607 const icon = document.createElement("div");1608 icon.className = `entry-icon${isNew ? " is-new" : ""}`;1609 if (isNew) {1610 icon.innerHTML = plusSvgStr();1611 } else {1612 1613 1614 1615 void url;1616 icon.innerHTML = keySvgStr();1617 }1618 return icon;1619 }16201621 function renderIdentityHelper() {1622 currentMode = "identity-helper";1623 container.innerHTML = "";1624 focusedIndex = -1;1625 searchInput = null;16261627 const fallbackKind: IdentityKind = options.identityKind ?? "username";1628 const identities = filterSavedIdentities(options.savedIdentities ?? [], query, identityMode);1629 const typedQuery = query.trim();1630 const customKind = resolveCustomIdentityKind(typedQuery, identityMode, fallbackKind);16311632 function applyIdentity(identity: string) {1633 if (options.hasEmailField) {1634 if (options.identityKind === "email") {1635 options.onFillFields?.("", "", identity); 1636 renderSecondaryIdentityStep("username", identity);1637 } else {1638 options.onFillFields?.(identity, "", ""); 1639 renderSecondaryIdentityStep("email", identity);1640 }1641 } else {1642 options.onFillFields?.(identity, "");1643 renderGenerator(identity);1644 }1645 }16461647 function applyCustomIdentity(identity: string) {1648 const trimmed = identity.trim();1649 if (!trimmed) return;1650 applyIdentity(trimmed);1651 }16521653 1654 const hdr = container.appendChild(document.createElement("div"));1655 hdr.className = "domain-header";1656 const headText = hdr.appendChild(document.createElement("div"));1657 headText.className = "domain-header-text";1658 const eyebrow = headText.appendChild(document.createElement("div"));1659 eyebrow.className = "domain-eyebrow";1660 eyebrow.textContent = "NEW ACCOUNT";1661 const domName = headText.appendChild(document.createElement("div"));1662 domName.className = "domain-name";1663 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");16641665 const actions = hdr.appendChild(document.createElement("div"));1666 actions.className = "identity-mode-toggle";1667 for (const mode of ["both", "email", "username"] as const) {1668 const btn = actions.appendChild(document.createElement("button"));1669 btn.type = "button";1670 btn.className = `identity-mode-btn${identityMode === mode ? " is-active" : ""}`;1671 btn.textContent = identityFilterLabel(mode);1672 btn.setAttribute("aria-pressed", String(identityMode === mode));1673 btn.addEventListener("click", () => {1674 identityMode = mode;1675 renderIdentityHelper();1676 });1677 }16781679 const searchWrap = container.appendChild(document.createElement("div"));1680 searchWrap.className = "search-wrap identity-search-wrap";1681 const search = searchWrap.appendChild(document.createElement("input"));1682 search.className = "search";1683 search.placeholder = "search identities…";1684 search.value = query;1685 search.addEventListener("input", () => {1686 query = search.value;1687 renderIdentityHelper();1688 searchInput?.focus();1689 searchInput?.setSelectionRange(query.length, query.length);1690 });1691 search.addEventListener("keydown", (e) => {1692 if (e.key === "Escape") {1693 e.preventDefault();1694 dismiss(true);1695 return;1696 }1697 if (e.key !== "Enter") return;1698 const exact = identities.find((id) => id.value.toLowerCase() === typedQuery.toLowerCase());1699 if (exact) {1700 e.preventDefault();1701 applyIdentity(exact.value);1702 return;1703 }1704 if (!typedQuery || identities.length > 0) return;1705 e.preventDefault();1706 applyCustomIdentity(typedQuery);1707 });1708 const searchTools = searchWrap.appendChild(document.createElement("div"));1709 searchTools.className = "identity-mode-toggle";1710 const sortHint = searchTools.appendChild(document.createElement("div"));1711 sortHint.className = "identity-empty";1712 sortHint.style.padding = "0";1713 sortHint.textContent = "sorted by recent use";1714 searchInput = search;1715 const customLabel = identityMode === "both"1716 ? customKind1717 : identityMode;17181719 1720 const list = container.appendChild(document.createElement("div"));1721 list.className = "identity-list";1722 1723 1724 1725 1726 const updateListFade = () => {1727 const overflowing = list.scrollHeight > list.clientHeight + 1;1728 const atBottom = list.scrollTop + list.clientHeight >= list.scrollHeight - 2;1729 const mask = overflowing && !atBottom1730 ? "linear-gradient(to bottom, black calc(100% - 24px), transparent 100%)"1731 : "none";1732 list.style.setProperty("mask-image", mask);1733 list.style.setProperty("-webkit-mask-image", mask);1734 };1735 list.addEventListener("scroll", updateListFade);1736 if (typedQuery && identities.length === 0) {1737 const empty = list.appendChild(document.createElement("div"));1738 empty.className = "identity-empty";1739 empty.textContent = identityMode === "both"1740 ? `No match for “${typedQuery}”.`1741 : `No ${identityFilterLabel(identityMode).toLowerCase()} match for “${typedQuery}”.`;1742 const custom = list.appendChild(document.createElement("div"));1743 custom.className = "identity-custom-row";1744 const customTitle = custom.appendChild(document.createElement("div"));1745 customTitle.className = "identity-custom-row-title";1746 customTitle.textContent = `Use “${typedQuery}” as a new ${customLabel}`;1747 const customSub = custom.appendChild(document.createElement("div"));1748 customSub.className = "identity-custom-row-sub";1749 customSub.textContent = "Press Enter or click to continue.";1750 custom.addEventListener("click", () => applyCustomIdentity(typedQuery));1751 }1752 for (const id of identities) {1753 const row = list.appendChild(document.createElement("div"));1754 row.className = "peach-identity-row";1755 row.setAttribute("tabindex", "-1");1756 const val = row.appendChild(document.createElement("div"));1757 val.className = "peach-identity-value";1758 val.textContent = id.value;1759 const meta = row.appendChild(document.createElement("div"));1760 meta.className = "peach-identity-meta";1761 meta.textContent = `used ${id.useCount}×`;1762 row.addEventListener("click", () => {1763 applyIdentity(id.value);1764 });1765 }17661767 updateListFade();17681769 1770 if (identities.length > 0) {1771 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1772 }17731774 1775 const altRow = container.appendChild(document.createElement("div"));1776 altRow.className = "identity-alt-row";1777 altRow.textContent = `+ Use a different ${customLabel}…`;17781779 const altInputWrap = container.appendChild(document.createElement("div"));1780 altInputWrap.className = "identity-alt-input-wrap";1781 const altInput = altInputWrap.appendChild(document.createElement("input"));1782 altInput.className = "identity-alt-input";1783 altInput.type = "text";1784 altInput.placeholder = customLabel === "email" ? "[email protected]" : "your username";1785 altInput.setAttribute("autocomplete", "off");1786 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1787 altConfirm.className = "identity-alt-confirm";1788 altConfirm.type = "button";1789 altConfirm.textContent = "✓";17901791 function fillCustomIdentity() {1792 const v = altInput.value.trim();1793 if (!v) return;1794 applyCustomIdentity(v);1795 }17961797 altRow.addEventListener("click", () => {1798 altRow.style.display = "none";1799 altInputWrap.classList.add("open");1800 altInput.value = typedQuery || altInput.value;1801 altInput.focus();1802 altInput.setSelectionRange(altInput.value.length, altInput.value.length);1803 });1804 altConfirm.addEventListener("click", fillCustomIdentity);1805 altInput.addEventListener("keydown", (e) => {1806 if (e.key === "Enter") { e.preventDefault(); fillCustomIdentity(); }1807 });18081809 1810 1811 1812 1813 1814 1815 1816 if (options.loadSearchableEntries || (options.searchableEntries?.length ?? 0) > 0) {1817 const escapeRow = container.appendChild(document.createElement("div"));1818 escapeRow.className = "identity-escape-row";1819 escapeRow.textContent = "I already have an account →";1820 escapeRow.setAttribute("tabindex", "0");1821 const toSearch = () => { void ensureSearchableLoaded(renderList); renderList(); };1822 escapeRow.addEventListener("click", toSearch);1823 escapeRow.addEventListener("keydown", (e) => {1824 if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toSearch(); }1825 });1826 }18271828 requestAnimationFrame(() => {1829 search.focus();1830 search.setSelectionRange(query.length, query.length);1831 });1832 }18331834 18351836183718381839 function renderSecondaryIdentityStep(stepKind: "email" | "username", firstValue: string) {1840 currentMode = "identity-helper";1841 container.innerHTML = "";18421843 const wrap = container.appendChild(document.createElement("div"));1844 wrap.className = "gen-view";18451846 1847 const hdrRow = wrap.appendChild(document.createElement("div"));1848 hdrRow.className = "domain-header";1849 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1850 eyebrow2.className = "domain-eyebrow";1851 eyebrow2.textContent = "NEW ACCOUNT";1852 const domName2 = hdrRow.appendChild(document.createElement("div"));1853 domName2.className = "domain-name";1854 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");18551856 1857 const conf = wrap.appendChild(document.createElement("div"));1858 conf.className = "gen-confirmed";1859 const confCheck = conf.appendChild(document.createElement("span"));1860 confCheck.className = "gen-confirmed-check";1861 confCheck.textContent = "✓";1862 const confVal = conf.appendChild(document.createElement("div"));1863 confVal.className = "gen-confirmed-val";1864 confVal.textContent = firstValue;18651866 const stepLbl = wrap.appendChild(document.createElement("div"));1867 stepLbl.className = "gen-label";1868 stepLbl.textContent = stepKind === "email" ? "Now choose your email" : "Now choose your username";18691870 1871 const identities = (options.savedIdentities ?? [])1872 .filter(s => s.type === stepKind)1873 .sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0))1874 .slice(0, 6);18751876 const list = wrap.appendChild(document.createElement("div"));1877 list.className = "identity-list";1878 for (const id of identities) {1879 const row = list.appendChild(document.createElement("div"));1880 row.className = "peach-identity-row";1881 row.setAttribute("tabindex", "-1");1882 const val = row.appendChild(document.createElement("div"));1883 val.className = "peach-identity-value";1884 val.textContent = id.value;1885 const meta = row.appendChild(document.createElement("div"));1886 meta.className = "peach-identity-meta";1887 meta.textContent = `used ${id.useCount}×`;1888 row.addEventListener("click", () => applySecondIdentity(id.value));1889 }18901891 1892 const altInputWrap = wrap.appendChild(document.createElement("div"));1893 altInputWrap.className = "identity-alt-input-wrap open";1894 const altInput = altInputWrap.appendChild(document.createElement("input"));1895 altInput.className = "identity-alt-input";1896 altInput.type = stepKind === "email" ? "email" : "text";1897 altInput.placeholder = stepKind === "email" ? "[email protected]" : "your username";1898 altInput.setAttribute("autocomplete", "off");1899 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1900 altConfirm.className = "identity-alt-confirm";1901 altConfirm.type = "button";1902 altConfirm.textContent = "→";19031904 function applySecondIdentity(value: string) {1905 const trimmed = value.trim();1906 if (!trimmed) return;1907 1908 const [email, username] = stepKind === "email"1909 ? [trimmed, firstValue]1910 : [firstValue, trimmed];1911 if (stepKind === "email") {1912 options.onFillFields?.("", "", trimmed); 1913 } else {1914 options.onFillFields?.(trimmed, "", ""); 1915 }1916 renderGenerator(email, username);1917 }19181919 altConfirm.addEventListener("click", () => applySecondIdentity(altInput.value));1920 altInput.addEventListener("keydown", (e) => {1921 if (e.key === "Enter") { e.preventDefault(); applySecondIdentity(altInput.value); }1922 });19231924 1925 const skipRow = wrap.appendChild(document.createElement("div"));1926 skipRow.className = "identity-escape-row";1927 skipRow.textContent = `Skip ${stepKind} →`;1928 skipRow.setAttribute("tabindex", "0");1929 skipRow.addEventListener("click", () => {1930 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1931 renderGenerator(email, username);1932 });1933 skipRow.addEventListener("keydown", (e) => {1934 if (e.key === "Enter" || e.key === " ") {1935 e.preventDefault();1936 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1937 renderGenerator(email, username);1938 }1939 });19401941 requestAnimationFrame(() => { altInput.focus(); });1942 }19431944 function renderGenerator(filledEmail: string, filledUsername?: string) {1945 container.innerHTML = "";1946 const sitePolicy = detectPasswordPolicy();19471948 const wrap = container.appendChild(document.createElement("div"));1949 wrap.className = "gen-view";19501951 1952 const hdrRow = wrap.appendChild(document.createElement("div"));1953 hdrRow.className = "domain-header";1954 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1955 eyebrow2.className = "domain-eyebrow";1956 eyebrow2.textContent = "NEW ACCOUNT";1957 const domName2 = hdrRow.appendChild(document.createElement("div"));1958 domName2.className = "domain-name";1959 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");19601961 1962 function confirmedRow(value: string) {1963 const conf = wrap.appendChild(document.createElement("div"));1964 conf.className = "gen-confirmed";1965 const confCheck = conf.appendChild(document.createElement("span"));1966 confCheck.className = "gen-confirmed-check";1967 confCheck.textContent = "✓";1968 const confVal = conf.appendChild(document.createElement("div"));1969 confVal.className = "gen-confirmed-val";1970 confVal.textContent = value;1971 }1972 if (filledEmail) confirmedRow(filledEmail);1973 if (filledUsername && filledUsername !== filledEmail) confirmedRow(filledUsername);19741975 const lbl = wrap.appendChild(document.createElement("div"));1976 lbl.className = "gen-label";1977 lbl.textContent = "Generate a password";19781979 let currentPw = "";1980 const smartGen = createSmartGenerator({1981 policy: sitePolicy,1982 onChange: (pw) => { currentPw = pw; },1983 });1984 wrap.appendChild(smartGen.root);19851986 const fillBtn = wrap.appendChild(document.createElement("button"));1987 fillBtn.className = "gen-fill-btn";1988 fillBtn.type = "button";1989 fillBtn.textContent = "Copy & fill";1990 fillBtn.addEventListener("click", async () => {1991 if (!currentPw) return;1992 fillBtn.disabled = true;1993 fillBtn.textContent = "Saving…";1994 1995 options.onFillFields?.("", currentPw, "");1996 1997 1998 const saveUsername = filledUsername || filledEmail;1999 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;2000 try {2001 const res = await chrome.runtime.sendMessage({2002 type: "ADD_ENTRY",2003 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: currentPw, type: "login" },2004 });2005 if ((res as { ok?: boolean }).ok) {2006 showSuccess(sTitle);2007 } else {2008 renderSaveOffer(filledEmail, currentPw, filledUsername);2009 }2010 } catch {2011 renderSaveOffer(filledEmail, currentPw, filledUsername);2012 }2013 });2014 }20152016 function renderSaveOffer(filledEmail: string, filledPassword: string, filledUsername?: string) {2017 container.innerHTML = "";2018 const domain = options.domain ?? location.hostname.replace(/^www\./, "");20192020 const wrap = container.appendChild(document.createElement("div"));2021 wrap.className = "save-offer";20222023 const title = wrap.appendChild(document.createElement("div"));2024 title.className = "save-offer-title";2025 title.textContent = "Save this login to Peach?";20262027 function detailRow(label: string, value: string) {2028 const row = wrap.appendChild(document.createElement("div"));2029 row.className = "save-offer-row";2030 const lbl = row.appendChild(document.createElement("div"));2031 lbl.className = "save-offer-lbl";2032 lbl.textContent = label;2033 const val = row.appendChild(document.createElement("div"));2034 val.className = "save-offer-val";2035 val.textContent = value;2036 }20372038 detailRow("site", domain);2039 2040 if (filledUsername) {2041 detailRow("username", filledUsername);2042 if (filledEmail) detailRow("email", filledEmail);2043 } else {2044 if (filledEmail) detailRow("email", filledEmail);2045 }2046 detailRow("password", "••••••••••••••••");20472048 const btns = wrap.appendChild(document.createElement("div"));2049 btns.className = "save-offer-btns";20502051 const saveBtn = btns.appendChild(document.createElement("button"));2052 saveBtn.className = "save-offer-save";2053 saveBtn.type = "button";2054 saveBtn.textContent = "Save";2055 saveBtn.addEventListener("click", async () => {2056 saveBtn.disabled = true;2057 saveBtn.textContent = "Saving…";2058 try {2059 2060 2061 const saveUsername = filledUsername || filledEmail;2062 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;2063 const res = await chrome.runtime.sendMessage({2064 type: "ADD_ENTRY",2065 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: filledPassword, type: "login" },2066 });2067 if ((res as { ok?: boolean }).ok) {2068 showSuccess(sTitle);2069 } else {2070 saveBtn.disabled = false;2071 saveBtn.textContent = "Save";2072 }2073 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }2074 });20752076 const skipBtn = btns.appendChild(document.createElement("button"));2077 skipBtn.className = "save-offer-skip";2078 skipBtn.type = "button";2079 skipBtn.textContent = "Not now";2080 skipBtn.addEventListener("click", () => { dismiss(true); });2081 }20822083 function renderList() {2084 container.innerHTML = "";2085 focusedIndex = -1;2086 searchInput = null;2087 currentMode = "list";20882089 const current = activeEntries();20902091 2092 2093 2094 const domHdr = document.createElement("div");2095 domHdr.className = "domain-header";2096 const headText = document.createElement("div");2097 headText.className = "domain-header-text";2098 const eyebrow = document.createElement("div");2099 eyebrow.className = "domain-eyebrow";2100 eyebrow.textContent = options.isSignup2101 ? "Sign up as"2102 : entries.length > 0 ? "Saved for this site" : "No saved login";2103 const domName = document.createElement("div");2104 domName.className = "domain-name";2105 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2106 headText.append(eyebrow, domName);2107 domHdr.appendChild(headText);21082109 if (!options.isSignup) {2110 const actions = document.createElement("div");2111 actions.className = "domain-header-actions";21122113 2114 2115 2116 if (silverPlatterOrigin) {2117 const backBtn = document.createElement("button");2118 backBtn.className = "domain-search-btn";2119 backBtn.type = "button";2120 backBtn.setAttribute("aria-label", "Back to suggested entry");2121 backBtn.title = "Back to suggested entry";2122 backBtn.innerHTML = backSvgStr();2123 backBtn.addEventListener("click", (e) => {2124 e.stopPropagation();2125 if (silverPlatterOrigin) renderSilverPlatter(silverPlatterOrigin);2126 });2127 actions.appendChild(backBtn);2128 }21292130 const addBtn = document.createElement("button");2131 addBtn.className = "domain-add-btn";2132 addBtn.type = "button";2133 addBtn.setAttribute("aria-label", "Create new entry");2134 addBtn.title = "Create new entry";2135 addBtn.textContent = "+";2136 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2137 actions.appendChild(addBtn);21382139 domHdr.appendChild(actions);2140 }21412142 container.appendChild(domHdr);21432144 2145 const sw = document.createElement("div"); sw.className = "search-wrap";2146 searchInput = document.createElement("input");2147 searchInput.className = "search";2148 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";2149 searchInput.value = query;2150 searchInput.addEventListener("input", () => {2151 query = searchInput?.value ?? "";2152 2153 2154 void ensureSearchableLoaded(renderList);2155 renderList();2156 searchInput?.focus();2157 searchInput?.setSelectionRange(query.length, query.length);2158 });2159 sw.appendChild(searchInput);2160 2161 2162 if (searchableLoading) {2163 const loadingHint = document.createElement("span");2164 loadingHint.className = "search-loading";2165 loadingHint.textContent = "searching…";2166 sw.appendChild(loadingHint);2167 }2168 container.appendChild(sw);21692170 2171 const results = document.createElement("div");2172 results.className = "results";2173 container.appendChild(results);21742175 if (current.length === 0 && entries.length > 0) {2176 2177 }21782179 for (let i = 0; i < current.length; i++) {2180 const e = current[i]!;2181 const entryEl = document.createElement("div");2182 entryEl.className = "entry";2183 entryEl.dataset.id = e.id;2184 entryEl.dataset.index = String(i);2185 entryEl.setAttribute("tabindex", "-1");21862187 entryEl.appendChild(makeEntryIcon(e.url));21882189 const info = document.createElement("div"); info.className = "entry-info";2190 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;2191 const subEl = document.createElement("div"); subEl.className = "entry-sub";2192 const domain = e.url ? getDomainFromUrl(e.url) : "";2193 subEl.textContent = domain || e.username || "";2194 info.append(titleEl, subEl);2195 entryEl.appendChild(info);21962197 if (e.hasTotp) {2198 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";2199 entryEl.appendChild(badge);2200 }2201 if (e.hasPasskey) {2202 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";2203 entryEl.appendChild(badge);2204 }22052206 entryEl.addEventListener("click", () => {2207 if (options.isSignup && options.onFillFields) {2208 2209 options.onFillFields(e.username ?? "", "", e.email);2210 dismiss();2211 } else {2212 onSelect(e.id);2213 dismiss();2214 }2215 });2216 results.appendChild(entryEl);2217 }22182219 2220 if (current.length > 0) {2221 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));2222 }22232224 2225 const newEl = document.createElement("div");2226 newEl.className = "entry";2227 newEl.setAttribute("tabindex", "-1");2228 newEl.appendChild(makeEntryIcon(undefined, true));22292230 const newInfo = document.createElement("div"); newInfo.className = "entry-info";2231 const newTitle = document.createElement("div");2232 newTitle.className = "entry-title is-new";2233 newTitle.textContent = `New entry for ${sTitle}`;2234 const newSub = document.createElement("div");2235 newSub.className = "entry-sub";2236 newSub.textContent = sOrigin;2237 newInfo.append(newTitle, newSub);2238 newEl.appendChild(newInfo);2239 newEl.addEventListener("click", showCreateForm);2240 results.appendChild(newEl);22412242 2243 if (searchInput) {2244 requestAnimationFrame(() => {2245 searchInput?.focus();2246 2247 2248 searchInput?.setSelectionRange(query.length, query.length);2249 });2250 }2251 }22522253 function setFocused(idx: number) {2254 const all = container.querySelectorAll<HTMLElement>(".entry");2255 all.forEach((el, i) => el.classList.toggle("focused", i === idx));2256 focusedIndex = idx;2257 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });2258 }22592260 function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[], fieldType: "username" | "email"): void {2261 const typed = identities.filter(s => s.type === fieldType);2262 if (!typed.length) return;2263 let collapsed = false;2264 let suppressNextFocus = false;22652266 const list = g.appendChild(document.createElement("div"));2267 list.className = "identity-suggest-list";2268 list.style.display = "none";22692270 2271 const collapseRow = list.appendChild(document.createElement("div"));2272 collapseRow.className = "identity-filter-row";2273 collapseRow.style.justifyContent = "flex-end";2274 const collapseBtn = collapseRow.appendChild(document.createElement("button"));2275 collapseBtn.type = "button";2276 collapseBtn.className = "identity-collapse-btn";2277 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>`;2278 collapseBtn.addEventListener("mousedown", (e) => {2279 e.preventDefault();2280 collapsed = !collapsed;2281 collapseBtn.classList.toggle("is-collapsed", collapsed);2282 rowsWrapper.classList.toggle("is-collapsed", collapsed);2283 });22842285 const rowsWrapper = list.appendChild(document.createElement("div"));2286 rowsWrapper.className = "identity-rows";2287 const rows = rowsWrapper.appendChild(document.createElement("div"));2288 rows.className = "identity-rows-inner";22892290 2291 2292 2293 const updateRowsFade = () => {2294 const overflowing = rows.scrollHeight > rows.clientHeight + 1;2295 const atBottom = rows.scrollTop + rows.clientHeight >= rows.scrollHeight - 2;2296 const mask = overflowing && !atBottom2297 ? "linear-gradient(to bottom, black calc(100% - 18px), transparent 100%)"2298 : "none";2299 rows.style.setProperty("mask-image", mask);2300 rows.style.setProperty("-webkit-mask-image", mask);2301 };2302 rows.addEventListener("scroll", updateRowsFade);23032304 2305 function hideList() {2306 list.style.display = "none";2307 document.removeEventListener("mousedown", onDocMousedown, { capture: true });2308 }23092310 2311 2312 2313 2314 2315 function onDocMousedown(e: MouseEvent) {2316 if (e.composedPath().includes(host)) return;2317 hideList();2318 }23192320 function pick(value: string) {2321 inp.value = value;2322 hideList();2323 suppressNextFocus = true;2324 inp.focus();2325 2326 2327 2328 setTimeout(() => { suppressNextFocus = false; }, 120);2329 }23302331 function render() {2332 const q = inp.value.toLowerCase();2333 const filtered = typed.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 8);2334 rows.innerHTML = "";2335 if (!filtered.length) { hideList(); return; }2336 for (const s of filtered) {2337 const row = rows.appendChild(document.createElement("button"));2338 row.type = "button"; row.className = "identity-suggest-row";2339 const val = row.appendChild(document.createElement("span"));2340 val.className = "identity-suggest-val"; val.textContent = s.value;2341 const meta = row.appendChild(document.createElement("span"));2342 meta.className = "identity-suggest-meta"; meta.textContent = `${s.useCount}×`;2343 row.addEventListener("mousedown", (e) => { e.preventDefault(); pick(s.value); });2344 row.addEventListener("keydown", (e) => {2345 if (e.key === "ArrowDown") {2346 e.preventDefault();2347 (row.nextElementSibling as HTMLElement | null)?.focus();2348 } else if (e.key === "ArrowUp") {2349 e.preventDefault();2350 const prev = row.previousElementSibling as HTMLElement | null;2351 if (prev) prev.focus();2352 else inp.focus();2353 } else if (e.key === "Enter" || e.key === " ") {2354 e.preventDefault();2355 pick(s.value);2356 } else if (e.key === "Escape") {2357 e.preventDefault();2358 hideList();2359 suppressNextFocus = true;2360 inp.focus();2361 }2362 });2363 2364 2365 2366 row.addEventListener("blur", (e) => {2367 const rt = (e as FocusEvent).relatedTarget as Node | null;2368 if (rt && (rt === inp || list.contains(rt))) return;2369 setTimeout(() => { hideList(); }, 150);2370 });2371 }2372 list.style.display = "";2373 updateRowsFade();2374 2375 2376 2377 document.addEventListener("mousedown", onDocMousedown, { capture: true });2378 }23792380 inp.addEventListener("focus", () => {2381 if (suppressNextFocus) { suppressNextFocus = false; return; }2382 render();2383 });2384 inp.addEventListener("input", render);2385 inp.addEventListener("keydown", (e) => {2386 if (list.style.display === "none") return;2387 if (e.key === "ArrowDown" && !collapsed) {2388 e.preventDefault();2389 (rows.firstElementChild as HTMLElement | null)?.focus();2390 } else if (e.key === "Escape") {2391 e.preventDefault();2392 hideList();2393 } else if (e.key === "Tab") {2394 2395 hideList();2396 }2397 });2398 2399 2400 2401 2402 24032404 2405 2406 cleanupHooks.push(hideList);2407 }24082409 async function showCreateForm() {2410 currentMode = "create";2411 container.innerHTML = "";24122413 2414 2415 2416 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;2417 if (!statusRes?.unlocked) {2418 renderLocked();2419 return;2420 }24212422 2423 2424 const sitePolicy = detectPasswordPolicy();24252426 2427 2428 let formIdentities: SavedIdentity[] = options.savedIdentities ?? [];2429 if (!formIdentities.length) {2430 try {2431 const idRes = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as { ok: boolean; data?: SavedIdentity[] } | null;2432 if (idRes?.ok && Array.isArray(idRes.data)) formIdentities = idRes.data;2433 } catch { }2434 }24352436 const wrap = document.createElement("div");2437 wrap.className = "create-form-wrap";24382439 2440 const head = document.createElement("div"); head.className = "create-head";2441 const backBtn = document.createElement("button"); backBtn.className = "back-btn";2442 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>`;2443 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });2444 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";2445 head.append(backBtn, lbl);24462447 2448 const body = document.createElement("div"); body.className = "create-body";24492450 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {2451 const g = document.createElement("div"); g.className = "f-group";2452 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;2453 const inp = document.createElement("input"); inp.className = "f-input";2454 g.append(l, inp); return { g, inp };2455 }24562457 2458 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";2459 body.appendChild(tg.g);24602461 2462 const ug = fGroup("website");2463 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";2464 if (loginSubdomain) {2465 const h = document.createElement("div"); h.className = "url-hint";2466 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);2467 }2468 body.appendChild(ug.g);24692470 2471 const xg = fGroup("username");2472 xg.inp.value = preUsername && !preUsername.includes("@") ? preUsername : "";2473 xg.inp.placeholder = "johndoe";2474 xg.inp.setAttribute("autocomplete", "off");2475 if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities, "username");2476 body.appendChild(xg.g);24772478 2479 const eg = fGroup("email");2480 (eg.inp as HTMLInputElement).type = "email";2481 eg.inp.value = preUsername && preUsername.includes("@") ? preUsername : "";2482 eg.inp.placeholder = "[email protected]";2483 eg.inp.setAttribute("autocomplete", "off");2484 if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities, "email");2485 body.appendChild(eg.g);24862487 2488 2489 2490 const pg = fGroup("password");2491 pg.inp.remove(); 2492 let currentPassword = prePassword || "";2493 const smartGen = createSmartGenerator({2494 policy: sitePolicy,2495 initialPassword: prePassword || undefined,2496 onChange: (pw) => { currentPassword = pw; },2497 });2498 pg.g.appendChild(smartGen.root);2499 body.appendChild(pg.g);25002501 2502 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";2503 saveBtn.textContent = "Save entry";2504 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";2505 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }2506 saveBtn.addEventListener("click", async () => {2507 const title = tg.inp.value.trim();2508 const pw = currentPassword.trim();2509 const username = xg.inp.value.trim();2510 const email = eg.inp.value.trim();2511 if (!title) {2512 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();2513 showFormError("Entry needs a title");2514 return;2515 }2516 if (!pw) {2517 showFormError("Entry needs a password — tap regen to make one");2518 return;2519 }2520 errEl.style.display = "none";2521 saveBtn.disabled = true; saveBtn.textContent = "Saving…";2522 try {2523 const res = await chrome.runtime.sendMessage({2524 type: "ADD_ENTRY",2525 payload: { title, url: ug.inp.value.trim(), username, email: email || undefined, password: pw, type: "login" },2526 });2527 if ((res as { ok?: boolean }).ok) {2528 2529 2530 options.onFillFields?.(username || email, pw);2531 showSuccess(title);2532 } else {2533 const err = (res as { error?: string })?.error;2534 const errMsg = err === "vault_locked"2535 ? "Vault locked — open Peach to save"2536 : err === "STORAGE_QUOTA_EXCEEDED"2537 ? "Out of storage — open Peach → Settings to free space"2538 : err2539 ? `Could not save: ${err}`2540 : "Could not save entry";2541 showFormError(errMsg);2542 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2543 }2544 } catch (e) {2545 showFormError(e instanceof Error && e.message ? `Could not reach Peach: ${e.message}` : "Could not reach Peach");2546 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2547 }2548 });2549 body.appendChild(saveBtn);2550 body.appendChild(errEl);25512552 wrap.append(head, body);2553 container.appendChild(wrap);2554 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });2555 }25562557 function showSuccess(title: string) {2558 currentMode = "success";2559 container.innerHTML = "";2560 const wrap = document.createElement("div"); wrap.className = "success-wrap";2561 const ring = document.createElement("div"); ring.className = "success-ring";2562 ring.innerHTML = checkSvgStr();2563 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;2564 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;2565 wrap.append(ring, t, s);2566 container.appendChild(wrap);2567 setTimeout(() => dismiss(), 1600);2568 }25692570 function flashActionBtn(btn: HTMLButtonElement) {2571 btn.classList.add("flashed");2572 setTimeout(() => btn.classList.remove("flashed"), 700);2573 }25742575 2576 2577 function flashCopyError(btn: HTMLButtonElement) {2578 const original = btn.textContent ?? "";2579 btn.textContent = "Blocked";2580 btn.classList.add("flashed");2581 setTimeout(() => {2582 btn.classList.remove("flashed");2583 if (btn.isConnected) btn.textContent = original;2584 }, 1200);2585 }25862587 function renderSilverPlatter(entry: SelectorEntry) {2588 container.innerHTML = "";2589 searchInput = null;2590 focusedIndex = -1;2591 currentMode = "silver-platter";2592 silverPlatterOrigin = entry;25932594 const domHdr = document.createElement("div");2595 domHdr.className = "domain-header";2596 const headText = document.createElement("div");2597 headText.className = "domain-header-text";2598 const eyebrow = document.createElement("div");2599 eyebrow.className = "domain-eyebrow";2600 eyebrow.textContent = "Saved for this site";2601 const domName = document.createElement("div");2602 domName.className = "domain-name";2603 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2604 headText.append(eyebrow, domName);2605 domHdr.appendChild(headText);2606 if (!options.isSignup) {2607 const actions = document.createElement("div");2608 actions.className = "domain-header-actions";26092610 2611 2612 2613 const searchBtn = document.createElement("button");2614 searchBtn.className = "domain-search-btn";2615 searchBtn.type = "button";2616 searchBtn.setAttribute("aria-label", "Search for a different entry");2617 searchBtn.title = "Search for a different entry";2618 searchBtn.innerHTML = searchSvgStr();2619 searchBtn.addEventListener("click", (e) => {2620 e.stopPropagation();2621 query = "";2622 renderList();2623 });2624 actions.appendChild(searchBtn);26252626 const addBtn = document.createElement("button");2627 addBtn.className = "domain-add-btn";2628 addBtn.type = "button";2629 addBtn.setAttribute("aria-label", "Create new entry");2630 addBtn.title = "Create new entry";2631 addBtn.textContent = "+";2632 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2633 actions.appendChild(addBtn);26342635 domHdr.appendChild(actions);2636 }2637 container.appendChild(domHdr);26382639 const platter = document.createElement("div");2640 platter.className = "silver-platter";26412642 const titleEl = document.createElement("div");2643 titleEl.className = "sp-title";2644 titleEl.textContent = entry.title;2645 platter.appendChild(titleEl);26462647 if (entry.email) {2648 const emailEl = document.createElement("div");2649 emailEl.className = "sp-identity-row";2650 const emailLbl = document.createElement("span");2651 emailLbl.className = "sp-identity-label";2652 emailLbl.textContent = "email";2653 const emailVal = document.createElement("span");2654 emailVal.className = "sp-username";2655 emailVal.textContent = entry.email;2656 emailEl.append(emailLbl, emailVal);2657 platter.appendChild(emailEl);2658 }26592660 if (entry.username) {2661 const userEl = document.createElement("div");2662 userEl.className = "sp-identity-row";2663 const userLbl = document.createElement("span");2664 userLbl.className = "sp-identity-label";2665 userLbl.textContent = entry.username.includes("@") ? "email" : "username";2666 const userVal = document.createElement("span");2667 userVal.className = "sp-username";2668 userVal.textContent = entry.username;2669 userEl.append(userLbl, userVal);2670 platter.appendChild(userEl);2671 }26722673 const fillBtn = document.createElement("button");2674 fillBtn.className = "sp-fill-btn";2675 fillBtn.type = "button";2676 fillBtn.textContent = "Fill credentials";2677 fillBtn.addEventListener("click", () => {2678 if (fillBtn.disabled) return;2679 fillBtn.disabled = true;2680 fillBtn.classList.add("filled");2681 fillBtn.innerHTML = checkSvgStr();2682 2683 2684 2685 2686 onSelect(entry.id);2687 setTimeout(() => dismiss(), 400);2688 });2689 platter.appendChild(fillBtn);26902691 const actionsGroup = document.createElement("div");2692 actionsGroup.className = "sp-actions-group";2693 const actionsLabel = document.createElement("div");2694 actionsLabel.className = "sp-actions-label";2695 actionsLabel.textContent = "Copy";2696 actionsGroup.appendChild(actionsLabel);26972698 const actions = document.createElement("div");2699 actions.className = "sp-actions";2700 actionsGroup.appendChild(actions);27012702 if (entry.email) {2703 const copyEmail = document.createElement("button");2704 copyEmail.className = "sp-action-btn";2705 copyEmail.type = "button";2706 copyEmail.title = "Copy email";2707 copyEmail.textContent = "Email";2708 copyEmail.addEventListener("click", async (e) => {2709 e.stopPropagation();2710 let alreadyWritten = false;2711 try {2712 await navigator.clipboard.writeText(entry.email!);2713 alreadyWritten = true;2714 } catch (err) {2715 console.warn("[Peach] clipboard write denied:", err);2716 }2717 const resp = await chrome.runtime.sendMessage({2718 type: "COPY",2719 payload: { text: entry.email, label: "Email", alreadyWritten },2720 }).catch(() => null) as { ok?: boolean } | null;2721 if (alreadyWritten || resp?.ok) flashActionBtn(copyEmail);2722 else flashCopyError(copyEmail);2723 });2724 actions.appendChild(copyEmail);2725 }27262727 const copyU = document.createElement("button");2728 copyU.className = "sp-action-btn";2729 copyU.type = "button";2730 const usernameIsEmail = entry.username?.includes("@") ?? false;2731 copyU.title = usernameIsEmail ? "Copy email" : "Copy username";2732 copyU.textContent = usernameIsEmail ? "Email" : "Username";2733 copyU.disabled = !entry.username;2734 copyU.addEventListener("click", async (e) => {2735 e.stopPropagation();2736 if (!entry.username) return;2737 let alreadyWritten = false;2738 try {2739 await navigator.clipboard.writeText(entry.username);2740 alreadyWritten = true;2741 } catch (err) {2742 console.warn("[Peach] clipboard write denied:", err);2743 }2744 const resp = await chrome.runtime.sendMessage({2745 type: "COPY",2746 payload: { text: entry.username, label: entry.username.includes("@") ? "Email" : "Username", alreadyWritten },2747 }).catch(() => null) as { ok?: boolean } | null;2748 if (alreadyWritten || resp?.ok) flashActionBtn(copyU);2749 else flashCopyError(copyU);2750 });2751 actions.appendChild(copyU);27522753 const copyP = document.createElement("button");2754 copyP.className = "sp-action-btn";2755 copyP.type = "button";2756 copyP.title = "Copy password";2757 copyP.textContent = "Password";2758 copyP.addEventListener("click", async (e) => {2759 e.stopPropagation();2760 const resp = await chrome.runtime.sendMessage({2761 type: "GET_ENTRY_FOR_FILL",2762 2763 payload: { id: entry.id, explicitSummon: true },2764 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;2765 if (!resp?.ok || !resp.data?.password) return;2766 let alreadyWritten = false;2767 try {2768 await navigator.clipboard.writeText(resp.data.password);2769 alreadyWritten = true;2770 } catch (err) {2771 console.warn("[Peach] clipboard write denied:", err);2772 }2773 const copyResp = await chrome.runtime.sendMessage({2774 type: "COPY",2775 payload: { text: resp.data.password, label: "Password", alreadyWritten },2776 }).catch(() => null) as { ok?: boolean } | null;2777 if (alreadyWritten || copyResp?.ok) flashActionBtn(copyP);2778 else flashCopyError(copyP);2779 });2780 actions.appendChild(copyP);27812782 let trayEl: HTMLDivElement | null = null;2783 let trayInterval: ReturnType<typeof setInterval> | null = null;27842785 function closeTray() {2786 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }2787 trayEl?.remove();2788 trayEl = null;2789 }2790 2791 2792 cleanupHooks.push(closeTray);27932794 async function openTOTPTray() {2795 2796 2797 2798 2799 async function fetchCode(): Promise<{ code: string; remaining: number } | null> {2800 const r = await chrome.runtime.sendMessage({2801 type: "GET_ENTRY_FOR_FILL",2802 2803 payload: { id: entry.id, explicitSummon: true },2804 }).catch(() => null) as { ok: boolean; data?: { totpCode?: string; totpRemaining?: number } } | null;2805 if (!r?.ok || !r.data?.totpCode) return null;2806 return { code: r.data.totpCode, remaining: r.data.totpRemaining ?? 30 };2807 }2808 let current = await fetchCode();2809 if (!current) return;28102811 trayEl = document.createElement("div");2812 trayEl.className = "sp-tray";28132814 const label = document.createElement("span");2815 label.className = "sp-tray-label";2816 label.textContent = "2FA";2817 trayEl.appendChild(label);28182819 const codeEl = document.createElement("div");2820 codeEl.className = "sp-totp-code";2821 trayEl.appendChild(codeEl);28222823 const ringNs = "http://www.w3.org/2000/svg";2824 const ring = document.createElementNS(ringNs, "svg");2825 ring.setAttribute("class", "sp-totp-ring");2826 ring.setAttribute("viewBox", "0 0 24 24");2827 const trackC = document.createElementNS(ringNs, "circle");2828 trackC.setAttribute("class", "ring-track");2829 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");2830 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");2831 const fillC = document.createElementNS(ringNs, "circle");2832 fillC.setAttribute("class", "ring-fill");2833 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");2834 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");2835 fillC.setAttribute("stroke-linecap", "round");2836 fillC.setAttribute("stroke-dasharray", "62.83");2837 ring.append(trackC, fillC);2838 trayEl.appendChild(ring);28392840 const copyBtn = document.createElement("button");2841 copyBtn.className = "sp-totp-copy";2842 copyBtn.type = "button";2843 copyBtn.textContent = "Copy";2844 trayEl.appendChild(copyBtn);28452846 function render() {2847 const { code, remaining } = current!;2848 2849 codeEl.textContent = code.length === 62850 ? `${code.slice(0, 3)} ${code.slice(3)}`2851 : code;2852 2853 2854 const fraction = (30 - remaining) / 30;2855 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));2856 }2857 render();2858 trayInterval = setInterval(async () => {2859 if (!current) return;2860 current = { code: current.code, remaining: current.remaining - 1 };2861 2862 if (current.remaining <= 0) {2863 const next = await fetchCode();2864 current = next ?? { code: current.code, remaining: 30 };2865 }2866 render();2867 }, 1000);28682869 copyBtn.addEventListener("click", async (e) => {2870 e.stopPropagation();2871 const code = current?.code;2872 if (!code) return;2873 let alreadyWritten = false;2874 try {2875 await navigator.clipboard.writeText(code);2876 alreadyWritten = true;2877 } catch (err) {2878 console.warn("[Peach] clipboard write denied:", err);2879 }2880 const resp = await chrome.runtime.sendMessage({2881 type: "COPY",2882 payload: { text: code, label: "TOTP", alreadyWritten },2883 }).catch(() => null) as { ok?: boolean } | null;2884 copyBtn.textContent = alreadyWritten || resp?.ok ? "Copied" : "Blocked";2885 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);2886 });28872888 platter.appendChild(trayEl);2889 }28902891 const editBtn = document.createElement("button");2892 editBtn.className = "sp-action-btn sp-action-edit";2893 editBtn.type = "button";2894 editBtn.title = "Edit entry";2895 editBtn.textContent = "Edit";2896 editBtn.addEventListener("click", (e) => {2897 e.stopPropagation();2898 closeTray();2899 void renderInlineEdit(entry);2900 });2901 actions.appendChild(editBtn);29022903 2904 2905 2906 if (entry.hasTotp) void openTOTPTray();29072908 platter.appendChild(actionsGroup);2909 container.appendChild(platter);2910 }29112912 291329142915291629172918 async function renderInlineEdit(summary: SelectorEntry) {2919 currentMode = "edit";2920 container.innerHTML = "";2921 const wrap = document.createElement("div");2922 wrap.className = "create-form-wrap";29232924 const head = document.createElement("div");2925 head.className = "create-head";2926 const back = document.createElement("button");2927 back.className = "back-btn";2928 back.type = "button";2929 back.title = "Cancel";2930 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>`;2931 back.addEventListener("click", () => renderSilverPlatter(summary));2932 head.appendChild(back);2933 const headLbl = document.createElement("div");2934 headLbl.className = "create-label";2935 headLbl.textContent = "Edit entry";2936 head.appendChild(headLbl);2937 wrap.appendChild(head);29382939 const body = document.createElement("div");2940 body.className = "create-body";29412942 2943 const loading = document.createElement("div");2944 loading.className = "empty-hint";2945 loading.textContent = "loading…";2946 body.appendChild(loading);2947 wrap.appendChild(body);2948 container.appendChild(wrap);29492950 const [res, idRes] = await Promise.all([2951 2952 2953 chrome.runtime.sendMessage({2954 type: "GET_ENTRY_FOR_EDIT",2955 payload: { id: summary.id },2956 }).catch(() => null),2957 chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null),2958 ]);2959 const entryRes = res as { ok: boolean; data?: { title?: string; username?: string; email?: string; password?: string; url?: string; totp?: string; notes?: string; passwordHistory?: PasswordHistoryItem[] } } | null;2960 const editIdentities: SavedIdentity[] = (idRes as { ok?: boolean; data?: SavedIdentity[] } | null)?.data ?? [];29612962 if (!entryRes || !entryRes.ok || !entryRes.data) {2963 loading.textContent = "couldn't load this entry. try again from the popup.";2964 return;2965 }2966 body.innerHTML = "";29672968 function field(label: string, value: string, type: "text" | "password" = "text"): { g: HTMLDivElement; inp: HTMLInputElement } {2969 const group = document.createElement("div");2970 group.className = "f-group";2971 const lbl = document.createElement("div");2972 lbl.className = "f-label";2973 lbl.textContent = label;2974 const input = document.createElement("input");2975 input.className = "f-input";2976 input.type = type;2977 input.value = value;2978 input.autocomplete = "off";2979 group.append(lbl, input);2980 body.appendChild(group);2981 return { g: group, inp: input };2982 }29832984 const titleInput = field("title", entryRes.data.title ?? "").inp;2985 const urlInput = field("url", entryRes.data.url ?? "").inp;2986 urlInput.classList.add("url-f");2987 const usernameLabel = (entryRes.data.username ?? "").includes("@") ? "email" : "username";2988 const { g: usernameGroup, inp: usernameInput } = field(usernameLabel, entryRes.data.username ?? "");2989 if (editIdentities.length) attachIdentitySuggestions(usernameGroup, usernameInput, editIdentities, "username");2990 const { g: emailGroup, inp: emailInput } = field("email", entryRes.data.email ?? "");2991 if (editIdentities.length) attachIdentitySuggestions(emailGroup, emailInput, editIdentities, "email");29922993 2994 const pwGroup = document.createElement("div");2995 pwGroup.className = "f-group";2996 const pwLbl = document.createElement("div");2997 pwLbl.className = "f-label";2998 pwLbl.textContent = "password";2999 const pwRow = document.createElement("div");3000 pwRow.className = "f-row";3001 const pwInput = document.createElement("input");3002 pwInput.className = "f-input";3003 pwInput.type = "password";3004 pwInput.value = entryRes.data.password ?? "";3005 pwInput.autocomplete = "new-password";3006 const revealBtn = document.createElement("button");3007 revealBtn.className = "gen-btn";3008 revealBtn.type = "button";3009 revealBtn.textContent = "show";3010 revealBtn.addEventListener("click", () => {3011 const reveal = pwInput.type === "password";3012 pwInput.type = reveal ? "text" : "password";3013 revealBtn.textContent = reveal ? "hide" : "show";3014 });3015 pwRow.append(pwInput, revealBtn);3016 const editSmartGen = createSmartGenerator({ policy: detectPasswordPolicy(), onChange: (pw) => { pwInput.value = pw; } });3017 editSmartGen.root.style.display = "none";3018 let editGenVisible = false;3019 const genToggleBtn = document.createElement("button");3020 genToggleBtn.className = "gen-toggle-link";3021 genToggleBtn.type = "button";3022 genToggleBtn.textContent = "⚙ generate";3023 genToggleBtn.addEventListener("click", () => {3024 editGenVisible = !editGenVisible;3025 pwRow.style.display = editGenVisible ? "none" : "";3026 editSmartGen.root.style.display = editGenVisible ? "" : "none";3027 genToggleBtn.textContent = editGenVisible ? "☰ hide generator" : "⚙ generate";3028 });3029 pwGroup.append(pwLbl, pwRow, genToggleBtn, editSmartGen.root);30303031 3032 3033 3034 3035 3036 const history = entryRes.data.passwordHistory ?? [];3037 {3038 const histList = document.createElement("div");3039 histList.className = "pw-history";3040 histList.style.display = "none";3041 let histVisible = false;30423043 const restLabel = history.length ? `↺ history (${history.length})` : "↺ history";3044 const histToggle = document.createElement("button");3045 histToggle.className = "gen-toggle-link";3046 histToggle.type = "button";3047 histToggle.textContent = restLabel;3048 histToggle.addEventListener("click", () => {3049 histVisible = !histVisible;3050 histList.style.display = histVisible ? "" : "none";3051 histToggle.textContent = histVisible ? "↺ hide history" : restLabel;3052 });30533054 if (history.length === 0) {3055 const empty = document.createElement("div");3056 empty.className = "pw-history-empty";3057 empty.textContent = "No earlier passwords yet. When you change this password, the old one is saved here so you can copy or restore it.";3058 histList.appendChild(empty);3059 }30603061 for (const item of history) {3062 const row = document.createElement("div");3063 row.className = "pw-history-row";3064 const main = document.createElement("div");3065 main.className = "pw-history-main";3066 const pwText = document.createElement("div");3067 pwText.className = "pw-history-pw";3068 let revealed = false;3069 const mask = "••••••••••••";3070 pwText.textContent = mask;3071 const age = document.createElement("div");3072 age.className = "pw-history-age";3073 age.textContent = timeSince(item.changedAt);3074 main.append(pwText, age);30753076 const acts = document.createElement("div");3077 acts.className = "pw-history-actions";3078 const revealBtn2 = document.createElement("button");3079 revealBtn2.className = "pw-history-btn";3080 revealBtn2.type = "button";3081 revealBtn2.textContent = "show";3082 revealBtn2.addEventListener("click", () => {3083 revealed = !revealed;3084 pwText.textContent = revealed ? item.password : mask;3085 revealBtn2.textContent = revealed ? "hide" : "show";3086 });3087 const copyBtn2 = document.createElement("button");3088 copyBtn2.className = "pw-history-btn";3089 copyBtn2.type = "button";3090 copyBtn2.textContent = "copy";3091 copyBtn2.addEventListener("click", async () => {3092 let alreadyWritten = false;3093 try {3094 await navigator.clipboard.writeText(item.password);3095 alreadyWritten = true;3096 } catch (err) {3097 console.warn("[Peach] clipboard write denied:", err);3098 }3099 await chrome.runtime.sendMessage({3100 type: "COPY",3101 payload: { text: item.password, label: "Password", alreadyWritten },3102 }).catch(() => null);3103 copyBtn2.textContent = "copied";3104 setTimeout(() => { if (copyBtn2.isConnected) copyBtn2.textContent = "copy"; }, 1200);3105 });3106 const useBtn = document.createElement("button");3107 useBtn.className = "pw-history-btn";3108 useBtn.type = "button";3109 useBtn.textContent = "use";3110 useBtn.title = "Use this password again";3111 useBtn.addEventListener("click", () => {3112 pwInput.value = item.password;3113 pwInput.type = "text";3114 revealBtn.textContent = "hide";3115 if (editGenVisible) {3116 editGenVisible = false;3117 pwRow.style.display = "";3118 editSmartGen.root.style.display = "none";3119 genToggleBtn.textContent = "⚙ generate";3120 }3121 histVisible = false;3122 histList.style.display = "none";3123 histToggle.textContent = `↺ history (${history.length})`;3124 });3125 acts.append(revealBtn2, copyBtn2, useBtn);3126 row.append(main, acts);3127 histList.appendChild(row);3128 }31293130 pwGroup.append(histToggle, histList);3131 }31323133 body.appendChild(pwGroup);31343135 const totpInput = field("2fa secret", entryRes.data.totp ?? "").inp;31363137 3138 const notesGroup = document.createElement("div");3139 notesGroup.className = "f-group";3140 const notesLbl = document.createElement("div");3141 notesLbl.className = "f-label";3142 notesLbl.textContent = "notes";3143 const notesInput = document.createElement("textarea");3144 notesInput.className = "f-input";3145 notesInput.rows = 3;3146 notesInput.value = entryRes.data.notes ?? "";3147 notesInput.style.fontFamily = "Inter, sans-serif";3148 notesInput.style.resize = "vertical";3149 notesGroup.append(notesLbl, notesInput);3150 body.appendChild(notesGroup);31513152 3153 const actionsRow = document.createElement("div");3154 actionsRow.className = "f-row";3155 const saveBtn = document.createElement("button");3156 saveBtn.className = "save-btn";3157 saveBtn.type = "button";3158 saveBtn.textContent = "Save changes";3159 saveBtn.style.flex = "1";3160 const cancelBtn = document.createElement("button");3161 cancelBtn.className = "gen-btn";3162 cancelBtn.type = "button";3163 cancelBtn.textContent = "cancel";3164 cancelBtn.addEventListener("click", () => renderSilverPlatter(summary));3165 actionsRow.append(saveBtn, cancelBtn);3166 body.appendChild(actionsRow);31673168 saveBtn.addEventListener("click", async () => {3169 saveBtn.disabled = true;3170 const originalLabel = saveBtn.textContent;3171 saveBtn.textContent = "Saving…";3172 const patch: Record<string, unknown> = {3173 title: titleInput.value.trim(),3174 url: urlInput.value.trim(),3175 username: usernameInput.value.trim(),3176 email: emailInput.value.trim() || undefined,3177 password: pwInput.value,3178 totp: totpInput.value.trim(),3179 notes: notesInput.value,3180 };3181 const updateRes = (await chrome.runtime.sendMessage({3182 type: "UPDATE_ENTRY",3183 payload: { id: summary.id, patch },3184 }).catch(() => null)) as { ok: boolean; error?: string } | null;3185 if (!updateRes?.ok) {3186 saveBtn.disabled = false;3187 saveBtn.textContent = originalLabel;3188 const err = document.createElement("div");3189 err.className = "empty-hint";3190 err.style.color = "color-mix(in srgb, #e55 70%, transparent)";3191 err.textContent = updateRes?.error ?? "Couldn't save. Try the popup.";3192 body.appendChild(err);3193 setTimeout(() => err.remove(), 3000);3194 return;3195 }3196 3197 const refreshed: SelectorEntry = {3198 id: summary.id,3199 title: patch.title as string,3200 username: patch.username as string,3201 email: patch.email as string | undefined,3202 url: patch.url as string,3203 hasTotp: Boolean(patch.totp),3204 hasPasskey: summary.hasPasskey,3205 };3206 renderSilverPlatter(refreshed);3207 });32083209 queueMicrotask(() => titleInput.focus());3210 }32113212 function renderLocked() {3213 container.innerHTML = "";32143215 3216 const domHdr = document.createElement("div");3217 domHdr.className = "domain-header";3218 const eyebrow = document.createElement("div");3219 eyebrow.className = "domain-eyebrow";3220 eyebrow.textContent = "vault locked";3221 const domName = document.createElement("div");3222 domName.className = "domain-name";3223 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");3224 domHdr.append(eyebrow, domName);3225 container.appendChild(domHdr);32263227 const wrap = document.createElement("div");3228 wrap.className = "locked-wrap";32293230 3231 const head = document.createElement("div"); head.className = "locked-head";3232 const icon = document.createElement("div"); icon.className = "locked-icon";3233 icon.innerHTML = keySvgStr();3234 const title = document.createElement("div"); title.className = "locked-title";3235 title.textContent = "unlock to autofill";3236 head.append(icon, title);32373238 3239 3240 3241 3242 3243 3244 3245 3246 const sub = document.createElement("div");3247 sub.className = "locked-sub";3248 sub.textContent = "Unlock in Peach — your master password is never typed on this page.";32493250 const openBtn = document.createElement("button");3251 openBtn.className = "locked-btn";3252 openBtn.type = "button";3253 openBtn.textContent = "Open Peach to unlock";3254 openBtn.addEventListener("click", () => {3255 chrome.runtime.sendMessage({ type: "OPEN_POPUP" }).catch(() => {});3256 openBtn.textContent = "opening Peach…";3257 openBtn.disabled = true;3258 3259 3260 setTimeout(() => {3261 if (openBtn.isConnected) {3262 openBtn.textContent = "Open Peach to unlock";3263 openBtn.disabled = false;3264 }3265 }, 2500);3266 });32673268 wrap.append(head, sub, openBtn);3269 container.appendChild(wrap);3270 }32713272 function handleKeyDown(e: KeyboardEvent) {3273 3274 3275 3276 if (!e.isTrusted) return;3277 if (options.locked) {3278 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3279 return;3280 }3281 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter" || currentMode === "identity-helper" || currentMode === "edit") {3282 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3283 return;3284 }3285 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");3286 const count = allEntryEls.length;3287 const activeElement = shadow.activeElement ?? document.activeElement;3288 if (activeElement === searchInput) {3289 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }3290 if (count === 0) return;3291 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;3292 }3293 if (count === 0) return;3294 switch (e.key) {3295 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;3296 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;3297 case "Enter": {3298 if (focusedIndex >= 0) {3299 const el = allEntryEls[focusedIndex] as HTMLElement;3300 const id = el.dataset.id;3301 if (id) { onSelect(id); dismiss(); }3302 else if (el.classList.contains("is-new") || !id) {3303 3304 showCreateForm();3305 }3306 }3307 break;3308 }3309 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;3310 }3311 }33123313 document.addEventListener("keydown", handleKeyDown);33143315 3316 3317 3318 3319 3320 const silverPlatterTypeHandler = (e: KeyboardEvent) => {3321 if (!e.isTrusted) return; 3322 if (currentMode !== "silver-platter") return;3323 if (e.ctrlKey || e.metaKey || e.altKey) return;3324 if (e.key.length !== 1) return;3325 e.preventDefault();3326 e.stopImmediatePropagation();3327 query = e.key;3328 3329 void ensureSearchableLoaded(renderList);3330 renderList();3331 };3332 document.addEventListener("keydown", silverPlatterTypeHandler, true);3333 cleanupHooks.push(() => document.removeEventListener("keydown", silverPlatterTypeHandler, true));33343335 const identityHelperTypeHandler = (e: KeyboardEvent) => {3336 if (!e.isTrusted) return; 3337 if (currentMode !== "identity-helper") return;3338 if (e.ctrlKey || e.metaKey || e.altKey) return;3339 const active = (shadow.activeElement ?? document.activeElement) as Element | null;3340 if (active === searchInput) return;3341 if (3342 active instanceof HTMLInputElement ||3343 active instanceof HTMLTextAreaElement ||3344 (active instanceof HTMLElement && active.isContentEditable)3345 ) {3346 return;3347 }3348 if (e.key.length !== 1) return;3349 e.preventDefault();3350 e.stopImmediatePropagation();3351 query = e.key;3352 renderIdentityHelper();3353 };3354 document.addEventListener("keydown", identityHelperTypeHandler, true);3355 cleanupHooks.push(() => document.removeEventListener("keydown", identityHelperTypeHandler, true));33563357 (modalAncestor ?? document.body).appendChild(host);33583359 3360 3361 3362 3363 3364 3365 3366 function establishesFixedContainingBlock(s: CSSStyleDeclaration): boolean {3367 const has = (p: string) => { const v = s.getPropertyValue(p); return v !== "" && v !== "none"; };3368 return (3369 has("transform") || has("perspective") || has("translate") ||3370 has("rotate") || has("scale") || has("filter") ||3371 has("backdrop-filter") || has("-webkit-backdrop-filter") ||3372 /\b(transform|perspective|filter)\b/.test(s.getPropertyValue("will-change")) ||3373 /\b(paint|layout|strict|content)\b/.test(s.getPropertyValue("contain"))3374 );3375 }3376 function containingBlockOffset(): { dx: number; dy: number } {3377 let el: HTMLElement | null = host.parentElement;3378 while (el && el !== document.documentElement) {3379 const s = getComputedStyle(el);3380 if (establishesFixedContainingBlock(s)) {3381 const r = el.getBoundingClientRect();3382 return {3383 dx: r.left + (parseFloat(s.borderLeftWidth) || 0),3384 dy: r.top + (parseFloat(s.borderTopWidth) || 0),3385 };3386 }3387 el = el.parentElement;3388 }3389 return { dx: 0, dy: 0 };3390 }33913392 function updatePosition() {3393 const r = anchorEl.getBoundingClientRect();3394 const { dx, dy } = containingBlockOffset();3395 host.style.top = `${r.bottom - dy + 5}px`;3396 host.style.left = `${r.left - dx}px`;3397 host.style.width = `${popupWidth}px`;3398 }3399 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);3400 updatePosition();34013402 3403 3404 3405 if (modalAncestor) {3406 modalAncestor.addEventListener("scroll", updatePosition, { passive: true });3407 cleanupHooks.push(() => modalAncestor.removeEventListener("scroll", updatePosition));3408 }34093410 3411 3412 3413 const removalObserver = new MutationObserver(() => {3414 if (!document.contains(anchorEl)) dismiss();3415 });3416 removalObserver.observe(document.body, { childList: true, subtree: true });3417 cleanupHooks.push(() => removalObserver.disconnect());34183419 if (options.locked) {3420 renderLocked();3421 } else if (options.isSignup && options.savedIdentities) {3422 3423 3424 renderIdentityHelper();3425 } else if (entries.length === 1 && !options.isSignup) {3426 3427 3428 renderSilverPlatter(entries[0]!);3429 } else {3430 renderList();3431 }34323433 let exitScheduled = false;3434 function dismiss(userAction = false) {3435 3436 3437 3438 if (exitScheduled) return;3439 exitScheduled = true;3440 3441 3442 3443 stopObserving();3444 document.removeEventListener("click", outsideClick, { capture: true });3445 document.removeEventListener("keydown", handleKeyDown);3446 for (const cleanup of cleanupHooks) {3447 try { cleanup(); } catch { }3448 }3449 if (userAction) options.onDismiss?.();3450 container.classList.add("is-exiting");3451 setTimeout(() => host.remove(), 130);3452 }34533454 function outsideClick(ev: MouseEvent) {3455 const path = ev.composedPath();3456 3457 3458 3459 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3460 }3461 3462 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);34633464 return dismiss;3465}34663467export function showTOTPSelector(3468 anchorEl: HTMLInputElement,3469 entries: SelectorEntry[],3470 onFill: (id: string) => void,3471 options: { domain?: string; onDismiss?: () => void } = {}3472): () => void {3473 const host = document.createElement("div");3474 host.style.cssText = "position: absolute; z-index: 2147483647;";3475 host.style.setProperty("--peach-accent", accentHex);3476 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));3477 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));3478 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));3479 const shadow = host.attachShadow({ mode: "closed" });34803481 const style = shadow.appendChild(document.createElement("style"));3482 style.textContent = SELECTOR_STYLES;34833484 const rect = anchorEl.getBoundingClientRect();3485 host.style.top = `${rect.bottom + window.scrollY + 5}px`;3486 host.style.left = `${rect.left + window.scrollX}px`;3487 const popupWidth = Math.min(Math.max(rect.width, 300), 360);3488 host.style.width = `${popupWidth}px`;34893490 const container = shadow.appendChild(document.createElement("div"));3491 container.className = "peach-inline-popup";3492 container.setAttribute("data-theme", popupTheme);34933494 const domain = options.domain ?? location.hostname.replace(/^www\./, "");3495 const current = entries.filter((entry) => entry.hasTotp);3496 let focusedIndex = current.length === 1 ? 0 : -1;34973498 function updatePosition() {3499 const r = anchorEl.getBoundingClientRect();3500 host.style.top = `${r.bottom + window.scrollY + 5}px`;3501 host.style.left = `${r.left + window.scrollX}px`;3502 host.style.width = `${popupWidth}px`;3503 }35043505 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);35063507 function choose(entry: SelectorEntry) {3508 onFill(entry.id);3509 dismiss();3510 }35113512 function render() {3513 container.innerHTML = "";35143515 const domHdr = document.createElement("div");3516 domHdr.className = "domain-header";3517 const headText = document.createElement("div");3518 headText.className = "domain-header-text";3519 const eyebrow = document.createElement("div");3520 eyebrow.className = "domain-eyebrow";3521 eyebrow.textContent = "2FA code";3522 const domName = document.createElement("div");3523 domName.className = "domain-name";3524 domName.textContent = domain;3525 headText.append(eyebrow, domName);3526 domHdr.appendChild(headText);3527 container.appendChild(domHdr);35283529 if (current.length === 1) {3530 const entry = current[0]!;3531 const platter = document.createElement("div");3532 platter.className = "silver-platter";35333534 const title = document.createElement("div");3535 title.className = "sp-title";3536 title.textContent = entry.title;3537 platter.appendChild(title);35383539 const sub = document.createElement("div");3540 sub.className = "sp-username";3541 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3542 platter.appendChild(sub);35433544 const fill = document.createElement("button");3545 fill.className = "sp-fill-btn";3546 fill.type = "button";3547 fill.textContent = "Fill 2FA code";3548 fill.addEventListener("click", () => choose(entry));3549 platter.appendChild(fill);35503551 container.appendChild(platter);3552 requestAnimationFrame(() => fill.focus({ preventScroll: true }));3553 return;3554 }35553556 const results = document.createElement("div");3557 results.className = "results";3558 container.appendChild(results);35593560 for (let i = 0; i < current.length; i++) {3561 const entry = current[i]!;3562 const row = document.createElement("div");3563 row.className = `entry${i === focusedIndex ? " focused" : ""}`;3564 row.dataset.index = String(i);3565 row.setAttribute("tabindex", "-1");35663567 const icon = document.createElement("div");3568 icon.className = "entry-icon";3569 icon.innerHTML = keySvgStr();3570 row.appendChild(icon);3571 const info = document.createElement("div");3572 info.className = "entry-info";3573 const title = document.createElement("div");3574 title.className = "entry-title";3575 title.textContent = entry.title;3576 const sub = document.createElement("div");3577 sub.className = "entry-sub";3578 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3579 info.append(title, sub);3580 row.appendChild(info);35813582 const badge = document.createElement("span");3583 badge.className = "totp-badge";3584 badge.textContent = "Fill";3585 row.appendChild(badge);3586 row.addEventListener("click", () => choose(entry));3587 results.appendChild(row);3588 }3589 }35903591 function setFocused(index: number) {3592 focusedIndex = index;3593 container.querySelectorAll(".entry").forEach((el, i) => {3594 el.classList.toggle("focused", i === focusedIndex);3595 if (i === focusedIndex) (el as HTMLElement).scrollIntoView({ block: "nearest" });3596 });3597 }35983599 function handleKeyDown(e: KeyboardEvent) {3600 if (e.key === "Escape" || e.key === "Tab") {3601 e.preventDefault();3602 dismiss(true);3603 return;3604 }3605 if (current.length === 0) return;3606 if (e.key === "Enter") {3607 e.preventDefault();3608 choose(current[Math.max(0, focusedIndex)]!);3609 return;3610 }3611 if (current.length === 1) return;3612 if (e.key === "ArrowDown") {3613 e.preventDefault();3614 setFocused(focusedIndex < current.length - 1 ? focusedIndex + 1 : 0);3615 } else if (e.key === "ArrowUp") {3616 e.preventDefault();3617 setFocused(focusedIndex > 0 ? focusedIndex - 1 : current.length - 1);3618 }3619 }36203621 let exitScheduled = false;3622 function dismiss(userAction = false) {3623 if (exitScheduled) return;3624 exitScheduled = true;3625 stopObserving();3626 document.removeEventListener("click", outsideClick, { capture: true });3627 document.removeEventListener("keydown", handleKeyDown);3628 if (userAction) options.onDismiss?.();3629 container.classList.add("is-exiting");3630 setTimeout(() => host.remove(), 130);3631 }36323633 function outsideClick(ev: MouseEvent) {3634 const path = ev.composedPath();3635 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3636 }36373638 document.addEventListener("keydown", handleKeyDown);3639 document.body.appendChild(host);3640 render();3641 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);36423643 return dismiss;3644}36453646export interface CardSelectorEntry {3647 id: string;3648 title: string;3649 cardholderName?: string;3650 last4?: string;3651 expMonth?: string;3652 expYear?: string;3653 network?: import("../shared/types").CardNetwork;3654}36553656interface CardSelectorOptions {3657 domain?: string;3658 3659 onFill: (entry: import("../shared/types").VaultEntry) => void;3660 3661 onDismiss?: () => void;3662}36633664366536663667366836693670export function showPasskeyProGate(): void {3671 const domain = location.hostname.replace(/^www\./, "");36723673 const host = document.createElement("div");3674 host.setAttribute("data-peach-passkey-gate", "1");3675 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";3676 const shadow = host.attachShadow({ mode: "closed" });36773678 const style = document.createElement("style");3679 style.textContent = `3680 :host { all: initial; }3681 .gate-wrap {3682 position: fixed;3683 top: 50%;3684 left: 50%;3685 transform: translate(-50%, -50%);3686 width: 280px;3687 pointer-events: auto;3688 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;3689 }3690 @keyframes g-in {3691 from { opacity:0; transform: translate(-50%,-52%); }3692 to { opacity:1; transform: translate(-50%,-50%); }3693 }3694 .gate-wrap.is-exiting {3695 animation: g-out 130ms ease-in forwards;3696 }3697 @keyframes g-out {3698 to { opacity:0; transform: translate(-50%,-52%); }3699 }3700 .gate {3701 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3702 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3703 border-radius: 12px;3704 box-shadow: 0 12px 48px rgba(0,0,0,0.75);3705 padding: 16px;3706 font-family: Inter, sans-serif;3707 }3708 .gate-header {3709 display: flex;3710 align-items: center;3711 gap: 7px;3712 margin-bottom: 12px;3713 padding-bottom: 10px;3714 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3715 }3716 .gate-domain {3717 font-size: 12px;3718 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3719 font-family: "IBM Plex Mono", monospace;3720 overflow: hidden;3721 text-overflow: ellipsis;3722 white-space: nowrap;3723 }3724 .gate-title {3725 font-size: 14px;3726 font-weight: 500;3727 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3728 margin-bottom: 6px;3729 }3730 .gate-subtitle {3731 font-size: 12px;3732 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};3733 line-height: 1.5;3734 margin-bottom: 14px;3735 }3736 .gate-btn {3737 display: block;3738 width: 100%;3739 padding: 9px 0;3740 border-radius: 8px;3741 border: none;3742 cursor: pointer;3743 font-size: 13px;3744 font-family: Inter, sans-serif;3745 font-weight: 500;3746 transition: opacity 100ms;3747 margin-bottom: 7px;3748 text-align: center;3749 }3750 .gate-btn:last-child { margin-bottom: 0; }3751 .gate-btn--accent {3752 background: ${accentHex};3753 color: #fff;3754 }3755 .gate-btn--accent:hover { opacity: 0.88; }3756 .gate-btn--ghost {3757 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3758 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};3759 }3760 .gate-btn--ghost:hover { opacity: 0.75; }3761 .backdrop {3762 position: fixed;3763 inset: 0;3764 pointer-events: auto;3765 }3766 `;3767 shadow.appendChild(style);37683769 const backdrop = document.createElement("div");3770 backdrop.className = "backdrop";3771 shadow.appendChild(backdrop);37723773 const wrap = document.createElement("div");3774 wrap.className = "gate-wrap";3775 shadow.appendChild(wrap);37763777 const gate = document.createElement("div");3778 gate.className = "gate";3779 wrap.appendChild(gate);37803781 3782 const header = document.createElement("div");3783 header.className = "gate-header";3784 const icon = document.createElement("span");3785 icon.innerHTML = peachIconSvg();3786 const domainEl = document.createElement("span");3787 domainEl.className = "gate-domain";3788 domainEl.textContent = domain;3789 header.append(icon, domainEl);3790 gate.appendChild(header);37913792 const title = document.createElement("div");3793 title.className = "gate-title";3794 title.textContent = "Passkeys are part of Peach Pro.";3795 gate.appendChild(title);37963797 const subtitle = document.createElement("div");3798 subtitle.className = "gate-subtitle";3799 subtitle.textContent = "Store and use passkeys across all your devices, securely.";3800 gate.appendChild(subtitle);38013802 const upgradeBtn = document.createElement("button");3803 upgradeBtn.className = "gate-btn gate-btn--accent";3804 upgradeBtn.type = "button";3805 upgradeBtn.textContent = "Upgrade to Pro";3806 gate.appendChild(upgradeBtn);38073808 const laterBtn = document.createElement("button");3809 laterBtn.className = "gate-btn gate-btn--ghost";3810 laterBtn.type = "button";3811 laterBtn.textContent = "Not right now";3812 gate.appendChild(laterBtn);38133814 function dismiss() {3815 wrap.classList.add("is-exiting");3816 setTimeout(() => host.remove(), 130);3817 }38183819 upgradeBtn.addEventListener("click", () => {3820 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});3821 dismiss();3822 });3823 laterBtn.addEventListener("click", dismiss);3824 backdrop.addEventListener("click", dismiss);38253826 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {3827 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }3828 });38293830 document.body.appendChild(host);3831}383238333834383538363837383838393840export function showPasskeyRegistrationOffer(args: {3841 rpId: string;3842 rpName?: string;3843 userName?: string;3844 userDisplayName?: string;3845}): void {3846 const domain = location.hostname.replace(/^www\./, "");3847 const displayName = args.rpName || domain;38483849 const host = document.createElement("div");3850 host.setAttribute("data-peach-passkey-offer", "1");3851 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";3852 const shadow = host.attachShadow({ mode: "closed" });38533854 const style = document.createElement("style");3855 style.textContent = `3856 :host { all: initial; }3857 .offer-wrap {3858 position: fixed;3859 top: 16px;3860 right: 16px;3861 width: 320px;3862 pointer-events: auto;3863 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;3864 }3865 @keyframes o-in {3866 from { opacity: 0; transform: translateY(-6px); }3867 to { opacity: 1; transform: translateY(0); }3868 }3869 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }3870 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }3871 .offer {3872 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3873 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3874 border-radius: 12px;3875 box-shadow: 0 12px 48px rgba(0,0,0,0.55);3876 padding: 14px 14px 12px;3877 font-family: Inter, sans-serif;3878 position: relative;3879 }3880 .offer-close {3881 position: absolute;3882 top: 8px;3883 right: 8px;3884 width: 18px;3885 height: 18px;3886 border: none;3887 background: transparent;3888 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};3889 cursor: pointer;3890 font-size: 14px;3891 line-height: 1;3892 padding: 0;3893 }3894 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }3895 .offer-header {3896 display: flex;3897 align-items: center;3898 gap: 7px;3899 margin-bottom: 8px;3900 padding-bottom: 8px;3901 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3902 }3903 .offer-domain {3904 font-size: 12px;3905 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3906 font-family: "IBM Plex Mono", monospace;3907 overflow: hidden;3908 text-overflow: ellipsis;3909 white-space: nowrap;3910 }3911 .offer-title {3912 font-size: 13px;3913 font-weight: 500;3914 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3915 margin-bottom: 4px;3916 }3917 .offer-subtitle {3918 font-size: 11px;3919 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};3920 line-height: 1.45;3921 margin-bottom: 12px;3922 }3923 .offer-btn {3924 display: block;3925 width: 100%;3926 padding: 8px 0;3927 border-radius: 7px;3928 border: none;3929 cursor: pointer;3930 font-size: 12px;3931 font-family: Inter, sans-serif;3932 font-weight: 500;3933 transition: opacity 100ms;3934 margin-bottom: 6px;3935 text-align: center;3936 }3937 .offer-btn:last-child { margin-bottom: 0; }3938 .offer-btn--accent { background: ${accentHex}; color: #fff; }3939 .offer-btn--accent:hover { opacity: 0.88; }3940 .offer-btn--ghost {3941 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3942 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};3943 }3944 .offer-btn--ghost:hover { opacity: 0.75; }3945 `;3946 shadow.appendChild(style);39473948 const wrap = document.createElement("div");3949 wrap.className = "offer-wrap";3950 shadow.appendChild(wrap);39513952 const offer = document.createElement("div");3953 offer.className = "offer";3954 wrap.appendChild(offer);39553956 const closeBtn = document.createElement("button");3957 closeBtn.className = "offer-close";3958 closeBtn.type = "button";3959 closeBtn.setAttribute("aria-label", "Dismiss");3960 closeBtn.textContent = "✕";3961 offer.appendChild(closeBtn);39623963 const header = document.createElement("div");3964 header.className = "offer-header";3965 const icon = document.createElement("span");3966 icon.innerHTML = peachIconSvg();3967 const domainEl = document.createElement("span");3968 domainEl.className = "offer-domain";3969 domainEl.textContent = domain;3970 header.append(icon, domainEl);3971 offer.appendChild(header);39723973 const title = document.createElement("div");3974 title.className = "offer-title";3975 title.textContent = "This site supports passkeys.";3976 offer.appendChild(title);39773978 const subtitle = document.createElement("div");3979 subtitle.className = "offer-subtitle";3980 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";3981 offer.appendChild(subtitle);39823983 const saveBtn = document.createElement("button");3984 saveBtn.className = "offer-btn offer-btn--accent";3985 saveBtn.type = "button";3986 saveBtn.textContent = "Save passkey to Peach";3987 offer.appendChild(saveBtn);39883989 const laterBtn = document.createElement("button");3990 laterBtn.className = "offer-btn offer-btn--ghost";3991 laterBtn.type = "button";3992 laterBtn.textContent = "Not now";3993 offer.appendChild(laterBtn);39943995 function dismiss(decline: boolean) {3996 if (decline) {3997 chrome.runtime.sendMessage({3998 type: "PASSKEY_OFFER_DECLINE",3999 payload: { domain },4000 }).catch(() => {});4001 }4002 wrap.classList.add("is-exiting");4003 setTimeout(() => host.remove(), 130);4004 }40054006 saveBtn.addEventListener("click", () => {4007 chrome.runtime.sendMessage({4008 type: "INITIATE_FIREFOX_PASSKEY_CREATE",4009 payload: {4010 rpId: args.rpId,4011 rpName: args.rpName ?? displayName,4012 userName: args.userName ?? "user",4013 userDisplayName: args.userDisplayName,4014 },4015 }).catch(() => {});4016 dismiss(false);4017 });4018 laterBtn.addEventListener("click", () => dismiss(true));4019 closeBtn.addEventListener("click", () => dismiss(true));40204021 document.body.appendChild(host);4022}4023402440254026402740284029403040314032export function showPasskeyAvailable(args: {4033 rpId: string;4034 userName: string;4035 fromBrowser?: "chrome" | "firefox";4036 currentBrowser?: "chrome" | "firefox" | "unknown";4037}): void {4038 const domain = location.hostname.replace(/^www\./, "");4039 const crossBrowser =4040 args.fromBrowser &&4041 args.currentBrowser &&4042 args.currentBrowser !== "unknown" &&4043 args.fromBrowser !== args.currentBrowser;40444045 const host = document.createElement("div");4046 host.setAttribute("data-peach-passkey-available", "1");4047 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";4048 const shadow = host.attachShadow({ mode: "closed" });40494050 const style = document.createElement("style");4051 style.textContent = `4052 :host { all: initial; }4053 .avail-wrap {4054 position: fixed;4055 top: 16px;4056 right: 16px;4057 width: 320px;4058 pointer-events: auto;4059 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;4060 }4061 @keyframes a-in {4062 from { opacity: 0; transform: translateY(-6px); }4063 to { opacity: 1; transform: translateY(0); }4064 }4065 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }4066 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }4067 .avail {4068 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};4069 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};4070 border-radius: 12px;4071 box-shadow: 0 12px 48px rgba(0,0,0,0.55);4072 padding: 14px 14px 12px;4073 font-family: Inter, sans-serif;4074 position: relative;4075 }4076 .avail-close {4077 position: absolute;4078 top: 8px;4079 right: 8px;4080 width: 18px;4081 height: 18px;4082 border: none;4083 background: transparent;4084 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};4085 cursor: pointer;4086 font-size: 14px;4087 line-height: 1;4088 padding: 0;4089 }4090 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }4091 .avail-header {4092 display: flex;4093 align-items: center;4094 gap: 7px;4095 margin-bottom: 8px;4096 padding-bottom: 8px;4097 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};4098 }4099 .avail-domain {4100 font-size: 12px;4101 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};4102 font-family: "IBM Plex Mono", monospace;4103 }4104 .avail-label {4105 font-size: 9px;4106 letter-spacing: 0.16em;4107 text-transform: uppercase;4108 color: ${accentHex};4109 margin-bottom: 4px;4110 }4111 .avail-user {4112 font-size: 13px;4113 font-weight: 500;4114 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};4115 margin-bottom: 12px;4116 overflow: hidden;4117 text-overflow: ellipsis;4118 white-space: nowrap;4119 }4120 .avail-cross {4121 font-size: 11px;4122 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};4123 line-height: 1.45;4124 margin-bottom: 12px;4125 padding: 8px;4126 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};4127 border-radius: 6px;4128 }4129 .avail-btn {4130 display: block;4131 width: 100%;4132 padding: 8px 0;4133 border-radius: 7px;4134 border: none;4135 cursor: pointer;4136 font-size: 12px;4137 font-family: Inter, sans-serif;4138 font-weight: 500;4139 transition: opacity 100ms;4140 margin-bottom: 6px;4141 text-align: center;4142 }4143 .avail-btn:last-child { margin-bottom: 0; }4144 .avail-btn--accent { background: ${accentHex}; color: #fff; }4145 .avail-btn--accent:hover { opacity: 0.88; }4146 .avail-btn--ghost {4147 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};4148 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};4149 }4150 .avail-btn--ghost:hover { opacity: 0.75; }4151 `;4152 shadow.appendChild(style);41534154 const wrap = document.createElement("div");4155 wrap.className = "avail-wrap";4156 shadow.appendChild(wrap);41574158 const avail = document.createElement("div");4159 avail.className = "avail";4160 wrap.appendChild(avail);41614162 const closeBtn = document.createElement("button");4163 closeBtn.className = "avail-close";4164 closeBtn.type = "button";4165 closeBtn.setAttribute("aria-label", "Dismiss");4166 closeBtn.textContent = "✕";4167 avail.appendChild(closeBtn);41684169 const header = document.createElement("div");4170 header.className = "avail-header";4171 const icon = document.createElement("span");4172 icon.innerHTML = peachIconSvg();4173 const domainEl = document.createElement("span");4174 domainEl.className = "avail-domain";4175 domainEl.textContent = domain;4176 header.append(icon, domainEl);4177 avail.appendChild(header);41784179 const label = document.createElement("div");4180 label.className = "avail-label";4181 label.textContent = "PASSKEY";4182 avail.appendChild(label);41834184 const user = document.createElement("div");4185 user.className = "avail-user";4186 user.textContent = args.userName;4187 avail.appendChild(user);41884189 if (crossBrowser) {4190 const cross = document.createElement("div");4191 cross.className = "avail-cross";4192 cross.textContent =4193 args.fromBrowser === "chrome"4194 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."4195 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";4196 avail.appendChild(cross);4197 }41984199 const signInBtn = document.createElement("button");4200 signInBtn.className = "avail-btn avail-btn--accent";4201 signInBtn.type = "button";4202 signInBtn.textContent = "Sign in with passkey";4203 avail.appendChild(signInBtn);42044205 const passwordBtn = document.createElement("button");4206 passwordBtn.className = "avail-btn avail-btn--ghost";4207 passwordBtn.type = "button";4208 passwordBtn.textContent = "Use password instead";4209 avail.appendChild(passwordBtn);42104211 function dismiss() {4212 wrap.classList.add("is-exiting");4213 setTimeout(() => host.remove(), 130);4214 }42154216 signInBtn.addEventListener("click", () => {4217 4218 4219 4220 4221 4222 dismiss();4223 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});4224 });4225 passwordBtn.addEventListener("click", dismiss);4226 closeBtn.addEventListener("click", dismiss);42274228 document.body.appendChild(host);4229}423042314232423342344235423642374238export function showCardSelector(4239 anchorEl: HTMLInputElement,4240 cards: CardSelectorEntry[],4241 options: CardSelectorOptions4242): () => void {4243 if (cards.length === 0) return () => {};4244 const card = cards[0]!;42454246 const host = document.createElement("div");4247 host.style.cssText = "position: absolute; z-index: 2147483647;";4248 host.style.setProperty("--peach-accent", accentHex);4249 host.style.setProperty("--peach-accent-dim", withAlpha(accentHex, "28"));4250 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));4251 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));4252 const shadow = host.attachShadow({ mode: "closed" });42534254 const style = shadow.appendChild(document.createElement("style"));4255 style.textContent = SELECTOR_STYLES;42564257 const rect = anchorEl.getBoundingClientRect();4258 host.style.top = `${rect.bottom + window.scrollY + 5}px`;4259 host.style.left = `${rect.left + window.scrollX}px`;4260 host.style.width = `${Math.min(Math.max(rect.width, 320), 400)}px`;42614262 const container = shadow.appendChild(document.createElement("div"));4263 container.className = "peach-inline-popup";4264 container.setAttribute("data-theme", popupTheme);42654266 4267 const domHdr = document.createElement("div");4268 domHdr.className = "domain-header";4269 const eyebrow = document.createElement("div");4270 eyebrow.className = "domain-eyebrow";4271 eyebrow.textContent = "Pay with Peach";4272 const domName = document.createElement("div");4273 domName.className = "domain-name";4274 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");4275 domHdr.append(eyebrow, domName);4276 container.appendChild(domHdr);42774278 const platter = document.createElement("div");4279 platter.className = "card-platter";42804281 if (card.network && card.network !== "unknown") {4282 const net = document.createElement("div");4283 net.className = "cp-network";4284 net.textContent = card.network;4285 platter.appendChild(net);4286 }42874288 const nick = document.createElement("div");4289 nick.className = "cp-nickname";4290 nick.textContent = card.title;4291 platter.appendChild(nick);42924293 if (card.last4) {4294 const l4 = document.createElement("div");4295 l4.className = "cp-last4";4296 l4.textContent = `•••• ${card.last4}`;4297 platter.appendChild(l4);4298 }42994300 const meta = document.createElement("div");4301 meta.className = "cp-meta";4302 if (card.cardholderName) {4303 const name = document.createElement("span");4304 name.textContent = card.cardholderName;4305 meta.appendChild(name);4306 }4307 if (card.expMonth && card.expYear) {4308 if (card.cardholderName) {4309 const sep = document.createElement("span");4310 sep.className = "cp-meta-sep";4311 sep.textContent = "·";4312 meta.appendChild(sep);4313 }4314 const exp = document.createElement("span");4315 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;4316 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;4317 meta.appendChild(exp);4318 }4319 if (meta.childElementCount > 0) platter.appendChild(meta);43204321 const fillWrap = document.createElement("div");4322 fillWrap.className = "cp-fill-wrap";4323 const fillBtn = document.createElement("button");4324 fillBtn.className = "sp-fill-btn";4325 fillBtn.type = "button";4326 fillBtn.textContent = "Fill card details";4327 fillBtn.addEventListener("click", async () => {4328 if (fillBtn.disabled) return;4329 fillBtn.disabled = true;4330 fillBtn.classList.add("filled");4331 fillBtn.innerHTML = checkSvgStr();4332 const resp = await chrome.runtime.sendMessage({4333 type: "GET_ENTRY_FOR_FILL",4334 4335 payload: { id: card.id, explicitSummon: true },4336 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4337 if (resp?.ok && resp.data) options.onFill(resp.data);4338 setTimeout(() => dismiss(), 400);4339 });4340 fillWrap.appendChild(fillBtn);4341 platter.appendChild(fillWrap);43424343 4344 const actions = document.createElement("div");4345 actions.className = "sp-actions";43464347 function flashBtn(b: HTMLButtonElement) {4348 b.classList.add("flashed");4349 setTimeout(() => b.classList.remove("flashed"), 700);4350 }43514352 4353 function flashCopyError(b: HTMLButtonElement) {4354 const original = b.textContent ?? "";4355 b.textContent = "Blocked";4356 b.classList.add("flashed");4357 setTimeout(() => {4358 b.classList.remove("flashed");4359 if (b.isConnected) b.textContent = original;4360 }, 1200);4361 }43624363 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {4364 const resp = await chrome.runtime.sendMessage({4365 type: "GET_ENTRY_FOR_FILL",4366 4367 payload: { id: card.id, explicitSummon: true },4368 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4369 return resp?.ok && resp.data ? resp.data : null;4370 }43714372 const copyNum = document.createElement("button");4373 copyNum.className = "sp-action-btn";4374 copyNum.type = "button";4375 copyNum.title = "Copy card number";4376 copyNum.textContent = "Copy number";4377 copyNum.addEventListener("click", async (e) => {4378 e.stopPropagation();4379 const full = await fetchFullCard();4380 if (!full?.cardNumber) return;4381 let alreadyWritten = false;4382 try {4383 await navigator.clipboard.writeText(full.cardNumber);4384 alreadyWritten = true;4385 } catch (err) {4386 console.warn("[Peach] clipboard write denied:", err);4387 }4388 const resp = await chrome.runtime.sendMessage({4389 type: "COPY",4390 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },4391 }).catch(() => null) as { ok?: boolean } | null;4392 if (alreadyWritten || resp?.ok) flashBtn(copyNum);4393 else flashCopyError(copyNum);4394 });4395 actions.appendChild(copyNum);43964397 const copyCvc = document.createElement("button");4398 copyCvc.className = "sp-action-btn";4399 copyCvc.type = "button";4400 copyCvc.title = "Copy CVC";4401 copyCvc.textContent = "Copy CVC";4402 copyCvc.addEventListener("click", async (e) => {4403 e.stopPropagation();4404 const full = await fetchFullCard();4405 if (!full?.cvc) return;4406 let alreadyWritten = false;4407 try {4408 await navigator.clipboard.writeText(full.cvc);4409 alreadyWritten = true;4410 } catch (err) {4411 console.warn("[Peach] clipboard write denied:", err);4412 }4413 const resp = await chrome.runtime.sendMessage({4414 type: "COPY",4415 payload: { text: full.cvc, label: "CVC", alreadyWritten },4416 }).catch(() => null) as { ok?: boolean } | null;4417 if (alreadyWritten || resp?.ok) flashBtn(copyCvc);4418 else flashCopyError(copyCvc);4419 });4420 actions.appendChild(copyCvc);44214422 const closeBtn = document.createElement("button");4423 closeBtn.className = "sp-action-btn";4424 closeBtn.type = "button";4425 closeBtn.title = "Dismiss";4426 closeBtn.textContent = "✕";4427 closeBtn.addEventListener("click", (e) => {4428 e.stopPropagation();4429 dismiss(true);4430 });4431 actions.appendChild(closeBtn);44324433 platter.appendChild(actions);4434 container.appendChild(platter);44354436 document.body.appendChild(host);4437 function updatePosition() {4438 const r = anchorEl.getBoundingClientRect();4439 host.style.top = `${r.bottom + window.scrollY + 5}px`;4440 host.style.left = `${r.left + window.scrollX}px`;4441 }4442 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);44434444 function onKeyDown(e: KeyboardEvent) {4445 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }4446 }4447 document.addEventListener("keydown", onKeyDown);44484449 let exitScheduled = false;4450 function dismiss(userAction = false) {4451 if (exitScheduled) return;4452 exitScheduled = true;4453 stopObserving();4454 document.removeEventListener("click", outsideClick, { capture: true });4455 document.removeEventListener("keydown", onKeyDown);4456 if (userAction) options.onDismiss?.();4457 container.classList.add("is-exiting");4458 setTimeout(() => host.remove(), 130);4459 }44604461 function outsideClick(ev: MouseEvent) {4462 const path = ev.composedPath();4463 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);4464 }4465 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);44664467 return dismiss;4468}