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-glow", withAlpha(accentHex, "18"));1477 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1478 1479 1480 1481 host.style.willChange = "top, left";1482 1483 1484 1485 1486 1487 const shadow = host.attachShadow({ mode: "closed", delegatesFocus: true });14881489 const style = shadow.appendChild(document.createElement("style"));1490 style.textContent = SELECTOR_STYLES;14911492 const rect = anchorEl.getBoundingClientRect();1493 1494 1495 host.style.top = `${rect.bottom + 5}px`;1496 host.style.left = `${rect.left}px`;1497 1498 1499 1500 1501 const popupWidth = Math.min(Math.max(rect.width, 320), 400);1502 host.style.width = `${popupWidth}px`;15031504 const container = shadow.appendChild(document.createElement("div"));1505 container.className = "peach-inline-popup";1506 container.setAttribute("data-theme", popupTheme);15071508 1509 1510 let searchableEntries: SelectorEntry[] = options.searchableEntries ?? [];1511 let searchableLoading = false;1512 let searchableLoaded = !options.loadSearchableEntries;15131514 async function ensureSearchableLoaded(rerender: () => void): Promise<void> {1515 if (searchableLoaded || searchableLoading || !options.loadSearchableEntries) {1516 return;1517 }1518 searchableLoading = true;1519 try {1520 searchableEntries = await options.loadSearchableEntries();1521 } catch {1522 searchableEntries = [];1523 } finally {1524 searchableLoaded = true;1525 searchableLoading = false;1526 rerender();1527 }1528 }1529 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();15301531 let query = "";1532 let focusedIndex = -1;1533 let currentMode: "list" | "create" | "success" | "silver-platter" | "identity-helper" | "edit" = "list";1534 let searchInput: HTMLInputElement | null = null;1535 let identityMode: IdentityFilterMode = "both";1536 1537 1538 1539 let silverPlatterOrigin: SelectorEntry | null = null;1540 1541 1542 const cleanupHooks: Array<() => void> = [];15431544 1545 1546 1547 {1548 let prevH = 0;1549 let blocked = false;1550 const heightRo = new ResizeObserver(([entry]) => {1551 if (blocked) return;1552 const h = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;1553 if (prevH > 0 && Math.abs(h - prevH) > 1) {1554 blocked = true;1555 container.style.height = `${prevH}px`;1556 requestAnimationFrame(() => requestAnimationFrame(() => {1557 container.style.height = `${h}px`;1558 prevH = h;1559 const onEnd = (e: TransitionEvent) => {1560 if (e.propertyName !== "height") return;1561 container.style.height = "";1562 blocked = false;1563 container.removeEventListener("transitionend", onEnd);1564 };1565 container.addEventListener("transitionend", onEnd);1566 }));1567 } else {1568 prevH = h;1569 }1570 });1571 heightRo.observe(container);1572 cleanupHooks.push(() => heightRo.disconnect());1573 }15741575 1576 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";1577 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");1578 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";15791580 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {1581 const seen = new Set<string>();1582 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });1583 }15841585 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {1586 const q = value.trim().toLowerCase();1587 const deduped = uniqueEntries(source);1588 if (!q) return deduped.slice(0, 8);1589 return deduped.filter(e =>1590 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)1591 ).slice(0, 12);1592 }15931594 function activeEntries(): SelectorEntry[] {1595 1596 if (query.trim()) {1597 const merged = uniqueEntries([...entries, ...searchableEntries]);1598 return filterEntries(merged, query);1599 }1600 1601 if (entries.length > 0) return entries;1602 return filterEntries(searchableEntries, "").slice(0, 8);1603 }16041605 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {1606 const icon = document.createElement("div");1607 icon.className = `entry-icon${isNew ? " is-new" : ""}`;1608 if (isNew) {1609 icon.innerHTML = plusSvgStr();1610 } else {1611 1612 1613 1614 void url;1615 icon.innerHTML = keySvgStr();1616 }1617 return icon;1618 }16191620 function renderIdentityHelper() {1621 currentMode = "identity-helper";1622 container.innerHTML = "";1623 focusedIndex = -1;1624 searchInput = null;16251626 const fallbackKind: IdentityKind = options.identityKind ?? "username";1627 const identities = filterSavedIdentities(options.savedIdentities ?? [], query, identityMode);1628 const typedQuery = query.trim();1629 const customKind = resolveCustomIdentityKind(typedQuery, identityMode, fallbackKind);16301631 function applyIdentity(identity: string) {1632 if (options.hasEmailField) {1633 if (options.identityKind === "email") {1634 options.onFillFields?.("", "", identity); 1635 renderSecondaryIdentityStep("username", identity);1636 } else {1637 options.onFillFields?.(identity, "", ""); 1638 renderSecondaryIdentityStep("email", identity);1639 }1640 } else {1641 options.onFillFields?.(identity, "");1642 renderGenerator(identity);1643 }1644 }16451646 function applyCustomIdentity(identity: string) {1647 const trimmed = identity.trim();1648 if (!trimmed) return;1649 applyIdentity(trimmed);1650 }16511652 1653 const hdr = container.appendChild(document.createElement("div"));1654 hdr.className = "domain-header";1655 const headText = hdr.appendChild(document.createElement("div"));1656 headText.className = "domain-header-text";1657 const eyebrow = headText.appendChild(document.createElement("div"));1658 eyebrow.className = "domain-eyebrow";1659 eyebrow.textContent = "NEW ACCOUNT";1660 const domName = headText.appendChild(document.createElement("div"));1661 domName.className = "domain-name";1662 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");16631664 const actions = hdr.appendChild(document.createElement("div"));1665 actions.className = "identity-mode-toggle";1666 for (const mode of ["both", "email", "username"] as const) {1667 const btn = actions.appendChild(document.createElement("button"));1668 btn.type = "button";1669 btn.className = `identity-mode-btn${identityMode === mode ? " is-active" : ""}`;1670 btn.textContent = identityFilterLabel(mode);1671 btn.setAttribute("aria-pressed", String(identityMode === mode));1672 btn.addEventListener("click", () => {1673 identityMode = mode;1674 renderIdentityHelper();1675 });1676 }16771678 const searchWrap = container.appendChild(document.createElement("div"));1679 searchWrap.className = "search-wrap identity-search-wrap";1680 const search = searchWrap.appendChild(document.createElement("input"));1681 search.className = "search";1682 search.placeholder = "search identities…";1683 search.value = query;1684 search.addEventListener("input", () => {1685 query = search.value;1686 renderIdentityHelper();1687 searchInput?.focus();1688 searchInput?.setSelectionRange(query.length, query.length);1689 });1690 search.addEventListener("keydown", (e) => {1691 if (e.key === "Escape") {1692 e.preventDefault();1693 dismiss(true);1694 return;1695 }1696 if (e.key !== "Enter") return;1697 const exact = identities.find((id) => id.value.toLowerCase() === typedQuery.toLowerCase());1698 if (exact) {1699 e.preventDefault();1700 applyIdentity(exact.value);1701 return;1702 }1703 if (!typedQuery || identities.length > 0) return;1704 e.preventDefault();1705 applyCustomIdentity(typedQuery);1706 });1707 const searchTools = searchWrap.appendChild(document.createElement("div"));1708 searchTools.className = "identity-mode-toggle";1709 const sortHint = searchTools.appendChild(document.createElement("div"));1710 sortHint.className = "identity-empty";1711 sortHint.style.padding = "0";1712 sortHint.textContent = "sorted by recent use";1713 searchInput = search;1714 const customLabel = identityMode === "both"1715 ? customKind1716 : identityMode;17171718 1719 const list = container.appendChild(document.createElement("div"));1720 list.className = "identity-list";1721 1722 1723 1724 1725 const updateListFade = () => {1726 const overflowing = list.scrollHeight > list.clientHeight + 1;1727 const atBottom = list.scrollTop + list.clientHeight >= list.scrollHeight - 2;1728 const mask = overflowing && !atBottom1729 ? "linear-gradient(to bottom, black calc(100% - 24px), transparent 100%)"1730 : "none";1731 list.style.setProperty("mask-image", mask);1732 list.style.setProperty("-webkit-mask-image", mask);1733 };1734 list.addEventListener("scroll", updateListFade);1735 if (typedQuery && identities.length === 0) {1736 const empty = list.appendChild(document.createElement("div"));1737 empty.className = "identity-empty";1738 empty.textContent = identityMode === "both"1739 ? `No match for “${typedQuery}”.`1740 : `No ${identityFilterLabel(identityMode).toLowerCase()} match for “${typedQuery}”.`;1741 const custom = list.appendChild(document.createElement("div"));1742 custom.className = "identity-custom-row";1743 const customTitle = custom.appendChild(document.createElement("div"));1744 customTitle.className = "identity-custom-row-title";1745 customTitle.textContent = `Use “${typedQuery}” as a new ${customLabel}`;1746 const customSub = custom.appendChild(document.createElement("div"));1747 customSub.className = "identity-custom-row-sub";1748 customSub.textContent = "Press Enter or click to continue.";1749 custom.addEventListener("click", () => applyCustomIdentity(typedQuery));1750 }1751 for (const id of identities) {1752 const row = list.appendChild(document.createElement("div"));1753 row.className = "peach-identity-row";1754 row.setAttribute("tabindex", "-1");1755 const val = row.appendChild(document.createElement("div"));1756 val.className = "peach-identity-value";1757 val.textContent = id.value;1758 const meta = row.appendChild(document.createElement("div"));1759 meta.className = "peach-identity-meta";1760 meta.textContent = `used ${id.useCount}×`;1761 row.addEventListener("click", () => {1762 applyIdentity(id.value);1763 });1764 }17651766 updateListFade();17671768 1769 if (identities.length > 0) {1770 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1771 }17721773 1774 const altRow = container.appendChild(document.createElement("div"));1775 altRow.className = "identity-alt-row";1776 altRow.textContent = `+ Use a different ${customLabel}…`;17771778 const altInputWrap = container.appendChild(document.createElement("div"));1779 altInputWrap.className = "identity-alt-input-wrap";1780 const altInput = altInputWrap.appendChild(document.createElement("input"));1781 altInput.className = "identity-alt-input";1782 altInput.type = "text";1783 altInput.placeholder = customLabel === "email" ? "[email protected]" : "your username";1784 altInput.setAttribute("autocomplete", "off");1785 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1786 altConfirm.className = "identity-alt-confirm";1787 altConfirm.type = "button";1788 altConfirm.textContent = "✓";17891790 function fillCustomIdentity() {1791 const v = altInput.value.trim();1792 if (!v) return;1793 applyCustomIdentity(v);1794 }17951796 altRow.addEventListener("click", () => {1797 altRow.style.display = "none";1798 altInputWrap.classList.add("open");1799 altInput.value = typedQuery || altInput.value;1800 altInput.focus();1801 altInput.setSelectionRange(altInput.value.length, altInput.value.length);1802 });1803 altConfirm.addEventListener("click", fillCustomIdentity);1804 altInput.addEventListener("keydown", (e) => {1805 if (e.key === "Enter") { e.preventDefault(); fillCustomIdentity(); }1806 });18071808 1809 1810 1811 1812 1813 1814 1815 if (options.loadSearchableEntries || (options.searchableEntries?.length ?? 0) > 0) {1816 const escapeRow = container.appendChild(document.createElement("div"));1817 escapeRow.className = "identity-escape-row";1818 escapeRow.textContent = "I already have an account →";1819 escapeRow.setAttribute("tabindex", "0");1820 const toSearch = () => { void ensureSearchableLoaded(renderList); renderList(); };1821 escapeRow.addEventListener("click", toSearch);1822 escapeRow.addEventListener("keydown", (e) => {1823 if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toSearch(); }1824 });1825 }18261827 requestAnimationFrame(() => {1828 search.focus();1829 search.setSelectionRange(query.length, query.length);1830 });1831 }18321833 18341835183618371838 function renderSecondaryIdentityStep(stepKind: "email" | "username", firstValue: string) {1839 currentMode = "identity-helper";1840 container.innerHTML = "";18411842 const wrap = container.appendChild(document.createElement("div"));1843 wrap.className = "gen-view";18441845 1846 const hdrRow = wrap.appendChild(document.createElement("div"));1847 hdrRow.className = "domain-header";1848 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1849 eyebrow2.className = "domain-eyebrow";1850 eyebrow2.textContent = "NEW ACCOUNT";1851 const domName2 = hdrRow.appendChild(document.createElement("div"));1852 domName2.className = "domain-name";1853 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");18541855 1856 const conf = wrap.appendChild(document.createElement("div"));1857 conf.className = "gen-confirmed";1858 const confCheck = conf.appendChild(document.createElement("span"));1859 confCheck.className = "gen-confirmed-check";1860 confCheck.textContent = "✓";1861 const confVal = conf.appendChild(document.createElement("div"));1862 confVal.className = "gen-confirmed-val";1863 confVal.textContent = firstValue;18641865 const stepLbl = wrap.appendChild(document.createElement("div"));1866 stepLbl.className = "gen-label";1867 stepLbl.textContent = stepKind === "email" ? "Now choose your email" : "Now choose your username";18681869 1870 const identities = (options.savedIdentities ?? [])1871 .filter(s => s.type === stepKind)1872 .sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0))1873 .slice(0, 6);18741875 const list = wrap.appendChild(document.createElement("div"));1876 list.className = "identity-list";1877 for (const id of identities) {1878 const row = list.appendChild(document.createElement("div"));1879 row.className = "peach-identity-row";1880 row.setAttribute("tabindex", "-1");1881 const val = row.appendChild(document.createElement("div"));1882 val.className = "peach-identity-value";1883 val.textContent = id.value;1884 const meta = row.appendChild(document.createElement("div"));1885 meta.className = "peach-identity-meta";1886 meta.textContent = `used ${id.useCount}×`;1887 row.addEventListener("click", () => applySecondIdentity(id.value));1888 }18891890 1891 const altInputWrap = wrap.appendChild(document.createElement("div"));1892 altInputWrap.className = "identity-alt-input-wrap open";1893 const altInput = altInputWrap.appendChild(document.createElement("input"));1894 altInput.className = "identity-alt-input";1895 altInput.type = stepKind === "email" ? "email" : "text";1896 altInput.placeholder = stepKind === "email" ? "[email protected]" : "your username";1897 altInput.setAttribute("autocomplete", "off");1898 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1899 altConfirm.className = "identity-alt-confirm";1900 altConfirm.type = "button";1901 altConfirm.textContent = "→";19021903 function applySecondIdentity(value: string) {1904 const trimmed = value.trim();1905 if (!trimmed) return;1906 1907 const [email, username] = stepKind === "email"1908 ? [trimmed, firstValue]1909 : [firstValue, trimmed];1910 if (stepKind === "email") {1911 options.onFillFields?.("", "", trimmed); 1912 } else {1913 options.onFillFields?.(trimmed, "", ""); 1914 }1915 renderGenerator(email, username);1916 }19171918 altConfirm.addEventListener("click", () => applySecondIdentity(altInput.value));1919 altInput.addEventListener("keydown", (e) => {1920 if (e.key === "Enter") { e.preventDefault(); applySecondIdentity(altInput.value); }1921 });19221923 1924 const skipRow = wrap.appendChild(document.createElement("div"));1925 skipRow.className = "identity-escape-row";1926 skipRow.textContent = `Skip ${stepKind} →`;1927 skipRow.setAttribute("tabindex", "0");1928 skipRow.addEventListener("click", () => {1929 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1930 renderGenerator(email, username);1931 });1932 skipRow.addEventListener("keydown", (e) => {1933 if (e.key === "Enter" || e.key === " ") {1934 e.preventDefault();1935 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1936 renderGenerator(email, username);1937 }1938 });19391940 requestAnimationFrame(() => { altInput.focus(); });1941 }19421943 function renderGenerator(filledEmail: string, filledUsername?: string) {1944 container.innerHTML = "";1945 const sitePolicy = detectPasswordPolicy();19461947 const wrap = container.appendChild(document.createElement("div"));1948 wrap.className = "gen-view";19491950 1951 const hdrRow = wrap.appendChild(document.createElement("div"));1952 hdrRow.className = "domain-header";1953 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1954 eyebrow2.className = "domain-eyebrow";1955 eyebrow2.textContent = "NEW ACCOUNT";1956 const domName2 = hdrRow.appendChild(document.createElement("div"));1957 domName2.className = "domain-name";1958 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");19591960 1961 function confirmedRow(value: string) {1962 const conf = wrap.appendChild(document.createElement("div"));1963 conf.className = "gen-confirmed";1964 const confCheck = conf.appendChild(document.createElement("span"));1965 confCheck.className = "gen-confirmed-check";1966 confCheck.textContent = "✓";1967 const confVal = conf.appendChild(document.createElement("div"));1968 confVal.className = "gen-confirmed-val";1969 confVal.textContent = value;1970 }1971 if (filledEmail) confirmedRow(filledEmail);1972 if (filledUsername && filledUsername !== filledEmail) confirmedRow(filledUsername);19731974 const lbl = wrap.appendChild(document.createElement("div"));1975 lbl.className = "gen-label";1976 lbl.textContent = "Generate a password";19771978 let currentPw = "";1979 const smartGen = createSmartGenerator({1980 policy: sitePolicy,1981 onChange: (pw) => { currentPw = pw; },1982 });1983 wrap.appendChild(smartGen.root);19841985 const fillBtn = wrap.appendChild(document.createElement("button"));1986 fillBtn.className = "gen-fill-btn";1987 fillBtn.type = "button";1988 fillBtn.textContent = "Copy & fill";1989 fillBtn.addEventListener("click", async () => {1990 if (!currentPw) return;1991 fillBtn.disabled = true;1992 fillBtn.textContent = "Saving…";1993 1994 options.onFillFields?.("", currentPw, "");1995 1996 1997 const saveUsername = filledUsername || filledEmail;1998 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;1999 try {2000 const res = await chrome.runtime.sendMessage({2001 type: "ADD_ENTRY",2002 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: currentPw, type: "login" },2003 });2004 if ((res as { ok?: boolean }).ok) {2005 showSuccess(sTitle);2006 } else {2007 renderSaveOffer(filledEmail, currentPw, filledUsername);2008 }2009 } catch {2010 renderSaveOffer(filledEmail, currentPw, filledUsername);2011 }2012 });2013 }20142015 function renderSaveOffer(filledEmail: string, filledPassword: string, filledUsername?: string) {2016 container.innerHTML = "";2017 const domain = options.domain ?? location.hostname.replace(/^www\./, "");20182019 const wrap = container.appendChild(document.createElement("div"));2020 wrap.className = "save-offer";20212022 const title = wrap.appendChild(document.createElement("div"));2023 title.className = "save-offer-title";2024 title.textContent = "Save this login to Peach?";20252026 function detailRow(label: string, value: string) {2027 const row = wrap.appendChild(document.createElement("div"));2028 row.className = "save-offer-row";2029 const lbl = row.appendChild(document.createElement("div"));2030 lbl.className = "save-offer-lbl";2031 lbl.textContent = label;2032 const val = row.appendChild(document.createElement("div"));2033 val.className = "save-offer-val";2034 val.textContent = value;2035 }20362037 detailRow("site", domain);2038 2039 if (filledUsername) {2040 detailRow("username", filledUsername);2041 if (filledEmail) detailRow("email", filledEmail);2042 } else {2043 if (filledEmail) detailRow("email", filledEmail);2044 }2045 detailRow("password", "••••••••••••••••");20462047 const btns = wrap.appendChild(document.createElement("div"));2048 btns.className = "save-offer-btns";20492050 const saveBtn = btns.appendChild(document.createElement("button"));2051 saveBtn.className = "save-offer-save";2052 saveBtn.type = "button";2053 saveBtn.textContent = "Save";2054 saveBtn.addEventListener("click", async () => {2055 saveBtn.disabled = true;2056 saveBtn.textContent = "Saving…";2057 try {2058 2059 2060 const saveUsername = filledUsername || filledEmail;2061 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;2062 const res = await chrome.runtime.sendMessage({2063 type: "ADD_ENTRY",2064 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: filledPassword, type: "login" },2065 });2066 if ((res as { ok?: boolean }).ok) {2067 showSuccess(sTitle);2068 } else {2069 saveBtn.disabled = false;2070 saveBtn.textContent = "Save";2071 }2072 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }2073 });20742075 const skipBtn = btns.appendChild(document.createElement("button"));2076 skipBtn.className = "save-offer-skip";2077 skipBtn.type = "button";2078 skipBtn.textContent = "Not now";2079 skipBtn.addEventListener("click", () => { dismiss(true); });2080 }20812082 function renderList() {2083 container.innerHTML = "";2084 focusedIndex = -1;2085 searchInput = null;2086 currentMode = "list";20872088 const current = activeEntries();20892090 2091 2092 2093 const domHdr = document.createElement("div");2094 domHdr.className = "domain-header";2095 const headText = document.createElement("div");2096 headText.className = "domain-header-text";2097 const eyebrow = document.createElement("div");2098 eyebrow.className = "domain-eyebrow";2099 eyebrow.textContent = options.isSignup2100 ? "Sign up as"2101 : entries.length > 0 ? "Saved for this site" : "No saved login";2102 const domName = document.createElement("div");2103 domName.className = "domain-name";2104 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2105 headText.append(eyebrow, domName);2106 domHdr.appendChild(headText);21072108 if (!options.isSignup) {2109 const actions = document.createElement("div");2110 actions.className = "domain-header-actions";21112112 2113 2114 2115 if (silverPlatterOrigin) {2116 const backBtn = document.createElement("button");2117 backBtn.className = "domain-search-btn";2118 backBtn.type = "button";2119 backBtn.setAttribute("aria-label", "Back to suggested entry");2120 backBtn.title = "Back to suggested entry";2121 backBtn.innerHTML = backSvgStr();2122 backBtn.addEventListener("click", (e) => {2123 e.stopPropagation();2124 if (silverPlatterOrigin) renderSilverPlatter(silverPlatterOrigin);2125 });2126 actions.appendChild(backBtn);2127 }21282129 const addBtn = document.createElement("button");2130 addBtn.className = "domain-add-btn";2131 addBtn.type = "button";2132 addBtn.setAttribute("aria-label", "Create new entry");2133 addBtn.title = "Create new entry";2134 addBtn.textContent = "+";2135 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2136 actions.appendChild(addBtn);21372138 domHdr.appendChild(actions);2139 }21402141 container.appendChild(domHdr);21422143 2144 const sw = document.createElement("div"); sw.className = "search-wrap";2145 searchInput = document.createElement("input");2146 searchInput.className = "search";2147 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";2148 searchInput.value = query;2149 searchInput.addEventListener("input", () => {2150 query = searchInput?.value ?? "";2151 2152 2153 void ensureSearchableLoaded(renderList);2154 renderList();2155 searchInput?.focus();2156 searchInput?.setSelectionRange(query.length, query.length);2157 });2158 sw.appendChild(searchInput);2159 2160 2161 if (searchableLoading) {2162 const loadingHint = document.createElement("span");2163 loadingHint.className = "search-loading";2164 loadingHint.textContent = "searching…";2165 sw.appendChild(loadingHint);2166 }2167 container.appendChild(sw);21682169 2170 const results = document.createElement("div");2171 results.className = "results";2172 container.appendChild(results);21732174 if (current.length === 0 && entries.length > 0) {2175 2176 }21772178 for (let i = 0; i < current.length; i++) {2179 const e = current[i]!;2180 const entryEl = document.createElement("div");2181 entryEl.className = "entry";2182 entryEl.dataset.id = e.id;2183 entryEl.dataset.index = String(i);2184 entryEl.setAttribute("tabindex", "-1");21852186 entryEl.appendChild(makeEntryIcon(e.url));21872188 const info = document.createElement("div"); info.className = "entry-info";2189 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;2190 const subEl = document.createElement("div"); subEl.className = "entry-sub";2191 const domain = e.url ? getDomainFromUrl(e.url) : "";2192 subEl.textContent = domain || e.username || "";2193 info.append(titleEl, subEl);2194 entryEl.appendChild(info);21952196 if (e.hasTotp) {2197 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";2198 entryEl.appendChild(badge);2199 }2200 if (e.hasPasskey) {2201 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";2202 entryEl.appendChild(badge);2203 }22042205 entryEl.addEventListener("click", () => {2206 if (options.isSignup && options.onFillFields) {2207 2208 options.onFillFields(e.username ?? "", "", e.email);2209 dismiss();2210 } else {2211 onSelect(e.id);2212 dismiss();2213 }2214 });2215 results.appendChild(entryEl);2216 }22172218 2219 if (current.length > 0) {2220 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));2221 }22222223 2224 const newEl = document.createElement("div");2225 newEl.className = "entry";2226 newEl.setAttribute("tabindex", "-1");2227 newEl.appendChild(makeEntryIcon(undefined, true));22282229 const newInfo = document.createElement("div"); newInfo.className = "entry-info";2230 const newTitle = document.createElement("div");2231 newTitle.className = "entry-title is-new";2232 newTitle.textContent = `New entry for ${sTitle}`;2233 const newSub = document.createElement("div");2234 newSub.className = "entry-sub";2235 newSub.textContent = sOrigin;2236 newInfo.append(newTitle, newSub);2237 newEl.appendChild(newInfo);2238 newEl.addEventListener("click", showCreateForm);2239 results.appendChild(newEl);22402241 2242 if (searchInput) {2243 requestAnimationFrame(() => {2244 searchInput?.focus();2245 2246 2247 searchInput?.setSelectionRange(query.length, query.length);2248 });2249 }2250 }22512252 function setFocused(idx: number) {2253 const all = container.querySelectorAll<HTMLElement>(".entry");2254 all.forEach((el, i) => el.classList.toggle("focused", i === idx));2255 focusedIndex = idx;2256 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });2257 }22582259 function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[], fieldType: "username" | "email"): void {2260 const typed = identities.filter(s => s.type === fieldType);2261 if (!typed.length) return;2262 let collapsed = false;2263 let suppressNextFocus = false;22642265 const list = g.appendChild(document.createElement("div"));2266 list.className = "identity-suggest-list";2267 list.style.display = "none";22682269 2270 const collapseRow = list.appendChild(document.createElement("div"));2271 collapseRow.className = "identity-filter-row";2272 collapseRow.style.justifyContent = "flex-end";2273 const collapseBtn = collapseRow.appendChild(document.createElement("button"));2274 collapseBtn.type = "button";2275 collapseBtn.className = "identity-collapse-btn";2276 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>`;2277 collapseBtn.addEventListener("mousedown", (e) => {2278 e.preventDefault();2279 collapsed = !collapsed;2280 collapseBtn.classList.toggle("is-collapsed", collapsed);2281 rowsWrapper.classList.toggle("is-collapsed", collapsed);2282 });22832284 const rowsWrapper = list.appendChild(document.createElement("div"));2285 rowsWrapper.className = "identity-rows";2286 const rows = rowsWrapper.appendChild(document.createElement("div"));2287 rows.className = "identity-rows-inner";22882289 2290 2291 2292 const updateRowsFade = () => {2293 const overflowing = rows.scrollHeight > rows.clientHeight + 1;2294 const atBottom = rows.scrollTop + rows.clientHeight >= rows.scrollHeight - 2;2295 const mask = overflowing && !atBottom2296 ? "linear-gradient(to bottom, black calc(100% - 18px), transparent 100%)"2297 : "none";2298 rows.style.setProperty("mask-image", mask);2299 rows.style.setProperty("-webkit-mask-image", mask);2300 };2301 rows.addEventListener("scroll", updateRowsFade);23022303 2304 function hideList() {2305 list.style.display = "none";2306 document.removeEventListener("mousedown", onDocMousedown, { capture: true });2307 }23082309 2310 2311 2312 2313 2314 function onDocMousedown(e: MouseEvent) {2315 if (e.composedPath().includes(host)) return;2316 hideList();2317 }23182319 function pick(value: string) {2320 inp.value = value;2321 hideList();2322 suppressNextFocus = true;2323 inp.focus();2324 2325 2326 2327 setTimeout(() => { suppressNextFocus = false; }, 120);2328 }23292330 function render() {2331 const q = inp.value.toLowerCase();2332 const filtered = typed.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 8);2333 rows.innerHTML = "";2334 if (!filtered.length) { hideList(); return; }2335 for (const s of filtered) {2336 const row = rows.appendChild(document.createElement("button"));2337 row.type = "button"; row.className = "identity-suggest-row";2338 const val = row.appendChild(document.createElement("span"));2339 val.className = "identity-suggest-val"; val.textContent = s.value;2340 const meta = row.appendChild(document.createElement("span"));2341 meta.className = "identity-suggest-meta"; meta.textContent = `${s.useCount}×`;2342 row.addEventListener("mousedown", (e) => { e.preventDefault(); pick(s.value); });2343 row.addEventListener("keydown", (e) => {2344 if (e.key === "ArrowDown") {2345 e.preventDefault();2346 (row.nextElementSibling as HTMLElement | null)?.focus();2347 } else if (e.key === "ArrowUp") {2348 e.preventDefault();2349 const prev = row.previousElementSibling as HTMLElement | null;2350 if (prev) prev.focus();2351 else inp.focus();2352 } else if (e.key === "Enter" || e.key === " ") {2353 e.preventDefault();2354 pick(s.value);2355 } else if (e.key === "Escape") {2356 e.preventDefault();2357 hideList();2358 suppressNextFocus = true;2359 inp.focus();2360 }2361 });2362 2363 2364 2365 row.addEventListener("blur", (e) => {2366 const rt = (e as FocusEvent).relatedTarget as Node | null;2367 if (rt && (rt === inp || list.contains(rt))) return;2368 setTimeout(() => { hideList(); }, 150);2369 });2370 }2371 list.style.display = "";2372 updateRowsFade();2373 2374 2375 2376 document.addEventListener("mousedown", onDocMousedown, { capture: true });2377 }23782379 inp.addEventListener("focus", () => {2380 if (suppressNextFocus) { suppressNextFocus = false; return; }2381 render();2382 });2383 inp.addEventListener("input", render);2384 inp.addEventListener("keydown", (e) => {2385 if (list.style.display === "none") return;2386 if (e.key === "ArrowDown" && !collapsed) {2387 e.preventDefault();2388 (rows.firstElementChild as HTMLElement | null)?.focus();2389 } else if (e.key === "Escape") {2390 e.preventDefault();2391 hideList();2392 } else if (e.key === "Tab") {2393 2394 hideList();2395 }2396 });2397 2398 2399 2400 2401 24022403 2404 2405 cleanupHooks.push(hideList);2406 }24072408 async function showCreateForm() {2409 currentMode = "create";2410 container.innerHTML = "";24112412 2413 2414 2415 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;2416 if (!statusRes?.unlocked) {2417 renderLocked();2418 return;2419 }24202421 2422 2423 const sitePolicy = detectPasswordPolicy();24242425 2426 2427 let formIdentities: SavedIdentity[] = options.savedIdentities ?? [];2428 if (!formIdentities.length) {2429 try {2430 const idRes = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as { ok: boolean; data?: SavedIdentity[] } | null;2431 if (idRes?.ok && Array.isArray(idRes.data)) formIdentities = idRes.data;2432 } catch { }2433 }24342435 const wrap = document.createElement("div");2436 wrap.className = "create-form-wrap";24372438 2439 const head = document.createElement("div"); head.className = "create-head";2440 const backBtn = document.createElement("button"); backBtn.className = "back-btn";2441 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>`;2442 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });2443 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";2444 head.append(backBtn, lbl);24452446 2447 const body = document.createElement("div"); body.className = "create-body";24482449 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {2450 const g = document.createElement("div"); g.className = "f-group";2451 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;2452 const inp = document.createElement("input"); inp.className = "f-input";2453 g.append(l, inp); return { g, inp };2454 }24552456 2457 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";2458 body.appendChild(tg.g);24592460 2461 const ug = fGroup("website");2462 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";2463 if (loginSubdomain) {2464 const h = document.createElement("div"); h.className = "url-hint";2465 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);2466 }2467 body.appendChild(ug.g);24682469 2470 const xg = fGroup("username");2471 xg.inp.value = preUsername && !preUsername.includes("@") ? preUsername : "";2472 xg.inp.placeholder = "johndoe";2473 xg.inp.setAttribute("autocomplete", "off");2474 if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities, "username");2475 body.appendChild(xg.g);24762477 2478 const eg = fGroup("email");2479 (eg.inp as HTMLInputElement).type = "email";2480 eg.inp.value = preUsername && preUsername.includes("@") ? preUsername : "";2481 eg.inp.placeholder = "[email protected]";2482 eg.inp.setAttribute("autocomplete", "off");2483 if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities, "email");2484 body.appendChild(eg.g);24852486 2487 2488 2489 const pg = fGroup("password");2490 pg.inp.remove(); 2491 let currentPassword = prePassword || "";2492 const smartGen = createSmartGenerator({2493 policy: sitePolicy,2494 initialPassword: prePassword || undefined,2495 onChange: (pw) => { currentPassword = pw; },2496 });2497 pg.g.appendChild(smartGen.root);2498 body.appendChild(pg.g);24992500 2501 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";2502 saveBtn.textContent = "Save entry";2503 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";2504 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }2505 saveBtn.addEventListener("click", async () => {2506 const title = tg.inp.value.trim();2507 const pw = currentPassword.trim();2508 const username = xg.inp.value.trim();2509 const email = eg.inp.value.trim();2510 if (!title) {2511 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();2512 showFormError("Entry needs a title");2513 return;2514 }2515 if (!pw) {2516 showFormError("Entry needs a password — tap regen to make one");2517 return;2518 }2519 errEl.style.display = "none";2520 saveBtn.disabled = true; saveBtn.textContent = "Saving…";2521 try {2522 const res = await chrome.runtime.sendMessage({2523 type: "ADD_ENTRY",2524 payload: { title, url: ug.inp.value.trim(), username, email: email || undefined, password: pw, type: "login" },2525 });2526 if ((res as { ok?: boolean }).ok) {2527 2528 2529 options.onFillFields?.(username || email, pw);2530 showSuccess(title);2531 } else {2532 const err = (res as { error?: string })?.error;2533 const errMsg = err === "vault_locked"2534 ? "Vault locked — open Peach to save"2535 : err === "STORAGE_QUOTA_EXCEEDED"2536 ? "Out of storage — open Peach → Settings to free space"2537 : err2538 ? `Could not save: ${err}`2539 : "Could not save entry";2540 showFormError(errMsg);2541 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2542 }2543 } catch (e) {2544 showFormError(e instanceof Error && e.message ? `Could not reach Peach: ${e.message}` : "Could not reach Peach");2545 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2546 }2547 });2548 body.appendChild(saveBtn);2549 body.appendChild(errEl);25502551 wrap.append(head, body);2552 container.appendChild(wrap);2553 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });2554 }25552556 function showSuccess(title: string) {2557 currentMode = "success";2558 container.innerHTML = "";2559 const wrap = document.createElement("div"); wrap.className = "success-wrap";2560 const ring = document.createElement("div"); ring.className = "success-ring";2561 ring.innerHTML = checkSvgStr();2562 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;2563 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;2564 wrap.append(ring, t, s);2565 container.appendChild(wrap);2566 setTimeout(() => dismiss(), 1600);2567 }25682569 function flashActionBtn(btn: HTMLButtonElement) {2570 btn.classList.add("flashed");2571 setTimeout(() => btn.classList.remove("flashed"), 700);2572 }25732574 2575 2576 function flashCopyError(btn: HTMLButtonElement) {2577 const original = btn.textContent ?? "";2578 btn.textContent = "Blocked";2579 btn.classList.add("flashed");2580 setTimeout(() => {2581 btn.classList.remove("flashed");2582 if (btn.isConnected) btn.textContent = original;2583 }, 1200);2584 }25852586 function renderSilverPlatter(entry: SelectorEntry) {2587 container.innerHTML = "";2588 searchInput = null;2589 focusedIndex = -1;2590 currentMode = "silver-platter";2591 silverPlatterOrigin = entry;25922593 const domHdr = document.createElement("div");2594 domHdr.className = "domain-header";2595 const headText = document.createElement("div");2596 headText.className = "domain-header-text";2597 const eyebrow = document.createElement("div");2598 eyebrow.className = "domain-eyebrow";2599 eyebrow.textContent = "Saved for this site";2600 const domName = document.createElement("div");2601 domName.className = "domain-name";2602 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2603 headText.append(eyebrow, domName);2604 domHdr.appendChild(headText);2605 if (!options.isSignup) {2606 const actions = document.createElement("div");2607 actions.className = "domain-header-actions";26082609 2610 2611 2612 const searchBtn = document.createElement("button");2613 searchBtn.className = "domain-search-btn";2614 searchBtn.type = "button";2615 searchBtn.setAttribute("aria-label", "Search for a different entry");2616 searchBtn.title = "Search for a different entry";2617 searchBtn.innerHTML = searchSvgStr();2618 searchBtn.addEventListener("click", (e) => {2619 e.stopPropagation();2620 query = "";2621 renderList();2622 });2623 actions.appendChild(searchBtn);26242625 const addBtn = document.createElement("button");2626 addBtn.className = "domain-add-btn";2627 addBtn.type = "button";2628 addBtn.setAttribute("aria-label", "Create new entry");2629 addBtn.title = "Create new entry";2630 addBtn.textContent = "+";2631 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2632 actions.appendChild(addBtn);26332634 domHdr.appendChild(actions);2635 }2636 container.appendChild(domHdr);26372638 const platter = document.createElement("div");2639 platter.className = "silver-platter";26402641 const titleEl = document.createElement("div");2642 titleEl.className = "sp-title";2643 titleEl.textContent = entry.title;2644 platter.appendChild(titleEl);26452646 if (entry.email) {2647 const emailEl = document.createElement("div");2648 emailEl.className = "sp-identity-row";2649 const emailLbl = document.createElement("span");2650 emailLbl.className = "sp-identity-label";2651 emailLbl.textContent = "email";2652 const emailVal = document.createElement("span");2653 emailVal.className = "sp-username";2654 emailVal.textContent = entry.email;2655 emailEl.append(emailLbl, emailVal);2656 platter.appendChild(emailEl);2657 }26582659 if (entry.username) {2660 const userEl = document.createElement("div");2661 userEl.className = "sp-identity-row";2662 const userLbl = document.createElement("span");2663 userLbl.className = "sp-identity-label";2664 userLbl.textContent = entry.username.includes("@") ? "email" : "username";2665 const userVal = document.createElement("span");2666 userVal.className = "sp-username";2667 userVal.textContent = entry.username;2668 userEl.append(userLbl, userVal);2669 platter.appendChild(userEl);2670 }26712672 const fillBtn = document.createElement("button");2673 fillBtn.className = "sp-fill-btn";2674 fillBtn.type = "button";2675 fillBtn.textContent = "Fill credentials";2676 fillBtn.addEventListener("click", () => {2677 if (fillBtn.disabled) return;2678 fillBtn.disabled = true;2679 fillBtn.classList.add("filled");2680 fillBtn.innerHTML = checkSvgStr();2681 2682 2683 2684 2685 onSelect(entry.id);2686 setTimeout(() => dismiss(), 400);2687 });2688 platter.appendChild(fillBtn);26892690 const actionsGroup = document.createElement("div");2691 actionsGroup.className = "sp-actions-group";2692 const actionsLabel = document.createElement("div");2693 actionsLabel.className = "sp-actions-label";2694 actionsLabel.textContent = "Copy";2695 actionsGroup.appendChild(actionsLabel);26962697 const actions = document.createElement("div");2698 actions.className = "sp-actions";2699 actionsGroup.appendChild(actions);27002701 if (entry.email) {2702 const copyEmail = document.createElement("button");2703 copyEmail.className = "sp-action-btn";2704 copyEmail.type = "button";2705 copyEmail.title = "Copy email";2706 copyEmail.textContent = "Email";2707 copyEmail.addEventListener("click", async (e) => {2708 e.stopPropagation();2709 let alreadyWritten = false;2710 try {2711 await navigator.clipboard.writeText(entry.email!);2712 alreadyWritten = true;2713 } catch (err) {2714 console.warn("[Peach] clipboard write denied:", err);2715 }2716 const resp = await chrome.runtime.sendMessage({2717 type: "COPY",2718 payload: { text: entry.email, label: "Email", alreadyWritten },2719 }).catch(() => null) as { ok?: boolean } | null;2720 if (alreadyWritten || resp?.ok) flashActionBtn(copyEmail);2721 else flashCopyError(copyEmail);2722 });2723 actions.appendChild(copyEmail);2724 }27252726 const copyU = document.createElement("button");2727 copyU.className = "sp-action-btn";2728 copyU.type = "button";2729 const usernameIsEmail = entry.username?.includes("@") ?? false;2730 copyU.title = usernameIsEmail ? "Copy email" : "Copy username";2731 copyU.textContent = usernameIsEmail ? "Email" : "Username";2732 copyU.disabled = !entry.username;2733 copyU.addEventListener("click", async (e) => {2734 e.stopPropagation();2735 if (!entry.username) return;2736 let alreadyWritten = false;2737 try {2738 await navigator.clipboard.writeText(entry.username);2739 alreadyWritten = true;2740 } catch (err) {2741 console.warn("[Peach] clipboard write denied:", err);2742 }2743 const resp = await chrome.runtime.sendMessage({2744 type: "COPY",2745 payload: { text: entry.username, label: entry.username.includes("@") ? "Email" : "Username", alreadyWritten },2746 }).catch(() => null) as { ok?: boolean } | null;2747 if (alreadyWritten || resp?.ok) flashActionBtn(copyU);2748 else flashCopyError(copyU);2749 });2750 actions.appendChild(copyU);27512752 const copyP = document.createElement("button");2753 copyP.className = "sp-action-btn";2754 copyP.type = "button";2755 copyP.title = "Copy password";2756 copyP.textContent = "Password";2757 copyP.addEventListener("click", async (e) => {2758 e.stopPropagation();2759 const resp = await chrome.runtime.sendMessage({2760 type: "GET_ENTRY_FOR_FILL",2761 2762 payload: { id: entry.id, explicitSummon: true },2763 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;2764 if (!resp?.ok || !resp.data?.password) return;2765 let alreadyWritten = false;2766 try {2767 await navigator.clipboard.writeText(resp.data.password);2768 alreadyWritten = true;2769 } catch (err) {2770 console.warn("[Peach] clipboard write denied:", err);2771 }2772 const copyResp = await chrome.runtime.sendMessage({2773 type: "COPY",2774 payload: { text: resp.data.password, label: "Password", alreadyWritten },2775 }).catch(() => null) as { ok?: boolean } | null;2776 if (alreadyWritten || copyResp?.ok) flashActionBtn(copyP);2777 else flashCopyError(copyP);2778 });2779 actions.appendChild(copyP);27802781 let trayEl: HTMLDivElement | null = null;2782 let trayInterval: ReturnType<typeof setInterval> | null = null;27832784 function closeTray() {2785 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }2786 trayEl?.remove();2787 trayEl = null;2788 }2789 2790 2791 cleanupHooks.push(closeTray);27922793 async function openTOTPTray() {2794 2795 2796 2797 2798 async function fetchCode(): Promise<{ code: string; remaining: number } | null> {2799 const r = await chrome.runtime.sendMessage({2800 type: "GET_ENTRY_FOR_FILL",2801 2802 payload: { id: entry.id, explicitSummon: true },2803 }).catch(() => null) as { ok: boolean; data?: { totpCode?: string; totpRemaining?: number } } | null;2804 if (!r?.ok || !r.data?.totpCode) return null;2805 return { code: r.data.totpCode, remaining: r.data.totpRemaining ?? 30 };2806 }2807 let current = await fetchCode();2808 if (!current) return;28092810 trayEl = document.createElement("div");2811 trayEl.className = "sp-tray";28122813 const label = document.createElement("span");2814 label.className = "sp-tray-label";2815 label.textContent = "2FA";2816 trayEl.appendChild(label);28172818 const codeEl = document.createElement("div");2819 codeEl.className = "sp-totp-code";2820 trayEl.appendChild(codeEl);28212822 const ringNs = "http://www.w3.org/2000/svg";2823 const ring = document.createElementNS(ringNs, "svg");2824 ring.setAttribute("class", "sp-totp-ring");2825 ring.setAttribute("viewBox", "0 0 24 24");2826 const trackC = document.createElementNS(ringNs, "circle");2827 trackC.setAttribute("class", "ring-track");2828 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");2829 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");2830 const fillC = document.createElementNS(ringNs, "circle");2831 fillC.setAttribute("class", "ring-fill");2832 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");2833 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");2834 fillC.setAttribute("stroke-linecap", "round");2835 fillC.setAttribute("stroke-dasharray", "62.83");2836 ring.append(trackC, fillC);2837 trayEl.appendChild(ring);28382839 const copyBtn = document.createElement("button");2840 copyBtn.className = "sp-totp-copy";2841 copyBtn.type = "button";2842 copyBtn.textContent = "Copy";2843 trayEl.appendChild(copyBtn);28442845 function render() {2846 const { code, remaining } = current!;2847 2848 codeEl.textContent = code.length === 62849 ? `${code.slice(0, 3)} ${code.slice(3)}`2850 : code;2851 2852 2853 const fraction = (30 - remaining) / 30;2854 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));2855 }2856 render();2857 trayInterval = setInterval(async () => {2858 if (!current) return;2859 current = { code: current.code, remaining: current.remaining - 1 };2860 2861 if (current.remaining <= 0) {2862 const next = await fetchCode();2863 current = next ?? { code: current.code, remaining: 30 };2864 }2865 render();2866 }, 1000);28672868 copyBtn.addEventListener("click", async (e) => {2869 e.stopPropagation();2870 const code = current?.code;2871 if (!code) return;2872 let alreadyWritten = false;2873 try {2874 await navigator.clipboard.writeText(code);2875 alreadyWritten = true;2876 } catch (err) {2877 console.warn("[Peach] clipboard write denied:", err);2878 }2879 const resp = await chrome.runtime.sendMessage({2880 type: "COPY",2881 payload: { text: code, label: "TOTP", alreadyWritten },2882 }).catch(() => null) as { ok?: boolean } | null;2883 copyBtn.textContent = alreadyWritten || resp?.ok ? "Copied" : "Blocked";2884 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);2885 });28862887 platter.appendChild(trayEl);2888 }28892890 const editBtn = document.createElement("button");2891 editBtn.className = "sp-action-btn sp-action-edit";2892 editBtn.type = "button";2893 editBtn.title = "Edit entry";2894 editBtn.textContent = "Edit";2895 editBtn.addEventListener("click", (e) => {2896 e.stopPropagation();2897 closeTray();2898 void renderInlineEdit(entry);2899 });2900 actions.appendChild(editBtn);29012902 2903 2904 2905 if (entry.hasTotp) void openTOTPTray();29062907 platter.appendChild(actionsGroup);2908 container.appendChild(platter);2909 }29102911 291229132914291529162917 async function renderInlineEdit(summary: SelectorEntry) {2918 currentMode = "edit";2919 container.innerHTML = "";2920 const wrap = document.createElement("div");2921 wrap.className = "create-form-wrap";29222923 const head = document.createElement("div");2924 head.className = "create-head";2925 const back = document.createElement("button");2926 back.className = "back-btn";2927 back.type = "button";2928 back.title = "Cancel";2929 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>`;2930 back.addEventListener("click", () => renderSilverPlatter(summary));2931 head.appendChild(back);2932 const headLbl = document.createElement("div");2933 headLbl.className = "create-label";2934 headLbl.textContent = "Edit entry";2935 head.appendChild(headLbl);2936 wrap.appendChild(head);29372938 const body = document.createElement("div");2939 body.className = "create-body";29402941 2942 const loading = document.createElement("div");2943 loading.className = "empty-hint";2944 loading.textContent = "loading…";2945 body.appendChild(loading);2946 wrap.appendChild(body);2947 container.appendChild(wrap);29482949 const [res, idRes] = await Promise.all([2950 2951 2952 chrome.runtime.sendMessage({2953 type: "GET_ENTRY_FOR_EDIT",2954 payload: { id: summary.id },2955 }).catch(() => null),2956 chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null),2957 ]);2958 const entryRes = res as { ok: boolean; data?: { title?: string; username?: string; email?: string; password?: string; url?: string; totp?: string; notes?: string; passwordHistory?: PasswordHistoryItem[] } } | null;2959 const editIdentities: SavedIdentity[] = (idRes as { ok?: boolean; data?: SavedIdentity[] } | null)?.data ?? [];29602961 if (!entryRes || !entryRes.ok || !entryRes.data) {2962 loading.textContent = "couldn't load this entry. try again from the popup.";2963 return;2964 }2965 body.innerHTML = "";29662967 function field(label: string, value: string, type: "text" | "password" = "text"): { g: HTMLDivElement; inp: HTMLInputElement } {2968 const group = document.createElement("div");2969 group.className = "f-group";2970 const lbl = document.createElement("div");2971 lbl.className = "f-label";2972 lbl.textContent = label;2973 const input = document.createElement("input");2974 input.className = "f-input";2975 input.type = type;2976 input.value = value;2977 input.autocomplete = "off";2978 group.append(lbl, input);2979 body.appendChild(group);2980 return { g: group, inp: input };2981 }29822983 const titleInput = field("title", entryRes.data.title ?? "").inp;2984 const urlInput = field("url", entryRes.data.url ?? "").inp;2985 urlInput.classList.add("url-f");2986 const usernameLabel = (entryRes.data.username ?? "").includes("@") ? "email" : "username";2987 const { g: usernameGroup, inp: usernameInput } = field(usernameLabel, entryRes.data.username ?? "");2988 if (editIdentities.length) attachIdentitySuggestions(usernameGroup, usernameInput, editIdentities, "username");2989 const { g: emailGroup, inp: emailInput } = field("email", entryRes.data.email ?? "");2990 if (editIdentities.length) attachIdentitySuggestions(emailGroup, emailInput, editIdentities, "email");29912992 2993 const pwGroup = document.createElement("div");2994 pwGroup.className = "f-group";2995 const pwLbl = document.createElement("div");2996 pwLbl.className = "f-label";2997 pwLbl.textContent = "password";2998 const pwRow = document.createElement("div");2999 pwRow.className = "f-row";3000 const pwInput = document.createElement("input");3001 pwInput.className = "f-input";3002 pwInput.type = "password";3003 pwInput.value = entryRes.data.password ?? "";3004 pwInput.autocomplete = "new-password";3005 const revealBtn = document.createElement("button");3006 revealBtn.className = "gen-btn";3007 revealBtn.type = "button";3008 revealBtn.textContent = "show";3009 revealBtn.addEventListener("click", () => {3010 const reveal = pwInput.type === "password";3011 pwInput.type = reveal ? "text" : "password";3012 revealBtn.textContent = reveal ? "hide" : "show";3013 });3014 pwRow.append(pwInput, revealBtn);3015 const editSmartGen = createSmartGenerator({ policy: detectPasswordPolicy(), onChange: (pw) => { pwInput.value = pw; } });3016 editSmartGen.root.style.display = "none";3017 let editGenVisible = false;3018 const genToggleBtn = document.createElement("button");3019 genToggleBtn.className = "gen-toggle-link";3020 genToggleBtn.type = "button";3021 genToggleBtn.textContent = "⚙ generate";3022 genToggleBtn.addEventListener("click", () => {3023 editGenVisible = !editGenVisible;3024 pwRow.style.display = editGenVisible ? "none" : "";3025 editSmartGen.root.style.display = editGenVisible ? "" : "none";3026 genToggleBtn.textContent = editGenVisible ? "☰ hide generator" : "⚙ generate";3027 });3028 pwGroup.append(pwLbl, pwRow, genToggleBtn, editSmartGen.root);30293030 3031 3032 3033 3034 3035 const history = entryRes.data.passwordHistory ?? [];3036 {3037 const histList = document.createElement("div");3038 histList.className = "pw-history";3039 histList.style.display = "none";3040 let histVisible = false;30413042 const restLabel = history.length ? `↺ history (${history.length})` : "↺ history";3043 const histToggle = document.createElement("button");3044 histToggle.className = "gen-toggle-link";3045 histToggle.type = "button";3046 histToggle.textContent = restLabel;3047 histToggle.addEventListener("click", () => {3048 histVisible = !histVisible;3049 histList.style.display = histVisible ? "" : "none";3050 histToggle.textContent = histVisible ? "↺ hide history" : restLabel;3051 });30523053 if (history.length === 0) {3054 const empty = document.createElement("div");3055 empty.className = "pw-history-empty";3056 empty.textContent = "No earlier passwords yet. When you change this password, the old one is saved here so you can copy or restore it.";3057 histList.appendChild(empty);3058 }30593060 for (const item of history) {3061 const row = document.createElement("div");3062 row.className = "pw-history-row";3063 const main = document.createElement("div");3064 main.className = "pw-history-main";3065 const pwText = document.createElement("div");3066 pwText.className = "pw-history-pw";3067 let revealed = false;3068 const mask = "••••••••••••";3069 pwText.textContent = mask;3070 const age = document.createElement("div");3071 age.className = "pw-history-age";3072 age.textContent = timeSince(item.changedAt);3073 main.append(pwText, age);30743075 const acts = document.createElement("div");3076 acts.className = "pw-history-actions";3077 const revealBtn2 = document.createElement("button");3078 revealBtn2.className = "pw-history-btn";3079 revealBtn2.type = "button";3080 revealBtn2.textContent = "show";3081 revealBtn2.addEventListener("click", () => {3082 revealed = !revealed;3083 pwText.textContent = revealed ? item.password : mask;3084 revealBtn2.textContent = revealed ? "hide" : "show";3085 });3086 const copyBtn2 = document.createElement("button");3087 copyBtn2.className = "pw-history-btn";3088 copyBtn2.type = "button";3089 copyBtn2.textContent = "copy";3090 copyBtn2.addEventListener("click", async () => {3091 let alreadyWritten = false;3092 try {3093 await navigator.clipboard.writeText(item.password);3094 alreadyWritten = true;3095 } catch (err) {3096 console.warn("[Peach] clipboard write denied:", err);3097 }3098 await chrome.runtime.sendMessage({3099 type: "COPY",3100 payload: { text: item.password, label: "Password", alreadyWritten },3101 }).catch(() => null);3102 copyBtn2.textContent = "copied";3103 setTimeout(() => { if (copyBtn2.isConnected) copyBtn2.textContent = "copy"; }, 1200);3104 });3105 const useBtn = document.createElement("button");3106 useBtn.className = "pw-history-btn";3107 useBtn.type = "button";3108 useBtn.textContent = "use";3109 useBtn.title = "Use this password again";3110 useBtn.addEventListener("click", () => {3111 pwInput.value = item.password;3112 pwInput.type = "text";3113 revealBtn.textContent = "hide";3114 if (editGenVisible) {3115 editGenVisible = false;3116 pwRow.style.display = "";3117 editSmartGen.root.style.display = "none";3118 genToggleBtn.textContent = "⚙ generate";3119 }3120 histVisible = false;3121 histList.style.display = "none";3122 histToggle.textContent = `↺ history (${history.length})`;3123 });3124 acts.append(revealBtn2, copyBtn2, useBtn);3125 row.append(main, acts);3126 histList.appendChild(row);3127 }31283129 pwGroup.append(histToggle, histList);3130 }31313132 body.appendChild(pwGroup);31333134 const totpInput = field("2fa secret", entryRes.data.totp ?? "").inp;31353136 3137 const notesGroup = document.createElement("div");3138 notesGroup.className = "f-group";3139 const notesLbl = document.createElement("div");3140 notesLbl.className = "f-label";3141 notesLbl.textContent = "notes";3142 const notesInput = document.createElement("textarea");3143 notesInput.className = "f-input";3144 notesInput.rows = 3;3145 notesInput.value = entryRes.data.notes ?? "";3146 notesInput.style.fontFamily = "Inter, sans-serif";3147 notesInput.style.resize = "vertical";3148 notesGroup.append(notesLbl, notesInput);3149 body.appendChild(notesGroup);31503151 3152 const actionsRow = document.createElement("div");3153 actionsRow.className = "f-row";3154 const saveBtn = document.createElement("button");3155 saveBtn.className = "save-btn";3156 saveBtn.type = "button";3157 saveBtn.textContent = "Save changes";3158 saveBtn.style.flex = "1";3159 const cancelBtn = document.createElement("button");3160 cancelBtn.className = "gen-btn";3161 cancelBtn.type = "button";3162 cancelBtn.textContent = "cancel";3163 cancelBtn.addEventListener("click", () => renderSilverPlatter(summary));3164 actionsRow.append(saveBtn, cancelBtn);3165 body.appendChild(actionsRow);31663167 saveBtn.addEventListener("click", async () => {3168 saveBtn.disabled = true;3169 const originalLabel = saveBtn.textContent;3170 saveBtn.textContent = "Saving…";3171 const patch: Record<string, unknown> = {3172 title: titleInput.value.trim(),3173 url: urlInput.value.trim(),3174 username: usernameInput.value.trim(),3175 email: emailInput.value.trim() || undefined,3176 password: pwInput.value,3177 totp: totpInput.value.trim(),3178 notes: notesInput.value,3179 };3180 const updateRes = (await chrome.runtime.sendMessage({3181 type: "UPDATE_ENTRY",3182 payload: { id: summary.id, patch },3183 }).catch(() => null)) as { ok: boolean; error?: string } | null;3184 if (!updateRes?.ok) {3185 saveBtn.disabled = false;3186 saveBtn.textContent = originalLabel;3187 const err = document.createElement("div");3188 err.className = "empty-hint";3189 err.style.color = "color-mix(in srgb, #e55 70%, transparent)";3190 err.textContent = updateRes?.error ?? "Couldn't save. Try the popup.";3191 body.appendChild(err);3192 setTimeout(() => err.remove(), 3000);3193 return;3194 }3195 3196 const refreshed: SelectorEntry = {3197 id: summary.id,3198 title: patch.title as string,3199 username: patch.username as string,3200 email: patch.email as string | undefined,3201 url: patch.url as string,3202 hasTotp: Boolean(patch.totp),3203 hasPasskey: summary.hasPasskey,3204 };3205 renderSilverPlatter(refreshed);3206 });32073208 queueMicrotask(() => titleInput.focus());3209 }32103211 function renderLocked() {3212 container.innerHTML = "";32133214 3215 const domHdr = document.createElement("div");3216 domHdr.className = "domain-header";3217 const eyebrow = document.createElement("div");3218 eyebrow.className = "domain-eyebrow";3219 eyebrow.textContent = "vault locked";3220 const domName = document.createElement("div");3221 domName.className = "domain-name";3222 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");3223 domHdr.append(eyebrow, domName);3224 container.appendChild(domHdr);32253226 const wrap = document.createElement("div");3227 wrap.className = "locked-wrap";32283229 3230 const head = document.createElement("div"); head.className = "locked-head";3231 const icon = document.createElement("div"); icon.className = "locked-icon";3232 icon.innerHTML = keySvgStr();3233 const title = document.createElement("div"); title.className = "locked-title";3234 title.textContent = "unlock to autofill";3235 head.append(icon, title);32363237 3238 3239 3240 3241 3242 3243 3244 3245 const sub = document.createElement("div");3246 sub.className = "locked-sub";3247 sub.textContent = "Unlock in Peach — your master password is never typed on this page.";32483249 const openBtn = document.createElement("button");3250 openBtn.className = "locked-btn";3251 openBtn.type = "button";3252 openBtn.textContent = "Open Peach to unlock";3253 openBtn.addEventListener("click", () => {3254 chrome.runtime.sendMessage({ type: "OPEN_POPUP" }).catch(() => {});3255 openBtn.textContent = "opening Peach…";3256 openBtn.disabled = true;3257 3258 3259 setTimeout(() => {3260 if (openBtn.isConnected) {3261 openBtn.textContent = "Open Peach to unlock";3262 openBtn.disabled = false;3263 }3264 }, 2500);3265 });32663267 wrap.append(head, sub, openBtn);3268 container.appendChild(wrap);3269 }32703271 function handleKeyDown(e: KeyboardEvent) {3272 3273 3274 3275 if (!e.isTrusted) return;3276 if (options.locked) {3277 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3278 return;3279 }3280 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter" || currentMode === "identity-helper" || currentMode === "edit") {3281 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3282 return;3283 }3284 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");3285 const count = allEntryEls.length;3286 const activeElement = shadow.activeElement ?? document.activeElement;3287 if (activeElement === searchInput) {3288 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }3289 if (count === 0) return;3290 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;3291 }3292 if (count === 0) return;3293 switch (e.key) {3294 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;3295 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;3296 case "Enter": {3297 if (focusedIndex >= 0) {3298 const el = allEntryEls[focusedIndex] as HTMLElement;3299 const id = el.dataset.id;3300 if (id) { onSelect(id); dismiss(); }3301 else if (el.classList.contains("is-new") || !id) {3302 3303 showCreateForm();3304 }3305 }3306 break;3307 }3308 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;3309 }3310 }33113312 document.addEventListener("keydown", handleKeyDown);33133314 3315 3316 3317 3318 3319 const silverPlatterTypeHandler = (e: KeyboardEvent) => {3320 if (!e.isTrusted) return; 3321 if (currentMode !== "silver-platter") return;3322 if (e.ctrlKey || e.metaKey || e.altKey) return;3323 if (e.key.length !== 1) return;3324 e.preventDefault();3325 e.stopImmediatePropagation();3326 query = e.key;3327 3328 void ensureSearchableLoaded(renderList);3329 renderList();3330 };3331 document.addEventListener("keydown", silverPlatterTypeHandler, true);3332 cleanupHooks.push(() => document.removeEventListener("keydown", silverPlatterTypeHandler, true));33333334 const identityHelperTypeHandler = (e: KeyboardEvent) => {3335 if (!e.isTrusted) return; 3336 if (currentMode !== "identity-helper") return;3337 if (e.ctrlKey || e.metaKey || e.altKey) return;3338 const active = (shadow.activeElement ?? document.activeElement) as Element | null;3339 if (active === searchInput) return;3340 if (3341 active instanceof HTMLInputElement ||3342 active instanceof HTMLTextAreaElement ||3343 (active instanceof HTMLElement && active.isContentEditable)3344 ) {3345 return;3346 }3347 if (e.key.length !== 1) return;3348 e.preventDefault();3349 e.stopImmediatePropagation();3350 query = e.key;3351 renderIdentityHelper();3352 };3353 document.addEventListener("keydown", identityHelperTypeHandler, true);3354 cleanupHooks.push(() => document.removeEventListener("keydown", identityHelperTypeHandler, true));33553356 (modalAncestor ?? document.body).appendChild(host);33573358 3359 3360 3361 3362 3363 3364 3365 function establishesFixedContainingBlock(s: CSSStyleDeclaration): boolean {3366 const has = (p: string) => { const v = s.getPropertyValue(p); return v !== "" && v !== "none"; };3367 return (3368 has("transform") || has("perspective") || has("translate") ||3369 has("rotate") || has("scale") || has("filter") ||3370 has("backdrop-filter") || has("-webkit-backdrop-filter") ||3371 /\b(transform|perspective|filter)\b/.test(s.getPropertyValue("will-change")) ||3372 /\b(paint|layout|strict|content)\b/.test(s.getPropertyValue("contain"))3373 );3374 }3375 function containingBlockOffset(): { dx: number; dy: number } {3376 let el: HTMLElement | null = host.parentElement;3377 while (el && el !== document.documentElement) {3378 const s = getComputedStyle(el);3379 if (establishesFixedContainingBlock(s)) {3380 const r = el.getBoundingClientRect();3381 return {3382 dx: r.left + (parseFloat(s.borderLeftWidth) || 0),3383 dy: r.top + (parseFloat(s.borderTopWidth) || 0),3384 };3385 }3386 el = el.parentElement;3387 }3388 return { dx: 0, dy: 0 };3389 }33903391 function updatePosition() {3392 const r = anchorEl.getBoundingClientRect();3393 const { dx, dy } = containingBlockOffset();3394 host.style.top = `${r.bottom - dy + 5}px`;3395 host.style.left = `${r.left - dx}px`;3396 host.style.width = `${popupWidth}px`;3397 }3398 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);3399 updatePosition();34003401 3402 3403 3404 if (modalAncestor) {3405 modalAncestor.addEventListener("scroll", updatePosition, { passive: true });3406 cleanupHooks.push(() => modalAncestor.removeEventListener("scroll", updatePosition));3407 }34083409 3410 3411 3412 const removalObserver = new MutationObserver(() => {3413 if (!document.contains(anchorEl)) dismiss();3414 });3415 removalObserver.observe(document.body, { childList: true, subtree: true });3416 cleanupHooks.push(() => removalObserver.disconnect());34173418 if (options.locked) {3419 renderLocked();3420 } else if (options.isSignup && options.savedIdentities) {3421 3422 3423 renderIdentityHelper();3424 } else if (entries.length === 1 && !options.isSignup) {3425 3426 3427 renderSilverPlatter(entries[0]!);3428 } else {3429 renderList();3430 }34313432 let exitScheduled = false;3433 function dismiss(userAction = false) {3434 3435 3436 3437 if (exitScheduled) return;3438 exitScheduled = true;3439 3440 3441 3442 stopObserving();3443 document.removeEventListener("click", outsideClick, { capture: true });3444 document.removeEventListener("keydown", handleKeyDown);3445 for (const cleanup of cleanupHooks) {3446 try { cleanup(); } catch { }3447 }3448 if (userAction) options.onDismiss?.();3449 container.classList.add("is-exiting");3450 setTimeout(() => host.remove(), 130);3451 }34523453 function outsideClick(ev: MouseEvent) {3454 const path = ev.composedPath();3455 3456 3457 3458 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3459 }3460 3461 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);34623463 return dismiss;3464}34653466export function showTOTPSelector(3467 anchorEl: HTMLInputElement,3468 entries: SelectorEntry[],3469 onFill: (id: string) => void,3470 options: { domain?: string; onDismiss?: () => void } = {}3471): () => void {3472 const host = document.createElement("div");3473 host.style.cssText = "position: absolute; z-index: 2147483647;";3474 host.style.setProperty("--peach-accent", accentHex);3475 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));3476 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));3477 const shadow = host.attachShadow({ mode: "closed" });34783479 const style = shadow.appendChild(document.createElement("style"));3480 style.textContent = SELECTOR_STYLES;34813482 const rect = anchorEl.getBoundingClientRect();3483 host.style.top = `${rect.bottom + window.scrollY + 5}px`;3484 host.style.left = `${rect.left + window.scrollX}px`;3485 const popupWidth = Math.min(Math.max(rect.width, 300), 360);3486 host.style.width = `${popupWidth}px`;34873488 const container = shadow.appendChild(document.createElement("div"));3489 container.className = "peach-inline-popup";3490 container.setAttribute("data-theme", popupTheme);34913492 const domain = options.domain ?? location.hostname.replace(/^www\./, "");3493 const current = entries.filter((entry) => entry.hasTotp);3494 let focusedIndex = current.length === 1 ? 0 : -1;34953496 function updatePosition() {3497 const r = anchorEl.getBoundingClientRect();3498 host.style.top = `${r.bottom + window.scrollY + 5}px`;3499 host.style.left = `${r.left + window.scrollX}px`;3500 host.style.width = `${popupWidth}px`;3501 }35023503 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);35043505 function choose(entry: SelectorEntry) {3506 onFill(entry.id);3507 dismiss();3508 }35093510 function render() {3511 container.innerHTML = "";35123513 const domHdr = document.createElement("div");3514 domHdr.className = "domain-header";3515 const headText = document.createElement("div");3516 headText.className = "domain-header-text";3517 const eyebrow = document.createElement("div");3518 eyebrow.className = "domain-eyebrow";3519 eyebrow.textContent = "2FA code";3520 const domName = document.createElement("div");3521 domName.className = "domain-name";3522 domName.textContent = domain;3523 headText.append(eyebrow, domName);3524 domHdr.appendChild(headText);3525 container.appendChild(domHdr);35263527 if (current.length === 1) {3528 const entry = current[0]!;3529 const platter = document.createElement("div");3530 platter.className = "silver-platter";35313532 const title = document.createElement("div");3533 title.className = "sp-title";3534 title.textContent = entry.title;3535 platter.appendChild(title);35363537 const sub = document.createElement("div");3538 sub.className = "sp-username";3539 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3540 platter.appendChild(sub);35413542 const fill = document.createElement("button");3543 fill.className = "sp-fill-btn";3544 fill.type = "button";3545 fill.textContent = "Fill 2FA code";3546 fill.addEventListener("click", () => choose(entry));3547 platter.appendChild(fill);35483549 container.appendChild(platter);3550 requestAnimationFrame(() => fill.focus({ preventScroll: true }));3551 return;3552 }35533554 const results = document.createElement("div");3555 results.className = "results";3556 container.appendChild(results);35573558 for (let i = 0; i < current.length; i++) {3559 const entry = current[i]!;3560 const row = document.createElement("div");3561 row.className = `entry${i === focusedIndex ? " focused" : ""}`;3562 row.dataset.index = String(i);3563 row.setAttribute("tabindex", "-1");35643565 const icon = document.createElement("div");3566 icon.className = "entry-icon";3567 icon.innerHTML = keySvgStr();3568 row.appendChild(icon);3569 const info = document.createElement("div");3570 info.className = "entry-info";3571 const title = document.createElement("div");3572 title.className = "entry-title";3573 title.textContent = entry.title;3574 const sub = document.createElement("div");3575 sub.className = "entry-sub";3576 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3577 info.append(title, sub);3578 row.appendChild(info);35793580 const badge = document.createElement("span");3581 badge.className = "totp-badge";3582 badge.textContent = "Fill";3583 row.appendChild(badge);3584 row.addEventListener("click", () => choose(entry));3585 results.appendChild(row);3586 }3587 }35883589 function setFocused(index: number) {3590 focusedIndex = index;3591 container.querySelectorAll(".entry").forEach((el, i) => {3592 el.classList.toggle("focused", i === focusedIndex);3593 if (i === focusedIndex) (el as HTMLElement).scrollIntoView({ block: "nearest" });3594 });3595 }35963597 function handleKeyDown(e: KeyboardEvent) {3598 if (e.key === "Escape" || e.key === "Tab") {3599 e.preventDefault();3600 dismiss(true);3601 return;3602 }3603 if (current.length === 0) return;3604 if (e.key === "Enter") {3605 e.preventDefault();3606 choose(current[Math.max(0, focusedIndex)]!);3607 return;3608 }3609 if (current.length === 1) return;3610 if (e.key === "ArrowDown") {3611 e.preventDefault();3612 setFocused(focusedIndex < current.length - 1 ? focusedIndex + 1 : 0);3613 } else if (e.key === "ArrowUp") {3614 e.preventDefault();3615 setFocused(focusedIndex > 0 ? focusedIndex - 1 : current.length - 1);3616 }3617 }36183619 let exitScheduled = false;3620 function dismiss(userAction = false) {3621 if (exitScheduled) return;3622 exitScheduled = true;3623 stopObserving();3624 document.removeEventListener("click", outsideClick, { capture: true });3625 document.removeEventListener("keydown", handleKeyDown);3626 if (userAction) options.onDismiss?.();3627 container.classList.add("is-exiting");3628 setTimeout(() => host.remove(), 130);3629 }36303631 function outsideClick(ev: MouseEvent) {3632 const path = ev.composedPath();3633 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3634 }36353636 document.addEventListener("keydown", handleKeyDown);3637 document.body.appendChild(host);3638 render();3639 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);36403641 return dismiss;3642}36433644export interface CardSelectorEntry {3645 id: string;3646 title: string;3647 cardholderName?: string;3648 last4?: string;3649 expMonth?: string;3650 expYear?: string;3651 network?: import("../shared/types").CardNetwork;3652}36533654interface CardSelectorOptions {3655 domain?: string;3656 3657 onFill: (entry: import("../shared/types").VaultEntry) => void;3658 3659 onDismiss?: () => void;3660}36613662366336643665366636673668export function showPasskeyProGate(): void {3669 const domain = location.hostname.replace(/^www\./, "");36703671 const host = document.createElement("div");3672 host.setAttribute("data-peach-passkey-gate", "1");3673 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";3674 const shadow = host.attachShadow({ mode: "closed" });36753676 const style = document.createElement("style");3677 style.textContent = `3678 :host { all: initial; }3679 .gate-wrap {3680 position: fixed;3681 top: 50%;3682 left: 50%;3683 transform: translate(-50%, -50%);3684 width: 280px;3685 pointer-events: auto;3686 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;3687 }3688 @keyframes g-in {3689 from { opacity:0; transform: translate(-50%,-52%); }3690 to { opacity:1; transform: translate(-50%,-50%); }3691 }3692 .gate-wrap.is-exiting {3693 animation: g-out 130ms ease-in forwards;3694 }3695 @keyframes g-out {3696 to { opacity:0; transform: translate(-50%,-52%); }3697 }3698 .gate {3699 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3700 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3701 border-radius: 12px;3702 box-shadow: 0 12px 48px rgba(0,0,0,0.75);3703 padding: 16px;3704 font-family: Inter, sans-serif;3705 }3706 .gate-header {3707 display: flex;3708 align-items: center;3709 gap: 7px;3710 margin-bottom: 12px;3711 padding-bottom: 10px;3712 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3713 }3714 .gate-domain {3715 font-size: 12px;3716 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3717 font-family: "IBM Plex Mono", monospace;3718 overflow: hidden;3719 text-overflow: ellipsis;3720 white-space: nowrap;3721 }3722 .gate-title {3723 font-size: 14px;3724 font-weight: 500;3725 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3726 margin-bottom: 6px;3727 }3728 .gate-subtitle {3729 font-size: 12px;3730 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};3731 line-height: 1.5;3732 margin-bottom: 14px;3733 }3734 .gate-btn {3735 display: block;3736 width: 100%;3737 padding: 9px 0;3738 border-radius: 8px;3739 border: none;3740 cursor: pointer;3741 font-size: 13px;3742 font-family: Inter, sans-serif;3743 font-weight: 500;3744 transition: opacity 100ms;3745 margin-bottom: 7px;3746 text-align: center;3747 }3748 .gate-btn:last-child { margin-bottom: 0; }3749 .gate-btn--accent {3750 background: ${accentHex};3751 color: #fff;3752 }3753 .gate-btn--accent:hover { opacity: 0.88; }3754 .gate-btn--ghost {3755 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3756 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};3757 }3758 .gate-btn--ghost:hover { opacity: 0.75; }3759 .backdrop {3760 position: fixed;3761 inset: 0;3762 pointer-events: auto;3763 }3764 `;3765 shadow.appendChild(style);37663767 const backdrop = document.createElement("div");3768 backdrop.className = "backdrop";3769 shadow.appendChild(backdrop);37703771 const wrap = document.createElement("div");3772 wrap.className = "gate-wrap";3773 shadow.appendChild(wrap);37743775 const gate = document.createElement("div");3776 gate.className = "gate";3777 wrap.appendChild(gate);37783779 3780 const header = document.createElement("div");3781 header.className = "gate-header";3782 const icon = document.createElement("span");3783 icon.innerHTML = peachIconSvg();3784 const domainEl = document.createElement("span");3785 domainEl.className = "gate-domain";3786 domainEl.textContent = domain;3787 header.append(icon, domainEl);3788 gate.appendChild(header);37893790 const title = document.createElement("div");3791 title.className = "gate-title";3792 title.textContent = "Passkeys are part of Peach Pro.";3793 gate.appendChild(title);37943795 const subtitle = document.createElement("div");3796 subtitle.className = "gate-subtitle";3797 subtitle.textContent = "Store and use passkeys across all your devices, securely.";3798 gate.appendChild(subtitle);37993800 const upgradeBtn = document.createElement("button");3801 upgradeBtn.className = "gate-btn gate-btn--accent";3802 upgradeBtn.type = "button";3803 upgradeBtn.textContent = "Upgrade to Pro";3804 gate.appendChild(upgradeBtn);38053806 const laterBtn = document.createElement("button");3807 laterBtn.className = "gate-btn gate-btn--ghost";3808 laterBtn.type = "button";3809 laterBtn.textContent = "Not right now";3810 gate.appendChild(laterBtn);38113812 function dismiss() {3813 wrap.classList.add("is-exiting");3814 setTimeout(() => host.remove(), 130);3815 }38163817 upgradeBtn.addEventListener("click", () => {3818 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});3819 dismiss();3820 });3821 laterBtn.addEventListener("click", dismiss);3822 backdrop.addEventListener("click", dismiss);38233824 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {3825 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }3826 });38273828 document.body.appendChild(host);3829}383038313832383338343835383638373838export function showPasskeyRegistrationOffer(args: {3839 rpId: string;3840 rpName?: string;3841 userName?: string;3842 userDisplayName?: string;3843}): void {3844 const domain = location.hostname.replace(/^www\./, "");3845 const displayName = args.rpName || domain;38463847 const host = document.createElement("div");3848 host.setAttribute("data-peach-passkey-offer", "1");3849 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";3850 const shadow = host.attachShadow({ mode: "closed" });38513852 const style = document.createElement("style");3853 style.textContent = `3854 :host { all: initial; }3855 .offer-wrap {3856 position: fixed;3857 top: 16px;3858 right: 16px;3859 width: 320px;3860 pointer-events: auto;3861 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;3862 }3863 @keyframes o-in {3864 from { opacity: 0; transform: translateY(-6px); }3865 to { opacity: 1; transform: translateY(0); }3866 }3867 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }3868 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }3869 .offer {3870 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3871 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3872 border-radius: 12px;3873 box-shadow: 0 12px 48px rgba(0,0,0,0.55);3874 padding: 14px 14px 12px;3875 font-family: Inter, sans-serif;3876 position: relative;3877 }3878 .offer-close {3879 position: absolute;3880 top: 8px;3881 right: 8px;3882 width: 18px;3883 height: 18px;3884 border: none;3885 background: transparent;3886 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};3887 cursor: pointer;3888 font-size: 14px;3889 line-height: 1;3890 padding: 0;3891 }3892 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }3893 .offer-header {3894 display: flex;3895 align-items: center;3896 gap: 7px;3897 margin-bottom: 8px;3898 padding-bottom: 8px;3899 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3900 }3901 .offer-domain {3902 font-size: 12px;3903 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3904 font-family: "IBM Plex Mono", monospace;3905 overflow: hidden;3906 text-overflow: ellipsis;3907 white-space: nowrap;3908 }3909 .offer-title {3910 font-size: 13px;3911 font-weight: 500;3912 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3913 margin-bottom: 4px;3914 }3915 .offer-subtitle {3916 font-size: 11px;3917 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};3918 line-height: 1.45;3919 margin-bottom: 12px;3920 }3921 .offer-btn {3922 display: block;3923 width: 100%;3924 padding: 8px 0;3925 border-radius: 7px;3926 border: none;3927 cursor: pointer;3928 font-size: 12px;3929 font-family: Inter, sans-serif;3930 font-weight: 500;3931 transition: opacity 100ms;3932 margin-bottom: 6px;3933 text-align: center;3934 }3935 .offer-btn:last-child { margin-bottom: 0; }3936 .offer-btn--accent { background: ${accentHex}; color: #fff; }3937 .offer-btn--accent:hover { opacity: 0.88; }3938 .offer-btn--ghost {3939 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3940 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};3941 }3942 .offer-btn--ghost:hover { opacity: 0.75; }3943 `;3944 shadow.appendChild(style);39453946 const wrap = document.createElement("div");3947 wrap.className = "offer-wrap";3948 shadow.appendChild(wrap);39493950 const offer = document.createElement("div");3951 offer.className = "offer";3952 wrap.appendChild(offer);39533954 const closeBtn = document.createElement("button");3955 closeBtn.className = "offer-close";3956 closeBtn.type = "button";3957 closeBtn.setAttribute("aria-label", "Dismiss");3958 closeBtn.textContent = "✕";3959 offer.appendChild(closeBtn);39603961 const header = document.createElement("div");3962 header.className = "offer-header";3963 const icon = document.createElement("span");3964 icon.innerHTML = peachIconSvg();3965 const domainEl = document.createElement("span");3966 domainEl.className = "offer-domain";3967 domainEl.textContent = domain;3968 header.append(icon, domainEl);3969 offer.appendChild(header);39703971 const title = document.createElement("div");3972 title.className = "offer-title";3973 title.textContent = "This site supports passkeys.";3974 offer.appendChild(title);39753976 const subtitle = document.createElement("div");3977 subtitle.className = "offer-subtitle";3978 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";3979 offer.appendChild(subtitle);39803981 const saveBtn = document.createElement("button");3982 saveBtn.className = "offer-btn offer-btn--accent";3983 saveBtn.type = "button";3984 saveBtn.textContent = "Save passkey to Peach";3985 offer.appendChild(saveBtn);39863987 const laterBtn = document.createElement("button");3988 laterBtn.className = "offer-btn offer-btn--ghost";3989 laterBtn.type = "button";3990 laterBtn.textContent = "Not now";3991 offer.appendChild(laterBtn);39923993 function dismiss(decline: boolean) {3994 if (decline) {3995 chrome.runtime.sendMessage({3996 type: "PASSKEY_OFFER_DECLINE",3997 payload: { domain },3998 }).catch(() => {});3999 }4000 wrap.classList.add("is-exiting");4001 setTimeout(() => host.remove(), 130);4002 }40034004 saveBtn.addEventListener("click", () => {4005 chrome.runtime.sendMessage({4006 type: "INITIATE_FIREFOX_PASSKEY_CREATE",4007 payload: {4008 rpId: args.rpId,4009 rpName: args.rpName ?? displayName,4010 userName: args.userName ?? "user",4011 userDisplayName: args.userDisplayName,4012 },4013 }).catch(() => {});4014 dismiss(false);4015 });4016 laterBtn.addEventListener("click", () => dismiss(true));4017 closeBtn.addEventListener("click", () => dismiss(true));40184019 document.body.appendChild(host);4020}4021402240234024402540264027402840294030export function showPasskeyAvailable(args: {4031 rpId: string;4032 userName: string;4033 fromBrowser?: "chrome" | "firefox";4034 currentBrowser?: "chrome" | "firefox" | "unknown";4035}): void {4036 const domain = location.hostname.replace(/^www\./, "");4037 const crossBrowser =4038 args.fromBrowser &&4039 args.currentBrowser &&4040 args.currentBrowser !== "unknown" &&4041 args.fromBrowser !== args.currentBrowser;40424043 const host = document.createElement("div");4044 host.setAttribute("data-peach-passkey-available", "1");4045 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";4046 const shadow = host.attachShadow({ mode: "closed" });40474048 const style = document.createElement("style");4049 style.textContent = `4050 :host { all: initial; }4051 .avail-wrap {4052 position: fixed;4053 top: 16px;4054 right: 16px;4055 width: 320px;4056 pointer-events: auto;4057 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;4058 }4059 @keyframes a-in {4060 from { opacity: 0; transform: translateY(-6px); }4061 to { opacity: 1; transform: translateY(0); }4062 }4063 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }4064 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }4065 .avail {4066 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};4067 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};4068 border-radius: 12px;4069 box-shadow: 0 12px 48px rgba(0,0,0,0.55);4070 padding: 14px 14px 12px;4071 font-family: Inter, sans-serif;4072 position: relative;4073 }4074 .avail-close {4075 position: absolute;4076 top: 8px;4077 right: 8px;4078 width: 18px;4079 height: 18px;4080 border: none;4081 background: transparent;4082 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};4083 cursor: pointer;4084 font-size: 14px;4085 line-height: 1;4086 padding: 0;4087 }4088 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }4089 .avail-header {4090 display: flex;4091 align-items: center;4092 gap: 7px;4093 margin-bottom: 8px;4094 padding-bottom: 8px;4095 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};4096 }4097 .avail-domain {4098 font-size: 12px;4099 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};4100 font-family: "IBM Plex Mono", monospace;4101 }4102 .avail-label {4103 font-size: 9px;4104 letter-spacing: 0.16em;4105 text-transform: uppercase;4106 color: ${accentHex};4107 margin-bottom: 4px;4108 }4109 .avail-user {4110 font-size: 13px;4111 font-weight: 500;4112 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};4113 margin-bottom: 12px;4114 overflow: hidden;4115 text-overflow: ellipsis;4116 white-space: nowrap;4117 }4118 .avail-cross {4119 font-size: 11px;4120 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};4121 line-height: 1.45;4122 margin-bottom: 12px;4123 padding: 8px;4124 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};4125 border-radius: 6px;4126 }4127 .avail-btn {4128 display: block;4129 width: 100%;4130 padding: 8px 0;4131 border-radius: 7px;4132 border: none;4133 cursor: pointer;4134 font-size: 12px;4135 font-family: Inter, sans-serif;4136 font-weight: 500;4137 transition: opacity 100ms;4138 margin-bottom: 6px;4139 text-align: center;4140 }4141 .avail-btn:last-child { margin-bottom: 0; }4142 .avail-btn--accent { background: ${accentHex}; color: #fff; }4143 .avail-btn--accent:hover { opacity: 0.88; }4144 .avail-btn--ghost {4145 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};4146 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};4147 }4148 .avail-btn--ghost:hover { opacity: 0.75; }4149 `;4150 shadow.appendChild(style);41514152 const wrap = document.createElement("div");4153 wrap.className = "avail-wrap";4154 shadow.appendChild(wrap);41554156 const avail = document.createElement("div");4157 avail.className = "avail";4158 wrap.appendChild(avail);41594160 const closeBtn = document.createElement("button");4161 closeBtn.className = "avail-close";4162 closeBtn.type = "button";4163 closeBtn.setAttribute("aria-label", "Dismiss");4164 closeBtn.textContent = "✕";4165 avail.appendChild(closeBtn);41664167 const header = document.createElement("div");4168 header.className = "avail-header";4169 const icon = document.createElement("span");4170 icon.innerHTML = peachIconSvg();4171 const domainEl = document.createElement("span");4172 domainEl.className = "avail-domain";4173 domainEl.textContent = domain;4174 header.append(icon, domainEl);4175 avail.appendChild(header);41764177 const label = document.createElement("div");4178 label.className = "avail-label";4179 label.textContent = "PASSKEY";4180 avail.appendChild(label);41814182 const user = document.createElement("div");4183 user.className = "avail-user";4184 user.textContent = args.userName;4185 avail.appendChild(user);41864187 if (crossBrowser) {4188 const cross = document.createElement("div");4189 cross.className = "avail-cross";4190 cross.textContent =4191 args.fromBrowser === "chrome"4192 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."4193 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";4194 avail.appendChild(cross);4195 }41964197 const signInBtn = document.createElement("button");4198 signInBtn.className = "avail-btn avail-btn--accent";4199 signInBtn.type = "button";4200 signInBtn.textContent = "Sign in with passkey";4201 avail.appendChild(signInBtn);42024203 const passwordBtn = document.createElement("button");4204 passwordBtn.className = "avail-btn avail-btn--ghost";4205 passwordBtn.type = "button";4206 passwordBtn.textContent = "Use password instead";4207 avail.appendChild(passwordBtn);42084209 function dismiss() {4210 wrap.classList.add("is-exiting");4211 setTimeout(() => host.remove(), 130);4212 }42134214 signInBtn.addEventListener("click", () => {4215 4216 4217 4218 4219 4220 dismiss();4221 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});4222 });4223 passwordBtn.addEventListener("click", dismiss);4224 closeBtn.addEventListener("click", dismiss);42254226 document.body.appendChild(host);4227}422842294230423142324233423442354236export function showCardSelector(4237 anchorEl: HTMLInputElement,4238 cards: CardSelectorEntry[],4239 options: CardSelectorOptions4240): () => void {4241 if (cards.length === 0) return () => {};4242 const card = cards[0]!;42434244 const host = document.createElement("div");4245 host.style.cssText = "position: absolute; z-index: 2147483647;";4246 host.style.setProperty("--peach-accent", accentHex);4247 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));4248 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));4249 const shadow = host.attachShadow({ mode: "closed" });42504251 const style = shadow.appendChild(document.createElement("style"));4252 style.textContent = SELECTOR_STYLES;42534254 const rect = anchorEl.getBoundingClientRect();4255 host.style.top = `${rect.bottom + window.scrollY + 5}px`;4256 host.style.left = `${rect.left + window.scrollX}px`;4257 host.style.width = `${Math.min(Math.max(rect.width, 320), 400)}px`;42584259 const container = shadow.appendChild(document.createElement("div"));4260 container.className = "peach-inline-popup";4261 container.setAttribute("data-theme", popupTheme);42624263 4264 const domHdr = document.createElement("div");4265 domHdr.className = "domain-header";4266 const eyebrow = document.createElement("div");4267 eyebrow.className = "domain-eyebrow";4268 eyebrow.textContent = "Pay with Peach";4269 const domName = document.createElement("div");4270 domName.className = "domain-name";4271 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");4272 domHdr.append(eyebrow, domName);4273 container.appendChild(domHdr);42744275 const platter = document.createElement("div");4276 platter.className = "card-platter";42774278 if (card.network && card.network !== "unknown") {4279 const net = document.createElement("div");4280 net.className = "cp-network";4281 net.textContent = card.network;4282 platter.appendChild(net);4283 }42844285 const nick = document.createElement("div");4286 nick.className = "cp-nickname";4287 nick.textContent = card.title;4288 platter.appendChild(nick);42894290 if (card.last4) {4291 const l4 = document.createElement("div");4292 l4.className = "cp-last4";4293 l4.textContent = `•••• ${card.last4}`;4294 platter.appendChild(l4);4295 }42964297 const meta = document.createElement("div");4298 meta.className = "cp-meta";4299 if (card.cardholderName) {4300 const name = document.createElement("span");4301 name.textContent = card.cardholderName;4302 meta.appendChild(name);4303 }4304 if (card.expMonth && card.expYear) {4305 if (card.cardholderName) {4306 const sep = document.createElement("span");4307 sep.className = "cp-meta-sep";4308 sep.textContent = "·";4309 meta.appendChild(sep);4310 }4311 const exp = document.createElement("span");4312 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;4313 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;4314 meta.appendChild(exp);4315 }4316 if (meta.childElementCount > 0) platter.appendChild(meta);43174318 const fillWrap = document.createElement("div");4319 fillWrap.className = "cp-fill-wrap";4320 const fillBtn = document.createElement("button");4321 fillBtn.className = "sp-fill-btn";4322 fillBtn.type = "button";4323 fillBtn.textContent = "Fill card details";4324 fillBtn.addEventListener("click", async () => {4325 if (fillBtn.disabled) return;4326 fillBtn.disabled = true;4327 fillBtn.classList.add("filled");4328 fillBtn.innerHTML = checkSvgStr();4329 const resp = await chrome.runtime.sendMessage({4330 type: "GET_ENTRY_FOR_FILL",4331 4332 payload: { id: card.id, explicitSummon: true },4333 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4334 if (resp?.ok && resp.data) options.onFill(resp.data);4335 setTimeout(() => dismiss(), 400);4336 });4337 fillWrap.appendChild(fillBtn);4338 platter.appendChild(fillWrap);43394340 4341 const actions = document.createElement("div");4342 actions.className = "sp-actions";43434344 function flashBtn(b: HTMLButtonElement) {4345 b.classList.add("flashed");4346 setTimeout(() => b.classList.remove("flashed"), 700);4347 }43484349 4350 function flashCopyError(b: HTMLButtonElement) {4351 const original = b.textContent ?? "";4352 b.textContent = "Blocked";4353 b.classList.add("flashed");4354 setTimeout(() => {4355 b.classList.remove("flashed");4356 if (b.isConnected) b.textContent = original;4357 }, 1200);4358 }43594360 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {4361 const resp = await chrome.runtime.sendMessage({4362 type: "GET_ENTRY_FOR_FILL",4363 4364 payload: { id: card.id, explicitSummon: true },4365 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4366 return resp?.ok && resp.data ? resp.data : null;4367 }43684369 const copyNum = document.createElement("button");4370 copyNum.className = "sp-action-btn";4371 copyNum.type = "button";4372 copyNum.title = "Copy card number";4373 copyNum.textContent = "Copy number";4374 copyNum.addEventListener("click", async (e) => {4375 e.stopPropagation();4376 const full = await fetchFullCard();4377 if (!full?.cardNumber) return;4378 let alreadyWritten = false;4379 try {4380 await navigator.clipboard.writeText(full.cardNumber);4381 alreadyWritten = true;4382 } catch (err) {4383 console.warn("[Peach] clipboard write denied:", err);4384 }4385 const resp = await chrome.runtime.sendMessage({4386 type: "COPY",4387 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },4388 }).catch(() => null) as { ok?: boolean } | null;4389 if (alreadyWritten || resp?.ok) flashBtn(copyNum);4390 else flashCopyError(copyNum);4391 });4392 actions.appendChild(copyNum);43934394 const copyCvc = document.createElement("button");4395 copyCvc.className = "sp-action-btn";4396 copyCvc.type = "button";4397 copyCvc.title = "Copy CVC";4398 copyCvc.textContent = "Copy CVC";4399 copyCvc.addEventListener("click", async (e) => {4400 e.stopPropagation();4401 const full = await fetchFullCard();4402 if (!full?.cvc) return;4403 let alreadyWritten = false;4404 try {4405 await navigator.clipboard.writeText(full.cvc);4406 alreadyWritten = true;4407 } catch (err) {4408 console.warn("[Peach] clipboard write denied:", err);4409 }4410 const resp = await chrome.runtime.sendMessage({4411 type: "COPY",4412 payload: { text: full.cvc, label: "CVC", alreadyWritten },4413 }).catch(() => null) as { ok?: boolean } | null;4414 if (alreadyWritten || resp?.ok) flashBtn(copyCvc);4415 else flashCopyError(copyCvc);4416 });4417 actions.appendChild(copyCvc);44184419 const closeBtn = document.createElement("button");4420 closeBtn.className = "sp-action-btn";4421 closeBtn.type = "button";4422 closeBtn.title = "Dismiss";4423 closeBtn.textContent = "✕";4424 closeBtn.addEventListener("click", (e) => {4425 e.stopPropagation();4426 dismiss(true);4427 });4428 actions.appendChild(closeBtn);44294430 platter.appendChild(actions);4431 container.appendChild(platter);44324433 document.body.appendChild(host);4434 function updatePosition() {4435 const r = anchorEl.getBoundingClientRect();4436 host.style.top = `${r.bottom + window.scrollY + 5}px`;4437 host.style.left = `${r.left + window.scrollX}px`;4438 }4439 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);44404441 function onKeyDown(e: KeyboardEvent) {4442 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }4443 }4444 document.addEventListener("keydown", onKeyDown);44454446 let exitScheduled = false;4447 function dismiss(userAction = false) {4448 if (exitScheduled) return;4449 exitScheduled = true;4450 stopObserving();4451 document.removeEventListener("click", outsideClick, { capture: true });4452 document.removeEventListener("keydown", onKeyDown);4453 if (userAction) options.onDismiss?.();4454 container.classList.add("is-exiting");4455 setTimeout(() => host.remove(), 130);4456 }44574458 function outsideClick(ev: MouseEvent) {4459 const path = ev.composedPath();4460 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);4461 }4462 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);44634464 return dismiss;4465}