123456789101112import { ACCENT_COLORS, type PasswordPolicy, type SavedIdentity } from "../shared/types";13import { detectHomograph, getSecurityOriginDisplay } from "../shared/domain";14import {15 DEFAULT_PASSPHRASE_WORDS,16 generatePassphrase,17 generatePassword,18 MAX_PASSPHRASE_WORDS,19 MIN_PASSPHRASE_WORDS,20 type PassphraseSeparator,21} from "../shared/generate";22import { detectPasswordPolicy } from "./policy";23import {24 filterSavedIdentities,25 identityFilterLabel,26 resolveCustomIdentityKind,27 type IdentityFilterMode,28 type IdentityKind,29} from "./identity-helper";3031let accentHex = "#E8956D";32let popupTheme: "dark" | "light" = "dark";33chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string; theme?: string } } | undefined) => {34 const data = resp?.data;35 const name = data?.accentColor ?? "peach";36 accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";37 popupTheme = data?.theme === "light" ? "light" : "dark";38});3940414243function withAlpha(hex: string, alpha: string): string {44 45 46 47 return /^#[0-9a-f]{6}$/i.test(hex) ? hex + alpha : hex;48}4950export interface SelectorEntry {51 id: string;52 title: string;53 username?: string;54 email?: string;55 url?: string;56 hasTotp?: boolean;57 58 hasPasskey?: boolean;59}6061interface SelectorOptions {62 domain?: string;63 64 securityOrigin?: string;65 66 capability?: string;67 searchableEntries?: SelectorEntry[];68 697071727374 loadSearchableEntries?: () => Promise<SelectorEntry[]>;75 76 isSignup?: boolean;77 78798081 identityKind?: IdentityKind;82 83848586 savedIdentities?: SavedIdentity[];87 8889909192 onFillFields?: (username: string, password: string, email?: string) => void;93 949596979899 hasEmailField?: boolean;100 101102103104105 locked?: boolean;106 107108109110 onUnlocked?: () => void;111 112113114115116 onDismiss?: () => void;117}118119function getDomainFromUrl(url: string): string {120 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }121}122123function keySvgStr(): string {124 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>`;125}126127function plusSvgStr(): string {128 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>`;129}130131function searchSvgStr(): 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"><circle cx="5.25" cy="5.25" r="3.5"/><path d="M7.9 7.9l2.35 2.35"/></svg>`;133}134135function backSvgStr(): 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.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 2.5L3.5 6l4 3.5"/></svg>`;137}138139function peachIconSvg(): string {140 141 142 143 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>`;144}145146function checkSvgStr(): string {147 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>`;148}149150function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {151 const { hostname, origin } = location;152 const parts = hostname.split(".");153 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",154 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",155 "oidc", "federation", "saml", "cas", "ldap"];156 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());157 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;158 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);159 return { origin, title, loginSubdomain };160}161162163164165166167export function showRememberSiteToast(168 pageOrigin: string,169 entryId: string,170): void {171 const host = document.createElement("div");172 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";173 host.style.setProperty("--peach-accent", accentHex);174 const shadow = host.attachShadow({ mode: "closed" });175176 const style = document.createElement("style");177 style.textContent = `178 .bar {179 display:flex;align-items:center;gap:10px;padding:10px 14px;180 background:#131109;border:1px solid rgba(240,232,216,0.09);181 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);182 font-family:Inter,sans-serif;white-space:nowrap;183 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;184 }185 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }186 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }187 .yes {188 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;189 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;190 }191 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }192 .no {193 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;194 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;195 }196 `;197 shadow.appendChild(style);198199 const bar = document.createElement("div"); bar.className = "bar";200 const txt = document.createElement("span"); txt.className = "txt";201 const origin = getSecurityOriginDisplay(pageOrigin);202 const warning = detectHomograph(origin) ? "⚠ Look-alike domain: " : "";203 txt.textContent = `${warning}Remember ${origin} for this entry?`;204 const yesBtn = document.createElement("button"); yesBtn.className = "yes";205 yesBtn.textContent = "Remember";206 yesBtn.addEventListener("click", async () => {207 208 209 210 await chrome.runtime.sendMessage({211 type: "REQUEST_REMEMBER_SITE",212 payload: { id: entryId, url: location.origin },213 });214 host.remove();215 });216 const noBtn = document.createElement("button"); noBtn.className = "no";217 noBtn.textContent = "Not now";218 noBtn.addEventListener("click", () => host.remove());219 bar.append(txt, yesBtn, noBtn);220 shadow.appendChild(bar);221 document.body.appendChild(host);222 setTimeout(() => host.remove(), 9000);223}224225226227let totpToastDismissTimer: ReturnType<typeof setTimeout> | null = null;228229230231232233234235236export function showTotpCopyToast(secondsUntilClear: number): void {237 document.getElementById("peach-totp-toast")?.remove();238 if (totpToastDismissTimer) {239 clearTimeout(totpToastDismissTimer);240 totpToastDismissTimer = null;241 }242243 const toast = document.createElement("div");244 toast.id = "peach-totp-toast";245 toast.className = "peach-totp-toast";246 toast.setAttribute("data-peach-toast", "totp-copy");247 toast.setAttribute("aria-live", "polite");248249 250 Object.assign(toast.style, {251 position: "fixed",252 top: "16px",253 right: "16px",254 zIndex: "2147483647",255 padding: "10px 14px",256 borderRadius: "8px",257 background: "rgba(30, 20, 10, 0.92)",258 border: "1px solid rgba(210, 140, 60, 0.3)",259 color: "#e8d5b0",260 fontFamily: '"IBM Plex Mono", monospace',261 fontSize: "11px",262 lineHeight: "1.5",263 boxShadow: "0 4px 24px rgba(0,0,0,0.4)",264 pointerEvents: "none",265 });266267 268 269 toast.innerHTML = `270 <div style="font-style: italic; font-family: 'Cormorant Garamond', serif; font-size: 13px; margin-bottom: 2px;">271 2FA code copied272 </div>273 <div style="opacity: 0.6; font-size: 10px;">274 clears in ${secondsUntilClear}s275 </div>276 `;277278 document.body.appendChild(toast);279280 totpToastDismissTimer = setTimeout(() => {281 totpToastDismissTimer = null;282 toast.style.transition = "opacity 0.3s ease";283 toast.style.opacity = "0";284 setTimeout(() => toast.remove(), 300);285 }, 3000);286}287288interface SmartGenerator {289 root: HTMLElement;290 getPassword: () => string;291 292 copy: () => Promise<void>;293}294295interface SmartGeneratorOptions {296 297 policy: PasswordPolicy;298 299300301302303 initialPassword?: string;304 305 onChange?: (pw: string) => void;306}307308309310311312313314315export function createSmartGenerator(opts: SmartGeneratorOptions): SmartGenerator {316 const policy = opts.policy ?? {};317 let mode: "password" | "passphrase" = "password";318 let wordCount = DEFAULT_PASSPHRASE_WORDS;319 let separator: PassphraseSeparator = "-";320321 322 let length = policy.maxLength ?? 64;323 if (policy.minLength) length = Math.max(length, policy.minLength);324 if (length < 8) length = 8;325 if (length > 256) length = 256;326327 let upper = true;328 let lower = true;329 let numbers = true;330 let symbols = !policy.forbidsSymbols;331 if (policy.requiresSymbols) symbols = true;332 const forbiddenChars = policy.forbiddenChars ?? "";333334 let password = "";335336 const root = document.createElement("div");337 root.className = "pwgen-block";338339 const modePills = root.appendChild(document.createElement("div"));340 modePills.className = "pwgen-pills";341 modePills.setAttribute("aria-label", "Generator type");342 const passwordModeBtn = modePills.appendChild(document.createElement("button"));343 passwordModeBtn.type = "button";344 passwordModeBtn.className = "pwgen-pill is-on";345 passwordModeBtn.textContent = "password";346 passwordModeBtn.setAttribute("aria-pressed", "true");347 const passphraseModeBtn = modePills.appendChild(document.createElement("button"));348 passphraseModeBtn.type = "button";349 passphraseModeBtn.className = "pwgen-pill";350 passphraseModeBtn.textContent = "passphrase";351 passphraseModeBtn.setAttribute("aria-pressed", "false");352353 const displayRow = root.appendChild(document.createElement("div"));354 displayRow.className = "pwgen-display-row";355 const output = displayRow.appendChild(document.createElement("input")) as HTMLInputElement;356 output.type = "text";357 output.className = "pwgen-output";358 output.setAttribute("autocomplete", "off");359 output.setAttribute("spellcheck", "false");360 output.setAttribute("autocorrect", "off");361 output.setAttribute("autocapitalize", "off");362363 const regenBtn = displayRow.appendChild(document.createElement("button"));364 regenBtn.className = "pwgen-icon-btn";365 regenBtn.type = "button";366 regenBtn.setAttribute("aria-label", "Regenerate password");367 regenBtn.title = "Regenerate";368 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>`;369370 const copyBtn = displayRow.appendChild(document.createElement("button"));371 copyBtn.className = "pwgen-icon-btn";372 copyBtn.type = "button";373 copyBtn.setAttribute("aria-label", "Copy password");374 copyBtn.title = "Copy";375 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>`;376377 const controls = root.appendChild(document.createElement("div"));378 controls.className = "pwgen-controls-row";379 const slider = controls.appendChild(document.createElement("input"));380 slider.type = "range";381 slider.className = "pwgen-slider";382 slider.min = "8";383 slider.max = "256";384 slider.value = String(length);385 if (policy.minLength) slider.min = String(Math.max(8, policy.minLength));386 if (policy.maxLength) slider.max = String(Math.min(256, policy.maxLength));387 slider.setAttribute("aria-label", "Password length");388 const amountLabel = controls.appendChild(document.createElement("span"));389 amountLabel.className = "pwgen-amount-label";390 amountLabel.textContent = "characters";391 const lenNum = controls.appendChild(document.createElement("span"));392 lenNum.className = "pwgen-len-num pwgen-len-num--edit";393 lenNum.textContent = String(length);394 lenNum.addEventListener("click", () => {395 const li = document.createElement("input");396 li.type = "text"; li.inputMode = "numeric";397 li.className = "pwgen-len-input"; li.value = String(length);398 lenNum.replaceWith(li);399 requestAnimationFrame(() => li.select());400 function commitLenEdit() {401 const n = parseInt(li.value, 10);402 if (!Number.isNaN(n)) {403 const clamped = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));404 if (mode === "passphrase") wordCount = clamped;405 else length = clamped;406 slider.value = String(mode === "passphrase" ? wordCount : length);407 regenerate();408 }409 lenNum.textContent = String(mode === "passphrase" ? wordCount : length);410 li.replaceWith(lenNum);411 }412 li.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commitLenEdit(); } else if (e.key === "Escape") { li.replaceWith(lenNum); } });413 li.addEventListener("blur", commitLenEdit);414 });415416 const pills = root.appendChild(document.createElement("div"));417 pills.className = "pwgen-pills";418419 function makePill(label: string, on: boolean, onToggle: (next: boolean) => void): HTMLButtonElement {420 const b = document.createElement("button");421 b.type = "button";422 b.className = `pwgen-pill ${on ? "is-on" : ""}`;423 b.textContent = label;424 b.addEventListener("click", () => {425 const next = !b.classList.contains("is-on");426 b.classList.toggle("is-on", next);427 onToggle(next);428 });429 return b;430 }431 pills.appendChild(makePill("A-Z", upper, (next) => { upper = next; regenerate(); }));432 pills.appendChild(makePill("a-z", lower, (next) => { lower = next; regenerate(); }));433 pills.appendChild(makePill("0-9", numbers, (next) => { numbers = next; regenerate(); }));434 pills.appendChild(makePill("!@#", symbols, (next) => {435 436 symbols = next;437 regenerate();438 }));439440 const separatorPills = root.appendChild(document.createElement("div"));441 separatorPills.className = "pwgen-pills";442 separatorPills.hidden = true;443 for (const value of ["-", " ", ".", "_"] as PassphraseSeparator[]) {444 const button = separatorPills.appendChild(document.createElement("button"));445 button.type = "button";446 button.className = `pwgen-pill ${value === separator ? "is-on" : ""}`;447 button.textContent = value === " " ? "space" : value;448 button.setAttribute("aria-label", value === " " ? "Space separator" : `${value} separator`);449 button.setAttribute("aria-pressed", String(value === separator));450 button.addEventListener("click", () => {451 separator = value;452 for (const sibling of separatorPills.querySelectorAll("button")) {453 const selected = sibling === button;454 sibling.classList.toggle("is-on", selected);455 sibling.setAttribute("aria-pressed", String(selected));456 }457 regenerate();458 });459 }460461 462 463 464 const hintParts: string[] = [];465 if (policy.minLength) hintParts.push(`min ${policy.minLength}`);466 if (policy.maxLength) hintParts.push(`max ${policy.maxLength}`);467 if (policy.requiresSymbols) hintParts.push("requires symbols");468 if (policy.forbidsSymbols) hintParts.push("no symbols");469 if (policy.forbiddenChars) hintParts.push(`excludes ${policy.forbiddenChars.slice(0, 6)}${policy.forbiddenChars.length > 6 ? "…" : ""}`);470 let policyHint: HTMLDivElement | null = null;471 if (hintParts.length > 0) {472 const hint = root.appendChild(document.createElement("div"));473 policyHint = hint;474 hint.className = "pwgen-policy-hint";475 const accent = document.createElement("span");476 accent.className = "pwgen-hint-accent";477 accent.textContent = "site rules · ";478 const text = document.createTextNode(hintParts.join(" · "));479 hint.appendChild(accent);480 hint.appendChild(text);481 }482483 function regenerate() {484 485 486 password = mode === "passphrase"487 ? generatePassphrase({ wordCount, separator })488 : generatePassword({489 length,490 includeUppercase: upper,491 includeLowercase: lower,492 includeNumbers: numbers,493 includeSymbols: symbols,494 forbiddenChars,495 });496 output.value = password;497 opts.onChange?.(password);498 }499500 output.addEventListener("input", () => {501 password = output.value;502 if (mode === "password") {503 length = password.length;504 slider.value = String(Math.max(8, Math.min(256, length)));505 lenNum.textContent = String(length);506 }507 opts.onChange?.(password);508 });509510 slider.addEventListener("input", () => {511 if (mode === "passphrase") wordCount = parseInt(slider.value, 10);512 else length = parseInt(slider.value, 10);513 lenNum.textContent = String(mode === "passphrase" ? wordCount : length);514 regenerate();515 });516 regenBtn.addEventListener("click", regenerate);517518 function selectMode(nextMode: "password" | "passphrase") {519 mode = nextMode;520 const isPassphrase = mode === "passphrase";521 passwordModeBtn.classList.toggle("is-on", !isPassphrase);522 passwordModeBtn.setAttribute("aria-pressed", String(!isPassphrase));523 passphraseModeBtn.classList.toggle("is-on", isPassphrase);524 passphraseModeBtn.setAttribute("aria-pressed", String(isPassphrase));525 pills.hidden = isPassphrase;526 separatorPills.hidden = !isPassphrase;527 if (policyHint) policyHint.hidden = isPassphrase;528 slider.min = String(isPassphrase ? MIN_PASSPHRASE_WORDS : Math.max(8, policy.minLength ?? 8));529 slider.max = String(isPassphrase ? MAX_PASSPHRASE_WORDS : Math.min(256, policy.maxLength ?? 256));530 slider.value = String(isPassphrase ? wordCount : length);531 slider.setAttribute("aria-label", isPassphrase ? "Word count" : "Password length");532 amountLabel.textContent = isPassphrase ? "words" : "characters";533 lenNum.textContent = String(isPassphrase ? wordCount : length);534 regenerate();535 }536 passwordModeBtn.addEventListener("click", () => selectMode("password"));537 passphraseModeBtn.addEventListener("click", () => selectMode("passphrase"));538539 let copyResetTimer: ReturnType<typeof setTimeout> | null = null;540 async function copy(): Promise<void> {541 try {542 await navigator.clipboard.writeText(password);543 copyBtn.classList.add("is-copied");544 if (copyResetTimer) clearTimeout(copyResetTimer);545 copyResetTimer = setTimeout(() => copyBtn.classList.remove("is-copied"), 1400);546 } catch {547 548 }549 }550 copyBtn.addEventListener("click", () => { void copy(); });551552 if (opts.initialPassword) {553 password = opts.initialPassword;554 output.value = password;555 opts.onChange?.(password);556 } else {557 regenerate();558 }559560 return {561 root,562 getPassword: () => password,563 copy,564 };565}566567export function observeAnchorPosition(568 anchorEl: HTMLElement,569 onUpdate: () => void570): () => void {571 let rafId = 0;572 let burstUntil = 0;573574 const runUpdate = () => {575 rafId = 0;576 onUpdate();577 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);578 };579 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };580 const scheduleBurst = (ms = 220) => {581 burstUntil = Math.max(burstUntil, performance.now() + ms);582 scheduleUpdate();583 };584585 const resizeObserver = new ResizeObserver(() => scheduleBurst());586 resizeObserver.observe(anchorEl);587 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);588589 const mutationObserver = new MutationObserver(() => scheduleBurst());590 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });591 if (anchorEl.parentElement) {592 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });593 }594595 const onScroll = () => scheduleUpdate();596 window.addEventListener("scroll", onScroll, true);597 window.addEventListener("resize", onScroll);598 window.visualViewport?.addEventListener("resize", onScroll);599 window.visualViewport?.addEventListener("scroll", onScroll);600 anchorEl.addEventListener("focus", () => scheduleBurst());601 anchorEl.addEventListener("input", () => scheduleBurst());602603 scheduleBurst(260);604605 return () => {606 if (rafId) cancelAnimationFrame(rafId);607 resizeObserver.disconnect();608 mutationObserver.disconnect();609 window.removeEventListener("scroll", onScroll, true);610 window.removeEventListener("resize", onScroll);611 window.visualViewport?.removeEventListener("resize", onScroll);612 window.visualViewport?.removeEventListener("scroll", onScroll);613 };614}615616export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {617 const button = document.createElement("button");618 button.type = "button";619 button.setAttribute("aria-label", "Peach autofill");620 button.title = "Fill with Peach";621 button.innerHTML = peachIconSvg();622 623 624 625 626 button.style.cssText = `627 position: fixed; z-index: 2147483646;628 width: 26px; height: 26px; cursor: pointer;629 pointer-events: auto; border: none; padding: 3px;630 display: inline-flex; align-items: center; justify-content: center;631 background: transparent; border-radius: 999px;632 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;633 opacity: 0;634 `;635 button.addEventListener("mouseenter", () => {636 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;637 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;638 button.style.transform = "scale(1.08)";639 });640 button.addEventListener("mouseleave", () => {641 button.style.background = "transparent";642 button.style.boxShadow = "none";643 button.style.transform = "none";644 });645 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });646 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });647648 document.body.appendChild(button);649650 function updatePosition() {651 const rect = field.getBoundingClientRect();652 if (rect.width === 0 || rect.height === 0) {653 button.style.opacity = "0"; button.style.pointerEvents = "none";654 return;655 }656 const sz = 26;657 const inset = Math.max(5, Math.min(10, rect.height * 0.22));658 const top = rect.top + rect.height / 2 - sz / 2;659 660 661 const minLeft = rect.left + 4;662 const left = Math.max(minLeft, rect.right - sz - inset);663 664 if (rect.width < 40) {665 button.style.opacity = "0"; button.style.pointerEvents = "none";666 return;667 }668 button.style.top = `${top}px`;669 button.style.left = `${left}px`;670 button.style.opacity = "1"; button.style.pointerEvents = "auto";671 }672 updatePosition();673 requestAnimationFrame(() => { button.style.opacity = "1"; });674 const stopObserving = observeAnchorPosition(field, updatePosition);675 return () => { stopObserving(); button.remove(); };676}677678679680681682683684const SELECTOR_STYLES = `685 :host { all: initial; }686 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }687688 .peach-inline-popup[data-theme="dark"] {689 --peach-bg: #1a1410;690 --peach-bg-card: #221c16;691 --peach-text: #f5f0ea;692 --peach-text-dim: #8a7a6a;693 --peach-text-muted: #4a3a2a;694 --peach-border: rgba(255,255,255,0.07);695 --peach-input-bg: rgba(255,255,255,0.05);696 --peach-input-border: rgba(255,255,255,0.08);697 }698 .peach-inline-popup[data-theme="light"] {699 --peach-bg: #fdf8f4;700 --peach-bg-card: #f5ede4;701 --peach-text: #1c1310;702 --peach-text-dim: #6b5a4a;703 --peach-text-muted: #a89080;704 --peach-border: rgba(0,0,0,0.08);705 --peach-input-bg: rgba(0,0,0,0.04);706 --peach-input-border: rgba(0,0,0,0.10);707 }708709 .peach-inline-popup {710 background: var(--peach-bg);711 border: 1px solid var(--peach-border-accent, var(--peach-border));712 border-radius: 14px;713 overflow: hidden;714 box-shadow:715 0 8px 40px var(--peach-glow, transparent),716 0 2px 12px rgba(0,0,0,0.5);717 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;718 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;719 transition: height 220ms cubic-bezier(0.16,1,0.3,1);720 }721 /* Light theme: shadow against a light page is too strong otherwise — drop both722 the accent glow and the dark drop-shadow opacity to keep the popup elegant. */723 .peach-inline-popup[data-theme="light"] {724 box-shadow:725 0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),726 0 2px 12px rgba(0,0,0,0.12);727 }728 .peach-inline-popup.is-exiting {729 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;730 pointer-events: none;731 }732733 @keyframes peach-enter {734 0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }735 65% { opacity: 1; filter: blur(0); }736 100% { opacity: 1; transform: translateY(0) scale(1); }737 }738 @keyframes peach-exit {739 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }740 100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }741 }742 @keyframes peach-slide-in {743 from { opacity: 0; transform: translateX(12px); }744 to { opacity: 1; transform: translateX(0); }745 }746 @keyframes peach-slide-back {747 from { opacity: 0; transform: translateX(-12px); }748 to { opacity: 1; transform: translateX(0); }749 }750 .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }751 .peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }752753 /* Domain header */754 .domain-header {755 padding: 9px 13px 7px;756 border-bottom: 1px solid var(--peach-border);757 display: flex; align-items: center; justify-content: space-between;758 gap: 8px;759 }760 .domain-header-text {761 display: flex; flex-direction: column; gap: 2px;762 min-width: 0; flex: 1;763 }764 .domain-eyebrow {765 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;766 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;767 }768 .domain-name {769 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);770 font-family: "IBM Plex Mono", monospace;771 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;772 }773 .domain-header-actions {774 display: inline-flex; align-items: center; gap: 5px;775 flex-shrink: 0;776 }777 .domain-add-btn,778 .domain-search-btn {779 flex-shrink: 0;780 width: 22px; height: 22px;781 display: inline-flex; align-items: center; justify-content: center;782 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);783 border: 1px solid color-mix(in srgb, var(--peach-accent) 28%, transparent);784 border-radius: 6px;785 color: var(--peach-accent);786 cursor: pointer; padding: 0;787 transition: background 120ms, border-color 120ms, transform 80ms;788 font-family: "IBM Plex Mono", monospace;789 font-size: 14px; line-height: 1;790 }791 .domain-add-btn:hover,792 .domain-search-btn:hover {793 background: color-mix(in srgb, var(--peach-accent) 22%, transparent);794 border-color: color-mix(in srgb, var(--peach-accent) 48%, transparent);795 transform: scale(1.06);796 }797 .domain-add-btn:active,798 .domain-search-btn:active { transform: scale(0.96); }799800 /* Search */801 .search-wrap {802 padding: 9px 12px 7px;803 border-bottom: 1px solid var(--peach-border);804 }805 /* UI-2 / MV-2: brief loading hint while the vault-wide list lazy-loads. */806 .search-loading {807 display: block;808 margin-top: 5px;809 font-size: 10px;810 color: var(--peach-text-dim);811 font-family: "IBM Plex Mono", monospace;812 animation: search-loading-pulse 1s ease-in-out infinite;813 }814 @keyframes search-loading-pulse { 0%,100% { opacity: 0.45; } 50% { opacity: 0.9; } }815 .search {816 width: 100%; background: var(--peach-input-bg);817 border: 1px solid var(--peach-input-border);818 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;819 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;820 transition: border-color 120ms, box-shadow 120ms;821 }822 .search:focus {823 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);824 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);825 }826 .search::placeholder { color: var(--peach-text-muted); }827828 /* Results */829 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }830 .results::-webkit-scrollbar { display: none; }831832 /* Entry rows */833 .entry {834 display: flex; align-items: center; padding: 8px 12px;835 cursor: pointer; border-bottom: 1px solid var(--peach-border);836 transition: background 70ms; position: relative; outline: none;837 }838 .entry:last-child { border-bottom: none; }839 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }840 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }841 .entry.focused::before {842 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;843 width: 2px; border-radius: 2px;844 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);845 }846847 .entry-icon {848 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;849 background: var(--peach-input-bg); border: 1px solid var(--peach-border);850 display: flex; align-items: center; justify-content: center;851 overflow: hidden; margin-right: 10px;852 }853 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }854 .entry-icon.is-new {855 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);856 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);857 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);858 }859860 .entry-info { flex: 1; min-width: 0; }861 .entry-title {862 font-size: 12.5px; color: var(--peach-text);863 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;864 line-height: 1.3;865 }866 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }867 .entry-sub {868 font-size: 10px; color: var(--peach-text-dim);869 font-family: "IBM Plex Mono", monospace;870 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;871 margin-top: 1px;872 }873874 .totp-badge {875 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;876 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);877 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);878 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;879 }880 .passkey-badge {881 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;882 border-radius: 4px;883 background: color-mix(in srgb, var(--peach-accent) 14%, transparent);884 color: color-mix(in srgb, var(--peach-accent) 85%, transparent);885 border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);886 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;887 }888889 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }890891 .empty-hint {892 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);893 font-family: "IBM Plex Mono", monospace;894 }895896 /* Create form */897 .create-form-wrap {898 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;899 }900 .create-head {901 display: flex; align-items: center; gap: 8px;902 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);903 }904 .back-btn {905 background: transparent; border: none; cursor: pointer;906 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;907 display: flex; align-items: center; line-height: 0;908 transition: color 100ms, background 100ms;909 }910 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }911 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }912 /* minmax(0, 1fr) caps the grid track at the popup width — without it,913 long monospace content (e.g. a 60-char generated password) would914 auto-size the track wider than the popup and overflow the right edge. */915 .create-body { padding: 12px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; }916 .f-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: 3px; }917 .f-label {918 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;919 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;920 }921 .f-input {922 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);923 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;924 font-size: 11px; font-family: "IBM Plex Mono", monospace;925 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;926 }927 .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); }928 .f-input::placeholder { color: var(--peach-text-muted); }929 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }930 .f-row { display: flex; gap: 6px; }931 .f-row .f-input { flex: 1; min-width: 0; }932 .gen-btn {933 padding: 0 10px; border-radius: 7px; border: none;934 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);935 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);936 font-size: 10px; font-family: "IBM Plex Mono", monospace;937 cursor: pointer; white-space: nowrap; flex-shrink: 0;938 display: flex; align-items: center; gap: 4px;939 transition: background 100ms;940 }941 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }942 .gen-toggle-link {943 background: none; border: none; padding: 2px 0;944 color: color-mix(in srgb, var(--peach-accent) 60%, transparent);945 font-size: 10px; font-family: "IBM Plex Mono", monospace;946 cursor: pointer; text-align: left; transition: color 100ms;947 }948 .gen-toggle-link:hover { color: var(--peach-accent); }949 .pw-history {950 display: grid; gap: 4px; margin-top: 4px;951 max-height: 150px; overflow-y: auto; scrollbar-width: thin;952 }953 .pw-history-row {954 display: flex; align-items: center; justify-content: space-between; gap: 6px;955 padding: 5px 7px; border-radius: 7px;956 background: color-mix(in srgb, var(--peach-accent) 6%, transparent);957 border: 1px solid color-mix(in srgb, var(--peach-accent) 10%, transparent);958 }959 .pw-history-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }960 .pw-history-pw {961 font-family: "IBM Plex Mono", monospace; font-size: 11px;962 color: var(--peach-t1, #eee); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;963 }964 .pw-history-age {965 font-family: "IBM Plex Mono", monospace; font-size: 9px;966 color: color-mix(in srgb, var(--peach-accent) 55%, transparent); letter-spacing: 0.03em;967 }968 .pw-history-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }969 .pw-history-btn {970 background: none; border: none; padding: 2px 5px; border-radius: 5px;971 color: color-mix(in srgb, var(--peach-accent) 65%, transparent);972 font-size: 9px; font-family: "IBM Plex Mono", monospace; cursor: pointer;973 transition: color 100ms, background 100ms;974 }975 .pw-history-btn:hover {976 color: var(--peach-accent);977 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);978 }979 .pw-history-empty {980 font-family: "IBM Plex Mono", monospace; font-size: 10px; line-height: 1.45;981 color: color-mix(in srgb, var(--peach-accent) 55%, transparent);982 padding: 6px 8px; border-radius: 7px;983 background: color-mix(in srgb, var(--peach-accent) 5%, transparent);984 border: 1px solid color-mix(in srgb, var(--peach-accent) 9%, transparent);985 }986 .save-btn {987 width: 100%; padding: 8px; border-radius: 8px; border: none;988 background: var(--peach-accent); color: #fff; font-size: 12px;989 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;990 transition: background 120ms, box-shadow 120ms, transform 80ms;991 letter-spacing: 0.01em;992 }993 .save-btn:hover {994 background: color-mix(in srgb, var(--peach-accent) 82%, #000);995 box-shadow: 0 4px 20px var(--peach-glow, transparent);996 }997 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }998 .url-hint {999 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);1000 font-family: "IBM Plex Mono", monospace; font-style: italic;1001 }10021003 /* Success */1004 .success-wrap {1005 display: flex; flex-direction: column; align-items: center;1006 padding: 24px 16px 20px; gap: 8px;1007 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;1008 }1009 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }1010 .success-ring {1011 width: 40px; height: 40px; border-radius: 50%;1012 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);1013 display: flex; align-items: center; justify-content: center;1014 }1015 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }1016 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }10171018 /* Silver platter (State 1 — single match) */1019 .silver-platter {1020 padding: 14px 14px 12px;1021 display: flex; flex-direction: column; gap: 6px;1022 }1023 .sp-title {1024 font-size: 14px; font-weight: 500; color: var(--peach-text);1025 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1026 line-height: 1.25;1027 }1028 .sp-identity-row {1029 display: flex; align-items: baseline; gap: 5px; margin-bottom: 3px;1030 }1031 .sp-identity-label {1032 font-size: 9px; font-weight: 600; letter-spacing: 0.05em;1033 text-transform: uppercase; color: var(--peach-text-muted); flex-shrink: 0;1034 }1035 .sp-username {1036 font-size: 12px; color: var(--peach-text-dim);1037 font-family: "IBM Plex Mono", monospace;1038 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1039 margin-bottom: 0;1040 }1041 .sp-identity-row + .sp-fill-btn, .sp-username + .sp-fill-btn { margin-top: 6px; }1042 .sp-fill-btn {1043 width: 100%; padding: 11px; border-radius: 9px; border: none;1044 background: var(--peach-accent); color: #fff;1045 font-size: 13px; font-weight: 500; cursor: pointer;1046 font-family: Inter, sans-serif; letter-spacing: 0.01em;1047 display: flex; align-items: center; justify-content: center; min-height: 38px;1048 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;1049 }1050 .sp-fill-btn:hover {1051 filter: brightness(1.1);1052 box-shadow: 0 4px 20px var(--peach-glow, transparent);1053 transform: translateY(-1px);1054 }1055 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }1056 .sp-fill-btn.filled {1057 background: #16a34a; transform: none; filter: none;1058 box-shadow: 0 4px 16px rgba(22,163,74,0.35);1059 cursor: default;1060 }1061 .sp-fill-btn:disabled { cursor: default; }1062 .sp-actions-group {1063 margin-top: 6px;1064 display: flex; flex-direction: column; gap: 4px;1065 }1066 .sp-actions-label {1067 font-size: 8px; letter-spacing: 0.18em; text-transform: uppercase;1068 color: var(--peach-text-muted);1069 font-family: "IBM Plex Mono", monospace;1070 text-align: center;1071 }1072 .sp-actions {1073 display: flex; gap: 4px; justify-content: space-between; align-items: center;1074 }1075 .sp-action-btn {1076 flex: 1; height: 28px; border: none; border-radius: 6px;1077 background: transparent; color: var(--peach-text-dim);1078 font-size: 12px; cursor: pointer;1079 transition: background 120ms ease, color 120ms ease;1080 font-family: Inter, sans-serif; letter-spacing: 0.04em;1081 display: inline-flex; align-items: center; justify-content: center; gap: 4px;1082 white-space: nowrap; overflow: hidden;1083 }1084 .sp-action-btn:hover:not(:disabled) {1085 background: color-mix(in srgb, var(--peach-text) 5%, transparent);1086 color: var(--peach-text);1087 }1088 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }1089 .sp-action-btn.flashed {1090 color: var(--peach-accent);1091 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1092 }10931094 /* TOTP tray — shown below the silver-platter secondary actions */1095 .sp-tray {1096 margin-top: 8px;1097 padding: 10px 12px;1098 border-radius: 9px;1099 background: var(--peach-input-bg);1100 border: 1px solid var(--peach-input-border);1101 display: flex; align-items: center; gap: 12px;1102 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;1103 }1104 .sp-tray-label {1105 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;1106 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;1107 flex-shrink: 0;1108 }1109 .sp-totp-code {1110 font-size: 18px; font-weight: 500;1111 color: var(--peach-text);1112 font-family: "IBM Plex Mono", monospace;1113 letter-spacing: 0.12em;1114 flex: 1;1115 text-align: center;1116 }1117 .sp-totp-ring {1118 width: 22px; height: 22px; flex-shrink: 0;1119 }1120 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }1121 .sp-totp-ring .ring-fill {1122 stroke: var(--peach-accent);1123 transition: stroke-dashoffset 1s linear;1124 transform: rotate(-90deg);1125 transform-origin: center;1126 }1127 .sp-totp-copy {1128 background: transparent; border: none; cursor: pointer;1129 padding: 4px 8px; border-radius: 5px;1130 color: var(--peach-text-dim); font-size: 11px;1131 font-family: Inter, sans-serif;1132 transition: background 120ms, color 120ms;1133 }1134 .sp-totp-copy:hover {1135 background: color-mix(in srgb, var(--peach-text) 5%, transparent);1136 color: var(--peach-text);1137 }11381139 /* Card silver platter (State 7) — reuses .sp-* primary action classes */1140 .card-platter {1141 padding: 12px 14px 10px;1142 display: flex; flex-direction: column; gap: 4px;1143 }1144 .cp-network {1145 font-size: 10px; font-weight: 600;1146 color: var(--peach-accent);1147 letter-spacing: 0.08em; text-transform: uppercase;1148 font-family: Inter, sans-serif;1149 }1150 .cp-nickname {1151 font-size: 14px; font-weight: 500; color: var(--peach-text);1152 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1153 line-height: 1.2;1154 }1155 .cp-last4 {1156 font-size: 12px; color: var(--peach-text-dim);1157 font-family: "IBM Plex Mono", monospace;1158 letter-spacing: 0.04em;1159 margin-top: 2px;1160 }1161 .cp-meta {1162 font-size: 11px; color: var(--peach-text-dim);1163 font-family: "IBM Plex Mono", monospace;1164 margin-top: 2px;1165 display: flex; gap: 10px; align-items: center;1166 }1167 .cp-meta-sep { opacity: 0.5; }1168 .cp-fill-wrap { margin-top: 10px; }11691170 /* Locked state */1171 .locked-wrap {1172 display: flex; flex-direction: column; align-items: stretch;1173 padding: 14px 13px 13px; gap: 9px;1174 }1175 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }1176 .locked-icon {1177 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;1178 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1179 display: flex; align-items: center; justify-content: center;1180 }1181 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }1182 .locked-sub { font-size: 10.5px; line-height: 1.4; color: var(--peach-text-muted); text-align: center; margin: 2px 0 2px; }1183 .locked-input {1184 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1185 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;1186 font-size: 11px; font-family: "IBM Plex Mono", monospace;1187 outline: none; transition: border-color 120ms, box-shadow 120ms;1188 }1189 .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); }1190 .locked-input::placeholder { color: var(--peach-text-muted); }1191 .locked-btn {1192 width: 100%; padding: 8px; border-radius: 8px; border: none;1193 background: var(--peach-accent); color: #fff; font-size: 12px;1194 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;1195 transition: background 120ms;1196 letter-spacing: 0.01em;1197 box-shadow: 0 2px 16px var(--peach-glow, transparent);1198 animation: peach-pulse-glow 2.4s ease-in-out infinite;1199 }1200 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }1201 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }1202 .locked-error {1203 font-size: 10px; color: rgba(220,80,60,0.85);1204 font-family: "IBM Plex Mono", monospace;1205 }12061207 @keyframes peach-pulse-glow {1208 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }1209 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),1210 0 0 48px var(--peach-glow, transparent); }1211 }12121213 /* ── State 10: identity helper ─────────────────────────────── */1214 .identity-list {1215 max-height: 192px; overflow-y: auto; padding: 4px 0;1216 scrollbar-width: none;1217 }1218 .identity-list::-webkit-scrollbar { display: none; }1219 .peach-identity-row {1220 display: flex; align-items: center; gap: 10px;1221 padding: 9px 12px; cursor: pointer; border-radius: 8px;1222 transition: background 100ms ease;1223 }1224 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1225 .peach-identity-value {1226 font-family: "IBM Plex Mono", monospace; font-size: 13px;1227 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;1228 }1229 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }1230 .identity-search-wrap {1231 display: flex; flex-direction: column; gap: 6px;1232 }1233 .identity-mode-toggle {1234 display: inline-flex; gap: 4px; flex-wrap: wrap;1235 }1236 .identity-mode-btn {1237 border: 1px solid var(--peach-input-border);1238 background: var(--peach-input-bg);1239 color: var(--peach-text-dim);1240 border-radius: 999px;1241 padding: 4px 9px;1242 cursor: pointer;1243 font-family: "IBM Plex Mono", monospace;1244 font-size: 9px;1245 letter-spacing: 0.03em;1246 transition: background 100ms ease, border-color 100ms ease, color 100ms ease, transform 80ms ease;1247 }1248 .identity-mode-btn:hover {1249 background: color-mix(in srgb, var(--peach-text) 6%, var(--peach-input-bg));1250 color: var(--peach-text);1251 }1252 .identity-mode-btn.is-active {1253 color: var(--peach-accent);1254 border-color: color-mix(in srgb, var(--peach-accent) 30%, var(--peach-input-border));1255 background: color-mix(in srgb, var(--peach-accent) 10%, var(--peach-input-bg));1256 }1257 .identity-mode-btn:active { transform: translateY(1px); }1258 .identity-custom-row {1259 display: flex; flex-direction: column; gap: 2px;1260 padding: 9px 12px; cursor: pointer; border-radius: 8px;1261 transition: background 100ms ease;1262 }1263 .identity-custom-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1264 .identity-custom-row-title {1265 color: var(--peach-text); font-size: 12px; font-family: Inter, sans-serif;1266 }1267 .identity-custom-row-sub {1268 color: var(--peach-text-muted); font-size: 10px; font-family: "IBM Plex Mono", monospace;1269 }1270 .identity-empty {1271 padding: 10px 12px 2px; color: var(--peach-text-dim);1272 font-size: 10px; font-family: "IBM Plex Mono", monospace;1273 }1274 .identity-alt-row {1275 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1276 border-radius: 8px; transition: background 100ms ease;1277 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;1278 }1279 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1280 .identity-escape-row {1281 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1282 border-radius: 8px; transition: background 100ms ease; outline: none;1283 color: var(--peach-accent); font-size: 11px; font-family: Inter, sans-serif;1284 margin-top: 2px;1285 }1286 .identity-escape-row:hover, .identity-escape-row:focus { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }1287 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }1288 .identity-alt-input-wrap.open { display: flex; }1289 .identity-alt-input {1290 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;1291 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);1292 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;1293 outline: none; transition: border-color 120ms;1294 }1295 .identity-alt-input:focus {1296 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);1297 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);1298 }1299 .identity-alt-confirm {1300 height: 32px; padding: 0 12px; border-radius: 7px; border: none;1301 background: var(--peach-accent); color: #fff; font-size: 12px;1302 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;1303 }1304 /* ── Generator view (after identity picked) ────────────────── */1305 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1306 .gen-confirmed {1307 display: flex; align-items: center; gap: 8px; padding: 7px 10px;1308 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1309 border-radius: 8px;1310 }1311 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }1312 .gen-confirmed-val {1313 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1314 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1315 }1316 .gen-label {1317 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;1318 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;1319 }1320 .gen-pw-row { display: flex; gap: 6px; align-items: center; }1321 .gen-pw-val {1322 flex: 1; padding: 8px 10px; border-radius: 7px;1323 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1324 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1325 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1326 }1327 .gen-regen-btn {1328 height: 34px; padding: 0 10px; border-radius: 7px; border: none;1329 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1330 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;1331 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;1332 }1333 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1334 .gen-fill-btn {1335 padding: 10px; border-radius: 8px; border: none;1336 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;1337 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;1338 transition: filter 120ms ease, transform 80ms ease;1339 }1340 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }1341 .gen-fill-btn:active { transform: translateY(0); }13421343 /* ── Smart password generator (parity with main UI compact mode) ─── */1344 .pwgen-block { display: flex; flex-direction: column; gap: 8px; }1345 .pwgen-display-row {1346 display: flex; align-items: center; gap: 6px;1347 padding: 8px 10px; border-radius: 7px;1348 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1349 }1350 .pwgen-output {1351 flex: 1; min-width: 0; width: 0;1352 font-family: "IBM Plex Mono", monospace; font-size: 12px;1353 color: var(--peach-text);1354 background: transparent; border: none; outline: none; padding: 0;1355 }1356 .pwgen-icon-btn {1357 flex-shrink: 0;1358 width: 26px; height: 26px;1359 display: inline-flex; align-items: center; justify-content: center;1360 background: transparent; border: 1px solid var(--peach-input-border);1361 border-radius: 6px; color: var(--peach-text-dim); cursor: pointer;1362 padding: 0; transition: background 100ms, color 100ms, border-color 100ms;1363 }1364 .pwgen-icon-btn:hover {1365 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1366 color: var(--peach-accent);1367 border-color: color-mix(in srgb, var(--peach-accent) 30%, transparent);1368 }1369 .pwgen-icon-btn.is-copied {1370 background: color-mix(in srgb, var(--peach-accent) 18%, transparent);1371 color: var(--peach-accent);1372 border-color: color-mix(in srgb, var(--peach-accent) 45%, transparent);1373 }1374 .pwgen-controls-row {1375 display: flex; align-items: center; gap: 8px;1376 flex-wrap: wrap;1377 }1378 .pwgen-slider {1379 flex: 1; min-width: 110px;1380 accent-color: var(--peach-accent);1381 cursor: pointer; height: 2px; background: transparent;1382 }1383 .pwgen-len-num {1384 font-family: "IBM Plex Mono", monospace; font-size: 11px;1385 color: var(--peach-accent); min-width: 22px; text-align: right;1386 }1387 .pwgen-amount-label {1388 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;1389 font-size: 9px; min-width: 50px;1390 }1391 .pwgen-len-num--edit { cursor: text; border-bottom: 1px dotted color-mix(in srgb, var(--peach-accent) 50%, transparent); }1392 .pwgen-len-input {1393 font-family: "IBM Plex Mono", monospace; font-size: 11px;1394 color: var(--peach-accent); width: 28px; text-align: right;1395 background: transparent; border: none; border-bottom: 1px solid var(--peach-accent);1396 outline: none; padding: 0; min-width: 22px;1397 }1398 @keyframes identity-suggest-in {1399 from { opacity: 0; transform: translateY(-4px); }1400 to { opacity: 1; transform: none; }1401 }1402 .identity-suggest-list { display: flex; flex-direction: column; gap: 1px; margin-top: 3px;1403 animation: identity-suggest-in 180ms cubic-bezier(0.16,1,0.3,1) both; }1404 .identity-filter-row { display: flex; align-items: center; gap: 4px; padding: 2px 0 4px; }1405 .identity-collapse-btn {1406 margin-left: auto; background: transparent; border: none; cursor: pointer;1407 color: var(--peach-text-muted); padding: 2px 4px; display: flex; align-items: center;1408 line-height: 0; border-radius: 3px;1409 transition: color 100ms ease, transform 220ms cubic-bezier(0.16,1,0.3,1);1410 }1411 .identity-collapse-btn:hover { color: var(--peach-text); }1412 .identity-collapse-btn.is-collapsed { transform: rotate(-90deg); }1413 .identity-rows { display: grid; grid-template-rows: 1fr;1414 transition: grid-template-rows 220ms cubic-bezier(0.16,1,0.3,1); }1415 .identity-rows.is-collapsed { grid-template-rows: 0fr; }1416 .identity-rows-inner { overflow-y: auto; min-height: 0; max-height: 148px;1417 scrollbar-width: thin; scrollbar-color: var(--peach-text-muted) transparent; }1418 .identity-rows-inner::-webkit-scrollbar { width: 4px; }1419 .identity-rows-inner::-webkit-scrollbar-track { background: transparent; }1420 .identity-rows-inner::-webkit-scrollbar-thumb { background: var(--peach-text-muted); border-radius: 2px; }1421 .identity-filter-pill {1422 padding: 2px 7px; border-radius: 999px;1423 border: 1px solid var(--peach-input-border);1424 background: transparent; color: var(--peach-text-muted);1425 font-family: "IBM Plex Mono", monospace; font-size: 9px;1426 cursor: pointer; transition: border-color 80ms, background 80ms, color 80ms;1427 }1428 .identity-filter-pill.is-on {1429 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1430 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1431 color: var(--peach-accent);1432 }1433 .identity-suggest-row {1434 display: flex; align-items: center; gap: 5px;1435 width: 100%; padding: 4px 6px; border-radius: 5px; border: none;1436 background: transparent; cursor: pointer; text-align: left;1437 transition: background 80ms;1438 }1439 .identity-suggest-row:hover,1440 .identity-suggest-row:focus { background: color-mix(in srgb, var(--peach-accent) 10%, transparent); outline: none; }1441 .identity-suggest-val {1442 font-family: "IBM Plex Mono", monospace; font-size: 10px; color: var(--peach-text);1443 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;1444 }1445 .identity-suggest-type {1446 font-family: "IBM Plex Mono", monospace; font-size: 8px; font-weight: 600;1447 flex-shrink: 0; padding: 1px 3px; border-radius: 3px;1448 }1449 .identity-suggest-type--email {1450 color: color-mix(in srgb, var(--peach-accent) 70%, transparent);1451 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1452 }1453 .identity-suggest-type--username {1454 color: var(--peach-text-muted);1455 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1456 }1457 .identity-suggest-meta {1458 font-family: "IBM Plex Mono", monospace; font-size: 9px;1459 color: var(--peach-text-muted); flex-shrink: 0;1460 }1461 .pwgen-pills { display: flex; gap: 5px; flex-wrap: wrap; }1462 .pwgen-pill {1463 padding: 3px 8px; border-radius: 999px;1464 border: 1px solid var(--peach-input-border);1465 background: transparent; color: var(--peach-text-muted);1466 font-family: "IBM Plex Mono", monospace; font-size: 9px;1467 letter-spacing: 0.06em; cursor: pointer;1468 transition: all 130ms ease;1469 }1470 .pwgen-pill.is-on {1471 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1472 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1473 color: var(--peach-accent);1474 }1475 .pwgen-pill:not(.is-on):hover {1476 color: var(--peach-text-dim);1477 border-color: var(--peach-text-muted);1478 }1479 .pwgen-policy-hint {1480 font-size: 10px; color: var(--peach-text-muted);1481 font-family: "IBM Plex Mono", monospace; letter-spacing: 0.04em;1482 line-height: 1.3;1483 }1484 .pwgen-policy-hint .pwgen-hint-accent { color: var(--peach-accent); }14851486 /* ── Save offer (after password filled) ───────────────────── */1487 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1488 .save-offer-title {1489 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;1490 }1491 .save-offer-row {1492 display: flex; align-items: center; gap: 8px; padding: 6px 8px;1493 border-radius: 7px; background: var(--peach-input-bg);1494 border: 1px solid var(--peach-input-border);1495 }1496 .save-offer-lbl {1497 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;1498 font-family: "IBM Plex Mono", monospace;1499 }1500 .save-offer-val {1501 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1502 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1503 }1504 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }1505 .save-offer-save {1506 flex: 1; padding: 9px; border-radius: 8px; border: none;1507 background: var(--peach-accent); color: #fff; font-size: 13px;1508 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;1509 }1510 .save-offer-save:hover { filter: brightness(1.08); }1511 .save-offer-skip {1512 flex: 1; padding: 9px; border-radius: 8px; border: none;1513 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1514 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;1515 font-family: Inter, sans-serif; transition: background 100ms ease;1516 }1517 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1518`;15191520export function showAutofillSelector(1521 anchorEl: HTMLInputElement,1522 entries: SelectorEntry[],1523 onSelect: (id: string) => void,1524 options: SelectorOptions = {}1525): () => void {1526 const securityOrigin = getSecurityOriginDisplay(options.securityOrigin ?? location.origin);1527 const securityOriginLabel = detectHomograph(securityOrigin) ? `⚠ ${securityOrigin}` : securityOrigin;1528 const host = document.createElement("div");1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 const modalAncestor = (1543 anchorEl.closest<HTMLElement>('dialog, [aria-modal="true"], [role="dialog"]') ??1544 (() => {1545 let cur: HTMLElement | null = anchorEl.parentElement;1546 while (cur && cur !== document.body) {1547 if (window.getComputedStyle(cur).position === 'fixed') return cur;1548 cur = cur.parentElement;1549 }1550 return null;1551 })()1552 );1553 host.style.cssText = `position: fixed; z-index: 2147483647;`;1554 host.style.setProperty("--peach-accent", accentHex);1555 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));1556 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1557 1558 1559 1560 host.style.willChange = "top, left";1561 1562 1563 1564 1565 1566 const shadow = host.attachShadow({ mode: "closed", delegatesFocus: true });15671568 const style = shadow.appendChild(document.createElement("style"));1569 style.textContent = SELECTOR_STYLES;15701571 const rect = anchorEl.getBoundingClientRect();1572 1573 1574 host.style.top = `${rect.bottom + 5}px`;1575 host.style.left = `${rect.left}px`;1576 1577 1578 1579 1580 const popupWidth = Math.min(Math.max(rect.width, 320), 400);1581 host.style.width = `${popupWidth}px`;15821583 const container = shadow.appendChild(document.createElement("div"));1584 container.className = "peach-inline-popup";1585 container.setAttribute("data-theme", popupTheme);15861587 1588 1589 let searchableEntries: SelectorEntry[] = options.searchableEntries ?? [];1590 let searchableLoading = false;1591 let searchableLoaded = !options.loadSearchableEntries;15921593 async function ensureSearchableLoaded(rerender: () => void): Promise<void> {1594 if (searchableLoaded || searchableLoading || !options.loadSearchableEntries) {1595 return;1596 }1597 searchableLoading = true;1598 try {1599 searchableEntries = await options.loadSearchableEntries();1600 } catch {1601 searchableEntries = [];1602 } finally {1603 searchableLoaded = true;1604 searchableLoading = false;1605 rerender();1606 }1607 }1608 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();16091610 let query = "";1611 let focusedIndex = -1;1612 let currentMode: "list" | "create" | "success" | "silver-platter" | "identity-helper" | "edit" = "list";1613 let searchInput: HTMLInputElement | null = null;1614 let identityMode: IdentityFilterMode = "both";1615 1616 1617 1618 let silverPlatterOrigin: SelectorEntry | null = null;1619 1620 1621 const cleanupHooks: Array<() => void> = [];16221623 1624 1625 1626 {1627 let prevH = 0;1628 let blocked = false;1629 const heightRo = new ResizeObserver(([entry]) => {1630 if (blocked) return;1631 const h = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;1632 if (prevH > 0 && Math.abs(h - prevH) > 1) {1633 blocked = true;1634 container.style.height = `${prevH}px`;1635 requestAnimationFrame(() => requestAnimationFrame(() => {1636 container.style.height = `${h}px`;1637 prevH = h;1638 const onEnd = (e: TransitionEvent) => {1639 if (e.propertyName !== "height") return;1640 container.style.height = "";1641 blocked = false;1642 container.removeEventListener("transitionend", onEnd);1643 };1644 container.addEventListener("transitionend", onEnd);1645 }));1646 } else {1647 prevH = h;1648 }1649 });1650 heightRo.observe(container);1651 cleanupHooks.push(() => heightRo.disconnect());1652 }16531654 1655 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";1656 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");1657 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";16581659 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {1660 const seen = new Set<string>();1661 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });1662 }16631664 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {1665 const q = value.trim().toLowerCase();1666 const deduped = uniqueEntries(source);1667 if (!q) return deduped.slice(0, 8);1668 return deduped.filter(e =>1669 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)1670 ).slice(0, 12);1671 }16721673 function activeEntries(): SelectorEntry[] {1674 1675 if (query.trim()) {1676 const merged = uniqueEntries([...entries, ...searchableEntries]);1677 return filterEntries(merged, query);1678 }1679 1680 if (entries.length > 0) return entries;1681 return filterEntries(searchableEntries, "").slice(0, 8);1682 }16831684 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {1685 const icon = document.createElement("div");1686 icon.className = `entry-icon${isNew ? " is-new" : ""}`;1687 if (isNew) {1688 icon.innerHTML = plusSvgStr();1689 } else {1690 1691 1692 1693 void url;1694 icon.innerHTML = keySvgStr();1695 }1696 return icon;1697 }16981699 function renderIdentityHelper() {1700 currentMode = "identity-helper";1701 container.innerHTML = "";1702 focusedIndex = -1;1703 searchInput = null;17041705 const fallbackKind: IdentityKind = options.identityKind ?? "username";1706 const identities = filterSavedIdentities(options.savedIdentities ?? [], query, identityMode);1707 const typedQuery = query.trim();1708 const customKind = resolveCustomIdentityKind(typedQuery, identityMode, fallbackKind);17091710 function applyIdentity(identity: string) {1711 if (options.hasEmailField) {1712 if (options.identityKind === "email") {1713 options.onFillFields?.("", "", identity); 1714 renderSecondaryIdentityStep("username", identity);1715 } else {1716 options.onFillFields?.(identity, "", ""); 1717 renderSecondaryIdentityStep("email", identity);1718 }1719 } else {1720 options.onFillFields?.(identity, "");1721 renderGenerator(identity);1722 }1723 }17241725 function applyCustomIdentity(identity: string) {1726 const trimmed = identity.trim();1727 if (!trimmed) return;1728 applyIdentity(trimmed);1729 }17301731 1732 const hdr = container.appendChild(document.createElement("div"));1733 hdr.className = "domain-header";1734 const headText = hdr.appendChild(document.createElement("div"));1735 headText.className = "domain-header-text";1736 const eyebrow = headText.appendChild(document.createElement("div"));1737 eyebrow.className = "domain-eyebrow";1738 eyebrow.textContent = "NEW ACCOUNT";1739 const domName = headText.appendChild(document.createElement("div"));1740 domName.className = "domain-name";1741 domName.textContent = securityOriginLabel;17421743 const actions = hdr.appendChild(document.createElement("div"));1744 actions.className = "identity-mode-toggle";1745 for (const mode of ["both", "email", "username"] as const) {1746 const btn = actions.appendChild(document.createElement("button"));1747 btn.type = "button";1748 btn.className = `identity-mode-btn${identityMode === mode ? " is-active" : ""}`;1749 btn.textContent = identityFilterLabel(mode);1750 btn.setAttribute("aria-pressed", String(identityMode === mode));1751 btn.addEventListener("click", () => {1752 identityMode = mode;1753 renderIdentityHelper();1754 });1755 }17561757 const searchWrap = container.appendChild(document.createElement("div"));1758 searchWrap.className = "search-wrap identity-search-wrap";1759 const search = searchWrap.appendChild(document.createElement("input"));1760 search.className = "search";1761 search.placeholder = "search identities…";1762 search.value = query;1763 search.addEventListener("input", () => {1764 query = search.value;1765 renderIdentityHelper();1766 searchInput?.focus();1767 searchInput?.setSelectionRange(query.length, query.length);1768 });1769 search.addEventListener("keydown", (e) => {1770 if (e.key === "Escape") {1771 e.preventDefault();1772 dismiss(true);1773 return;1774 }1775 if (e.key !== "Enter") return;1776 const exact = identities.find((id) => id.value.toLowerCase() === typedQuery.toLowerCase());1777 if (exact) {1778 e.preventDefault();1779 applyIdentity(exact.value);1780 return;1781 }1782 if (!typedQuery || identities.length > 0) return;1783 e.preventDefault();1784 applyCustomIdentity(typedQuery);1785 });1786 const searchTools = searchWrap.appendChild(document.createElement("div"));1787 searchTools.className = "identity-mode-toggle";1788 const sortHint = searchTools.appendChild(document.createElement("div"));1789 sortHint.className = "identity-empty";1790 sortHint.style.padding = "0";1791 sortHint.textContent = "sorted by recent use";1792 searchInput = search;1793 const customLabel = identityMode === "both"1794 ? customKind1795 : identityMode;17961797 1798 const list = container.appendChild(document.createElement("div"));1799 list.className = "identity-list";1800 1801 1802 1803 1804 const updateListFade = () => {1805 const overflowing = list.scrollHeight > list.clientHeight + 1;1806 const atBottom = list.scrollTop + list.clientHeight >= list.scrollHeight - 2;1807 const mask = overflowing && !atBottom1808 ? "linear-gradient(to bottom, black calc(100% - 24px), transparent 100%)"1809 : "none";1810 list.style.setProperty("mask-image", mask);1811 list.style.setProperty("-webkit-mask-image", mask);1812 };1813 list.addEventListener("scroll", updateListFade);1814 if (typedQuery && identities.length === 0) {1815 const empty = list.appendChild(document.createElement("div"));1816 empty.className = "identity-empty";1817 empty.textContent = identityMode === "both"1818 ? `No match for “${typedQuery}”.`1819 : `No ${identityFilterLabel(identityMode).toLowerCase()} match for “${typedQuery}”.`;1820 const custom = list.appendChild(document.createElement("div"));1821 custom.className = "identity-custom-row";1822 const customTitle = custom.appendChild(document.createElement("div"));1823 customTitle.className = "identity-custom-row-title";1824 customTitle.textContent = `Use “${typedQuery}” as a new ${customLabel}`;1825 const customSub = custom.appendChild(document.createElement("div"));1826 customSub.className = "identity-custom-row-sub";1827 customSub.textContent = "Press Enter or click to continue.";1828 custom.addEventListener("click", () => applyCustomIdentity(typedQuery));1829 }1830 for (const id of identities) {1831 const row = list.appendChild(document.createElement("div"));1832 row.className = "peach-identity-row";1833 row.setAttribute("tabindex", "-1");1834 const val = row.appendChild(document.createElement("div"));1835 val.className = "peach-identity-value";1836 val.textContent = id.value;1837 const meta = row.appendChild(document.createElement("div"));1838 meta.className = "peach-identity-meta";1839 meta.textContent = `used ${id.useCount}×`;1840 row.addEventListener("click", () => {1841 applyIdentity(id.value);1842 });1843 }18441845 updateListFade();18461847 1848 if (identities.length > 0) {1849 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1850 }18511852 1853 const altRow = container.appendChild(document.createElement("div"));1854 altRow.className = "identity-alt-row";1855 altRow.textContent = `+ Use a different ${customLabel}…`;18561857 const altInputWrap = container.appendChild(document.createElement("div"));1858 altInputWrap.className = "identity-alt-input-wrap";1859 const altInput = altInputWrap.appendChild(document.createElement("input"));1860 altInput.className = "identity-alt-input";1861 altInput.type = "text";1862 altInput.placeholder = customLabel === "email" ? "[email protected]" : "your username";1863 altInput.setAttribute("autocomplete", "off");1864 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1865 altConfirm.className = "identity-alt-confirm";1866 altConfirm.type = "button";1867 altConfirm.textContent = "✓";18681869 function fillCustomIdentity() {1870 const v = altInput.value.trim();1871 if (!v) return;1872 applyCustomIdentity(v);1873 }18741875 altRow.addEventListener("click", () => {1876 altRow.style.display = "none";1877 altInputWrap.classList.add("open");1878 altInput.value = typedQuery || altInput.value;1879 altInput.focus();1880 altInput.setSelectionRange(altInput.value.length, altInput.value.length);1881 });1882 altConfirm.addEventListener("click", fillCustomIdentity);1883 altInput.addEventListener("keydown", (e) => {1884 if (e.key === "Enter") { e.preventDefault(); fillCustomIdentity(); }1885 });18861887 1888 1889 1890 1891 1892 1893 1894 if (options.loadSearchableEntries || (options.searchableEntries?.length ?? 0) > 0) {1895 const escapeRow = container.appendChild(document.createElement("div"));1896 escapeRow.className = "identity-escape-row";1897 escapeRow.textContent = "I already have an account →";1898 escapeRow.setAttribute("tabindex", "0");1899 const toSearch = () => { void ensureSearchableLoaded(renderList); renderList(); };1900 escapeRow.addEventListener("click", toSearch);1901 escapeRow.addEventListener("keydown", (e) => {1902 if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toSearch(); }1903 });1904 }19051906 requestAnimationFrame(() => {1907 search.focus();1908 search.setSelectionRange(query.length, query.length);1909 });1910 }19111912 19131914191519161917 function renderSecondaryIdentityStep(stepKind: "email" | "username", firstValue: string) {1918 currentMode = "identity-helper";1919 container.innerHTML = "";19201921 const wrap = container.appendChild(document.createElement("div"));1922 wrap.className = "gen-view";19231924 1925 const hdrRow = wrap.appendChild(document.createElement("div"));1926 hdrRow.className = "domain-header";1927 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1928 eyebrow2.className = "domain-eyebrow";1929 eyebrow2.textContent = "NEW ACCOUNT";1930 const domName2 = hdrRow.appendChild(document.createElement("div"));1931 domName2.className = "domain-name";1932 domName2.textContent = securityOriginLabel;19331934 1935 const conf = wrap.appendChild(document.createElement("div"));1936 conf.className = "gen-confirmed";1937 const confCheck = conf.appendChild(document.createElement("span"));1938 confCheck.className = "gen-confirmed-check";1939 confCheck.textContent = "✓";1940 const confVal = conf.appendChild(document.createElement("div"));1941 confVal.className = "gen-confirmed-val";1942 confVal.textContent = firstValue;19431944 const stepLbl = wrap.appendChild(document.createElement("div"));1945 stepLbl.className = "gen-label";1946 stepLbl.textContent = stepKind === "email" ? "Now choose your email" : "Now choose your username";19471948 1949 const identities = (options.savedIdentities ?? [])1950 .filter(s => s.type === stepKind)1951 .sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0))1952 .slice(0, 6);19531954 const list = wrap.appendChild(document.createElement("div"));1955 list.className = "identity-list";1956 for (const id of identities) {1957 const row = list.appendChild(document.createElement("div"));1958 row.className = "peach-identity-row";1959 row.setAttribute("tabindex", "-1");1960 const val = row.appendChild(document.createElement("div"));1961 val.className = "peach-identity-value";1962 val.textContent = id.value;1963 const meta = row.appendChild(document.createElement("div"));1964 meta.className = "peach-identity-meta";1965 meta.textContent = `used ${id.useCount}×`;1966 row.addEventListener("click", () => applySecondIdentity(id.value));1967 }19681969 1970 const altInputWrap = wrap.appendChild(document.createElement("div"));1971 altInputWrap.className = "identity-alt-input-wrap open";1972 const altInput = altInputWrap.appendChild(document.createElement("input"));1973 altInput.className = "identity-alt-input";1974 altInput.type = stepKind === "email" ? "email" : "text";1975 altInput.placeholder = stepKind === "email" ? "[email protected]" : "your username";1976 altInput.setAttribute("autocomplete", "off");1977 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1978 altConfirm.className = "identity-alt-confirm";1979 altConfirm.type = "button";1980 altConfirm.textContent = "→";19811982 function applySecondIdentity(value: string) {1983 const trimmed = value.trim();1984 if (!trimmed) return;1985 1986 const [email, username] = stepKind === "email"1987 ? [trimmed, firstValue]1988 : [firstValue, trimmed];1989 if (stepKind === "email") {1990 options.onFillFields?.("", "", trimmed); 1991 } else {1992 options.onFillFields?.(trimmed, "", ""); 1993 }1994 renderGenerator(email, username);1995 }19961997 altConfirm.addEventListener("click", () => applySecondIdentity(altInput.value));1998 altInput.addEventListener("keydown", (e) => {1999 if (e.key === "Enter") { e.preventDefault(); applySecondIdentity(altInput.value); }2000 });20012002 2003 const skipRow = wrap.appendChild(document.createElement("div"));2004 skipRow.className = "identity-escape-row";2005 skipRow.textContent = `Skip ${stepKind} →`;2006 skipRow.setAttribute("tabindex", "0");2007 skipRow.addEventListener("click", () => {2008 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];2009 renderGenerator(email, username);2010 });2011 skipRow.addEventListener("keydown", (e) => {2012 if (e.key === "Enter" || e.key === " ") {2013 e.preventDefault();2014 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];2015 renderGenerator(email, username);2016 }2017 });20182019 requestAnimationFrame(() => { altInput.focus(); });2020 }20212022 function renderGenerator(filledEmail: string, filledUsername?: string) {2023 container.innerHTML = "";2024 const sitePolicy = detectPasswordPolicy();20252026 const wrap = container.appendChild(document.createElement("div"));2027 wrap.className = "gen-view";20282029 2030 const hdrRow = wrap.appendChild(document.createElement("div"));2031 hdrRow.className = "domain-header";2032 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));2033 eyebrow2.className = "domain-eyebrow";2034 eyebrow2.textContent = "NEW ACCOUNT";2035 const domName2 = hdrRow.appendChild(document.createElement("div"));2036 domName2.className = "domain-name";2037 domName2.textContent = securityOriginLabel;20382039 2040 function confirmedRow(value: string) {2041 const conf = wrap.appendChild(document.createElement("div"));2042 conf.className = "gen-confirmed";2043 const confCheck = conf.appendChild(document.createElement("span"));2044 confCheck.className = "gen-confirmed-check";2045 confCheck.textContent = "✓";2046 const confVal = conf.appendChild(document.createElement("div"));2047 confVal.className = "gen-confirmed-val";2048 confVal.textContent = value;2049 }2050 if (filledEmail) confirmedRow(filledEmail);2051 if (filledUsername && filledUsername !== filledEmail) confirmedRow(filledUsername);20522053 const lbl = wrap.appendChild(document.createElement("div"));2054 lbl.className = "gen-label";2055 lbl.textContent = "Generate a password";20562057 let currentPw = "";2058 const smartGen = createSmartGenerator({2059 policy: sitePolicy,2060 onChange: (pw) => { currentPw = pw; },2061 });2062 wrap.appendChild(smartGen.root);20632064 const fillBtn = wrap.appendChild(document.createElement("button"));2065 fillBtn.className = "gen-fill-btn";2066 fillBtn.type = "button";2067 fillBtn.textContent = "Copy & fill";2068 fillBtn.addEventListener("click", async () => {2069 if (!currentPw) return;2070 fillBtn.disabled = true;2071 fillBtn.textContent = "Saving…";2072 2073 options.onFillFields?.("", currentPw, "");2074 2075 2076 const saveUsername = filledUsername || filledEmail;2077 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;2078 try {2079 const res = await chrome.runtime.sendMessage({2080 type: "ADD_ENTRY",2081 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: currentPw, type: "login" },2082 });2083 if ((res as { ok?: boolean }).ok) {2084 showSuccess(sTitle);2085 } else {2086 renderSaveOffer(filledEmail, currentPw, filledUsername);2087 }2088 } catch {2089 renderSaveOffer(filledEmail, currentPw, filledUsername);2090 }2091 });2092 }20932094 function renderSaveOffer(filledEmail: string, filledPassword: string, filledUsername?: string) {2095 container.innerHTML = "";2096 const domain = securityOriginLabel;20972098 const wrap = container.appendChild(document.createElement("div"));2099 wrap.className = "save-offer";21002101 const title = wrap.appendChild(document.createElement("div"));2102 title.className = "save-offer-title";2103 title.textContent = "Save this login to Peach?";21042105 function detailRow(label: string, value: string) {2106 const row = wrap.appendChild(document.createElement("div"));2107 row.className = "save-offer-row";2108 const lbl = row.appendChild(document.createElement("div"));2109 lbl.className = "save-offer-lbl";2110 lbl.textContent = label;2111 const val = row.appendChild(document.createElement("div"));2112 val.className = "save-offer-val";2113 val.textContent = value;2114 }21152116 detailRow("site", domain);2117 2118 if (filledUsername) {2119 detailRow("username", filledUsername);2120 if (filledEmail) detailRow("email", filledEmail);2121 } else {2122 if (filledEmail) detailRow("email", filledEmail);2123 }2124 detailRow("password", "••••••••••••••••");21252126 const btns = wrap.appendChild(document.createElement("div"));2127 btns.className = "save-offer-btns";21282129 const saveBtn = btns.appendChild(document.createElement("button"));2130 saveBtn.className = "save-offer-save";2131 saveBtn.type = "button";2132 saveBtn.textContent = "Save";2133 saveBtn.addEventListener("click", async () => {2134 saveBtn.disabled = true;2135 saveBtn.textContent = "Saving…";2136 try {2137 2138 2139 const saveUsername = filledUsername || filledEmail;2140 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;2141 const res = await chrome.runtime.sendMessage({2142 type: "ADD_ENTRY",2143 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: filledPassword, type: "login" },2144 });2145 if ((res as { ok?: boolean }).ok) {2146 showSuccess(sTitle);2147 } else {2148 saveBtn.disabled = false;2149 saveBtn.textContent = "Save";2150 }2151 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }2152 });21532154 const skipBtn = btns.appendChild(document.createElement("button"));2155 skipBtn.className = "save-offer-skip";2156 skipBtn.type = "button";2157 skipBtn.textContent = "Not now";2158 skipBtn.addEventListener("click", () => { dismiss(true); });2159 }21602161 function renderList() {2162 container.innerHTML = "";2163 focusedIndex = -1;2164 searchInput = null;2165 currentMode = "list";21662167 const current = activeEntries();21682169 2170 2171 2172 const domHdr = document.createElement("div");2173 domHdr.className = "domain-header";2174 const headText = document.createElement("div");2175 headText.className = "domain-header-text";2176 const eyebrow = document.createElement("div");2177 eyebrow.className = "domain-eyebrow";2178 eyebrow.textContent = options.isSignup2179 ? "Sign up as"2180 : entries.length > 0 ? "Saved for this site" : "No saved login";2181 const domName = document.createElement("div");2182 domName.className = "domain-name";2183 domName.textContent = securityOriginLabel;2184 headText.append(eyebrow, domName);2185 domHdr.appendChild(headText);21862187 if (!options.isSignup) {2188 const actions = document.createElement("div");2189 actions.className = "domain-header-actions";21902191 2192 2193 2194 if (silverPlatterOrigin) {2195 const backBtn = document.createElement("button");2196 backBtn.className = "domain-search-btn";2197 backBtn.type = "button";2198 backBtn.setAttribute("aria-label", "Back to suggested entry");2199 backBtn.title = "Back to suggested entry";2200 backBtn.innerHTML = backSvgStr();2201 backBtn.addEventListener("click", (e) => {2202 e.stopPropagation();2203 if (silverPlatterOrigin) renderSilverPlatter(silverPlatterOrigin);2204 });2205 actions.appendChild(backBtn);2206 }22072208 const addBtn = document.createElement("button");2209 addBtn.className = "domain-add-btn";2210 addBtn.type = "button";2211 addBtn.setAttribute("aria-label", "Create new entry");2212 addBtn.title = "Create new entry";2213 addBtn.textContent = "+";2214 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2215 actions.appendChild(addBtn);22162217 domHdr.appendChild(actions);2218 }22192220 container.appendChild(domHdr);22212222 2223 const sw = document.createElement("div"); sw.className = "search-wrap";2224 searchInput = document.createElement("input");2225 searchInput.className = "search";2226 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";2227 searchInput.value = query;2228 searchInput.addEventListener("input", () => {2229 query = searchInput?.value ?? "";2230 2231 2232 void ensureSearchableLoaded(renderList);2233 renderList();2234 searchInput?.focus();2235 searchInput?.setSelectionRange(query.length, query.length);2236 });2237 sw.appendChild(searchInput);2238 2239 2240 if (searchableLoading) {2241 const loadingHint = document.createElement("span");2242 loadingHint.className = "search-loading";2243 loadingHint.textContent = "searching…";2244 sw.appendChild(loadingHint);2245 }2246 container.appendChild(sw);22472248 2249 const results = document.createElement("div");2250 results.className = "results";2251 container.appendChild(results);22522253 if (current.length === 0 && entries.length > 0) {2254 2255 }22562257 for (let i = 0; i < current.length; i++) {2258 const e = current[i]!;2259 const entryEl = document.createElement("div");2260 entryEl.className = "entry";2261 entryEl.dataset.id = e.id;2262 entryEl.dataset.index = String(i);2263 entryEl.setAttribute("tabindex", "-1");22642265 entryEl.appendChild(makeEntryIcon(e.url));22662267 const info = document.createElement("div"); info.className = "entry-info";2268 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;2269 const subEl = document.createElement("div"); subEl.className = "entry-sub";2270 const domain = e.url ? getDomainFromUrl(e.url) : "";2271 subEl.textContent = domain || e.username || "";2272 info.append(titleEl, subEl);2273 entryEl.appendChild(info);22742275 if (e.hasTotp) {2276 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";2277 entryEl.appendChild(badge);2278 }2279 if (e.hasPasskey) {2280 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";2281 entryEl.appendChild(badge);2282 }22832284 entryEl.addEventListener("click", () => {2285 if (options.isSignup && options.onFillFields) {2286 2287 options.onFillFields(e.username ?? "", "", e.email);2288 dismiss();2289 } else {2290 onSelect(e.id);2291 dismiss();2292 }2293 });2294 results.appendChild(entryEl);2295 }22962297 2298 if (current.length > 0) {2299 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));2300 }23012302 2303 const newEl = document.createElement("div");2304 newEl.className = "entry";2305 newEl.setAttribute("tabindex", "-1");2306 newEl.appendChild(makeEntryIcon(undefined, true));23072308 const newInfo = document.createElement("div"); newInfo.className = "entry-info";2309 const newTitle = document.createElement("div");2310 newTitle.className = "entry-title is-new";2311 newTitle.textContent = `New entry for ${sTitle}`;2312 const newSub = document.createElement("div");2313 newSub.className = "entry-sub";2314 newSub.textContent = sOrigin;2315 newInfo.append(newTitle, newSub);2316 newEl.appendChild(newInfo);2317 newEl.addEventListener("click", showCreateForm);2318 results.appendChild(newEl);23192320 2321 if (searchInput) {2322 requestAnimationFrame(() => {2323 searchInput?.focus();2324 2325 2326 searchInput?.setSelectionRange(query.length, query.length);2327 });2328 }2329 }23302331 function setFocused(idx: number) {2332 const all = container.querySelectorAll<HTMLElement>(".entry");2333 all.forEach((el, i) => el.classList.toggle("focused", i === idx));2334 focusedIndex = idx;2335 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });2336 }23372338 function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[], fieldType: "username" | "email"): void {2339 const typed = identities.filter(s => s.type === fieldType);2340 if (!typed.length) return;2341 let collapsed = false;2342 let suppressNextFocus = false;23432344 const list = g.appendChild(document.createElement("div"));2345 list.className = "identity-suggest-list";2346 list.style.display = "none";23472348 2349 const collapseRow = list.appendChild(document.createElement("div"));2350 collapseRow.className = "identity-filter-row";2351 collapseRow.style.justifyContent = "flex-end";2352 const collapseBtn = collapseRow.appendChild(document.createElement("button"));2353 collapseBtn.type = "button";2354 collapseBtn.className = "identity-collapse-btn";2355 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>`;2356 collapseBtn.addEventListener("mousedown", (e) => {2357 e.preventDefault();2358 collapsed = !collapsed;2359 collapseBtn.classList.toggle("is-collapsed", collapsed);2360 rowsWrapper.classList.toggle("is-collapsed", collapsed);2361 });23622363 const rowsWrapper = list.appendChild(document.createElement("div"));2364 rowsWrapper.className = "identity-rows";2365 const rows = rowsWrapper.appendChild(document.createElement("div"));2366 rows.className = "identity-rows-inner";23672368 2369 2370 2371 const updateRowsFade = () => {2372 const overflowing = rows.scrollHeight > rows.clientHeight + 1;2373 const atBottom = rows.scrollTop + rows.clientHeight >= rows.scrollHeight - 2;2374 const mask = overflowing && !atBottom2375 ? "linear-gradient(to bottom, black calc(100% - 18px), transparent 100%)"2376 : "none";2377 rows.style.setProperty("mask-image", mask);2378 rows.style.setProperty("-webkit-mask-image", mask);2379 };2380 rows.addEventListener("scroll", updateRowsFade);23812382 2383 function hideList() {2384 list.style.display = "none";2385 document.removeEventListener("mousedown", onDocMousedown, { capture: true });2386 }23872388 2389 2390 2391 2392 2393 function onDocMousedown(e: MouseEvent) {2394 if (e.composedPath().includes(host)) return;2395 hideList();2396 }23972398 function pick(value: string) {2399 inp.value = value;2400 hideList();2401 suppressNextFocus = true;2402 inp.focus();2403 2404 2405 2406 setTimeout(() => { suppressNextFocus = false; }, 120);2407 }24082409 function render() {2410 const q = inp.value.toLowerCase();2411 const filtered = typed.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 8);2412 rows.innerHTML = "";2413 if (!filtered.length) { hideList(); return; }2414 for (const s of filtered) {2415 const row = rows.appendChild(document.createElement("button"));2416 row.type = "button"; row.className = "identity-suggest-row";2417 const val = row.appendChild(document.createElement("span"));2418 val.className = "identity-suggest-val"; val.textContent = s.value;2419 const meta = row.appendChild(document.createElement("span"));2420 meta.className = "identity-suggest-meta"; meta.textContent = `${s.useCount}×`;2421 row.addEventListener("mousedown", (e) => { e.preventDefault(); pick(s.value); });2422 row.addEventListener("keydown", (e) => {2423 if (e.key === "ArrowDown") {2424 e.preventDefault();2425 (row.nextElementSibling as HTMLElement | null)?.focus();2426 } else if (e.key === "ArrowUp") {2427 e.preventDefault();2428 const prev = row.previousElementSibling as HTMLElement | null;2429 if (prev) prev.focus();2430 else inp.focus();2431 } else if (e.key === "Enter" || e.key === " ") {2432 e.preventDefault();2433 pick(s.value);2434 } else if (e.key === "Escape") {2435 e.preventDefault();2436 hideList();2437 suppressNextFocus = true;2438 inp.focus();2439 }2440 });2441 2442 2443 2444 row.addEventListener("blur", (e) => {2445 const rt = (e as FocusEvent).relatedTarget as Node | null;2446 if (rt && (rt === inp || list.contains(rt))) return;2447 setTimeout(() => { hideList(); }, 150);2448 });2449 }2450 list.style.display = "";2451 updateRowsFade();2452 2453 2454 2455 document.addEventListener("mousedown", onDocMousedown, { capture: true });2456 }24572458 inp.addEventListener("focus", () => {2459 if (suppressNextFocus) { suppressNextFocus = false; return; }2460 render();2461 });2462 inp.addEventListener("input", render);2463 inp.addEventListener("keydown", (e) => {2464 if (list.style.display === "none") return;2465 if (e.key === "ArrowDown" && !collapsed) {2466 e.preventDefault();2467 (rows.firstElementChild as HTMLElement | null)?.focus();2468 } else if (e.key === "Escape") {2469 e.preventDefault();2470 hideList();2471 } else if (e.key === "Tab") {2472 2473 hideList();2474 }2475 });2476 2477 2478 2479 2480 24812482 2483 2484 cleanupHooks.push(hideList);2485 }24862487 async function showCreateForm() {2488 currentMode = "create";2489 container.innerHTML = "";24902491 2492 2493 const sitePolicy = detectPasswordPolicy();24942495 const formIdentities = options.savedIdentities ?? [];24962497 const wrap = document.createElement("div");2498 wrap.className = "create-form-wrap";24992500 2501 const head = document.createElement("div"); head.className = "create-head";2502 const backBtn = document.createElement("button"); backBtn.className = "back-btn";2503 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>`;2504 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });2505 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";2506 head.append(backBtn, lbl);25072508 2509 const body = document.createElement("div"); body.className = "create-body";25102511 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {2512 const g = document.createElement("div"); g.className = "f-group";2513 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;2514 const inp = document.createElement("input"); inp.className = "f-input";2515 g.append(l, inp); return { g, inp };2516 }25172518 2519 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";2520 body.appendChild(tg.g);25212522 2523 const ug = fGroup("website");2524 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";2525 if (loginSubdomain) {2526 const h = document.createElement("div"); h.className = "url-hint";2527 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);2528 }2529 body.appendChild(ug.g);25302531 2532 const xg = fGroup("username");2533 xg.inp.value = preUsername && !preUsername.includes("@") ? preUsername : "";2534 xg.inp.placeholder = "johndoe";2535 xg.inp.setAttribute("autocomplete", "off");2536 if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities, "username");2537 body.appendChild(xg.g);25382539 2540 const eg = fGroup("email");2541 (eg.inp as HTMLInputElement).type = "email";2542 eg.inp.value = preUsername && preUsername.includes("@") ? preUsername : "";2543 eg.inp.placeholder = "[email protected]";2544 eg.inp.setAttribute("autocomplete", "off");2545 if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities, "email");2546 body.appendChild(eg.g);25472548 2549 2550 2551 const pg = fGroup("password");2552 pg.inp.remove(); 2553 let currentPassword = prePassword || "";2554 const smartGen = createSmartGenerator({2555 policy: sitePolicy,2556 initialPassword: prePassword || undefined,2557 onChange: (pw) => { currentPassword = pw; },2558 });2559 pg.g.appendChild(smartGen.root);2560 body.appendChild(pg.g);25612562 2563 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";2564 saveBtn.textContent = "Save entry";2565 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";2566 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }2567 saveBtn.addEventListener("click", async () => {2568 const title = tg.inp.value.trim();2569 const pw = currentPassword.trim();2570 const username = xg.inp.value.trim();2571 const email = eg.inp.value.trim();2572 if (!title) {2573 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();2574 showFormError("Entry needs a title");2575 return;2576 }2577 if (!pw) {2578 showFormError("Entry needs a password — tap regen to make one");2579 return;2580 }2581 errEl.style.display = "none";2582 saveBtn.disabled = true; saveBtn.textContent = "Saving…";2583 try {2584 const res = await chrome.runtime.sendMessage({2585 type: "ADD_ENTRY",2586 payload: { title, url: ug.inp.value.trim(), username, email: email || undefined, password: pw, type: "login" },2587 });2588 if ((res as { ok?: boolean }).ok) {2589 2590 2591 options.onFillFields?.(username || email, pw);2592 showSuccess(title);2593 } else {2594 const err = (res as { error?: string })?.error;2595 const errMsg = err === "vault_locked"2596 ? "Vault locked — open Peach to save"2597 : err === "STORAGE_QUOTA_EXCEEDED"2598 ? "Out of storage — open Peach → Settings to free space"2599 : err2600 ? `Could not save: ${err}`2601 : "Could not save entry";2602 showFormError(errMsg);2603 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2604 }2605 } catch (e) {2606 showFormError(e instanceof Error && e.message ? `Could not reach Peach: ${e.message}` : "Could not reach Peach");2607 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2608 }2609 });2610 body.appendChild(saveBtn);2611 body.appendChild(errEl);26122613 wrap.append(head, body);2614 container.appendChild(wrap);2615 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });2616 }26172618 function showSuccess(title: string) {2619 currentMode = "success";2620 container.innerHTML = "";2621 const wrap = document.createElement("div"); wrap.className = "success-wrap";2622 const ring = document.createElement("div"); ring.className = "success-ring";2623 ring.innerHTML = checkSvgStr();2624 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;2625 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;2626 wrap.append(ring, t, s);2627 container.appendChild(wrap);2628 setTimeout(() => dismiss(), 1600);2629 }26302631 function flashActionBtn(btn: HTMLButtonElement) {2632 btn.classList.add("flashed");2633 setTimeout(() => btn.classList.remove("flashed"), 700);2634 }26352636 2637 2638 function flashCopyError(btn: HTMLButtonElement) {2639 const original = btn.textContent ?? "";2640 btn.textContent = "Blocked";2641 btn.classList.add("flashed");2642 setTimeout(() => {2643 btn.classList.remove("flashed");2644 if (btn.isConnected) btn.textContent = original;2645 }, 1200);2646 }26472648 function renderSilverPlatter(entry: SelectorEntry) {2649 container.innerHTML = "";2650 searchInput = null;2651 focusedIndex = -1;2652 currentMode = "silver-platter";2653 silverPlatterOrigin = entry;26542655 const domHdr = document.createElement("div");2656 domHdr.className = "domain-header";2657 const headText = document.createElement("div");2658 headText.className = "domain-header-text";2659 const eyebrow = document.createElement("div");2660 eyebrow.className = "domain-eyebrow";2661 eyebrow.textContent = "Saved for this site";2662 const domName = document.createElement("div");2663 domName.className = "domain-name";2664 domName.textContent = securityOriginLabel;2665 headText.append(eyebrow, domName);2666 domHdr.appendChild(headText);2667 if (!options.isSignup) {2668 const actions = document.createElement("div");2669 actions.className = "domain-header-actions";26702671 2672 2673 2674 const searchBtn = document.createElement("button");2675 searchBtn.className = "domain-search-btn";2676 searchBtn.type = "button";2677 searchBtn.setAttribute("aria-label", "Search for a different entry");2678 searchBtn.title = "Search for a different entry";2679 searchBtn.innerHTML = searchSvgStr();2680 searchBtn.addEventListener("click", (e) => {2681 e.stopPropagation();2682 query = "";2683 renderList();2684 });2685 actions.appendChild(searchBtn);26862687 const addBtn = document.createElement("button");2688 addBtn.className = "domain-add-btn";2689 addBtn.type = "button";2690 addBtn.setAttribute("aria-label", "Create new entry");2691 addBtn.title = "Create new entry";2692 addBtn.textContent = "+";2693 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2694 actions.appendChild(addBtn);26952696 domHdr.appendChild(actions);2697 }2698 container.appendChild(domHdr);26992700 const platter = document.createElement("div");2701 platter.className = "silver-platter";2702 let capabilityActionClaimed = false;2703 const claimCapabilityAction = () => {2704 if (!options.capability) return true;2705 if (capabilityActionClaimed) return false;2706 capabilityActionClaimed = true;2707 return true;2708 };27092710 const titleEl = document.createElement("div");2711 titleEl.className = "sp-title";2712 titleEl.textContent = entry.title;2713 platter.appendChild(titleEl);27142715 if (entry.email) {2716 const emailEl = document.createElement("div");2717 emailEl.className = "sp-identity-row";2718 const emailLbl = document.createElement("span");2719 emailLbl.className = "sp-identity-label";2720 emailLbl.textContent = "email";2721 const emailVal = document.createElement("span");2722 emailVal.className = "sp-username";2723 emailVal.textContent = entry.email;2724 emailEl.append(emailLbl, emailVal);2725 platter.appendChild(emailEl);2726 }27272728 if (entry.username) {2729 const userEl = document.createElement("div");2730 userEl.className = "sp-identity-row";2731 const userLbl = document.createElement("span");2732 userLbl.className = "sp-identity-label";2733 userLbl.textContent = entry.username.includes("@") ? "email" : "username";2734 const userVal = document.createElement("span");2735 userVal.className = "sp-username";2736 userVal.textContent = entry.username;2737 userEl.append(userLbl, userVal);2738 platter.appendChild(userEl);2739 }27402741 const fillBtn = document.createElement("button");2742 fillBtn.className = "sp-fill-btn";2743 fillBtn.type = "button";2744 fillBtn.textContent = "Fill credentials";2745 fillBtn.addEventListener("click", () => {2746 if (fillBtn.disabled || !claimCapabilityAction()) return;2747 fillBtn.disabled = true;2748 fillBtn.classList.add("filled");2749 fillBtn.innerHTML = checkSvgStr();2750 2751 2752 2753 2754 onSelect(entry.id);2755 setTimeout(() => dismiss(), 400);2756 });2757 platter.appendChild(fillBtn);27582759 const actionsGroup = document.createElement("div");2760 actionsGroup.className = "sp-actions-group";2761 const actionsLabel = document.createElement("div");2762 actionsLabel.className = "sp-actions-label";2763 actionsLabel.textContent = "Copy";2764 actionsGroup.appendChild(actionsLabel);27652766 const actions = document.createElement("div");2767 actions.className = "sp-actions";2768 actionsGroup.appendChild(actions);27692770 if (entry.email) {2771 const copyEmail = document.createElement("button");2772 copyEmail.className = "sp-action-btn";2773 copyEmail.type = "button";2774 copyEmail.title = "Copy email";2775 copyEmail.textContent = "Email";2776 copyEmail.addEventListener("click", async (e) => {2777 e.stopPropagation();2778 let alreadyWritten = false;2779 try {2780 await navigator.clipboard.writeText(entry.email!);2781 alreadyWritten = true;2782 } catch (err) {2783 console.warn("[Peach] clipboard write denied:", err);2784 }2785 const resp = await chrome.runtime.sendMessage({2786 type: "COPY",2787 payload: { text: entry.email, label: "Email", alreadyWritten },2788 }).catch(() => null) as { ok?: boolean } | null;2789 if (alreadyWritten || resp?.ok) flashActionBtn(copyEmail);2790 else flashCopyError(copyEmail);2791 });2792 actions.appendChild(copyEmail);2793 }27942795 const copyU = document.createElement("button");2796 copyU.className = "sp-action-btn";2797 copyU.type = "button";2798 const usernameIsEmail = entry.username?.includes("@") ?? false;2799 copyU.title = usernameIsEmail ? "Copy email" : "Copy username";2800 copyU.textContent = usernameIsEmail ? "Email" : "Username";2801 copyU.disabled = !entry.username;2802 copyU.addEventListener("click", async (e) => {2803 e.stopPropagation();2804 if (!entry.username) return;2805 let alreadyWritten = false;2806 try {2807 await navigator.clipboard.writeText(entry.username);2808 alreadyWritten = true;2809 } catch (err) {2810 console.warn("[Peach] clipboard write denied:", err);2811 }2812 const resp = await chrome.runtime.sendMessage({2813 type: "COPY",2814 payload: { text: entry.username, label: entry.username.includes("@") ? "Email" : "Username", alreadyWritten },2815 }).catch(() => null) as { ok?: boolean } | null;2816 if (alreadyWritten || resp?.ok) flashActionBtn(copyU);2817 else flashCopyError(copyU);2818 });2819 actions.appendChild(copyU);28202821 const copyP = document.createElement("button");2822 copyP.className = "sp-action-btn";2823 copyP.type = "button";2824 copyP.title = "Copy password";2825 copyP.textContent = "Password";2826 copyP.addEventListener("click", async (e) => {2827 e.stopPropagation();2828 if (!claimCapabilityAction()) return;2829 const resp = await chrome.runtime.sendMessage({2830 type: "GET_ENTRY_FOR_FILL",2831 payload: { id: entry.id, capability: options.capability },2832 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;2833 if (!resp?.ok || !resp.data?.password) {2834 if (options.capability) dismiss();2835 return;2836 }2837 let alreadyWritten = false;2838 try {2839 await navigator.clipboard.writeText(resp.data.password);2840 alreadyWritten = true;2841 } catch (err) {2842 console.warn("[Peach] clipboard write denied:", err);2843 }2844 const copyResp = await chrome.runtime.sendMessage({2845 type: "COPY",2846 payload: { text: resp.data.password, label: "Password", alreadyWritten },2847 }).catch(() => null) as { ok?: boolean } | null;2848 if (alreadyWritten || copyResp?.ok) flashActionBtn(copyP);2849 else flashCopyError(copyP);2850 if (options.capability) dismiss();2851 });2852 actions.appendChild(copyP);28532854 let trayEl: HTMLDivElement | null = null;2855 let trayInterval: ReturnType<typeof setInterval> | null = null;28562857 function closeTray() {2858 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }2859 trayEl?.remove();2860 trayEl = null;2861 }2862 2863 2864 cleanupHooks.push(closeTray);28652866 async function openTOTPTray() {2867 2868 2869 2870 2871 async function fetchCode(): Promise<{ code: string; remaining: number } | null> {2872 const r = await chrome.runtime.sendMessage({2873 type: "GET_ENTRY_FOR_FILL",2874 payload: { id: entry.id, capability: options.capability },2875 }).catch(() => null) as { ok: boolean; data?: { totpCode?: string; totpRemaining?: number } } | null;2876 if (!r?.ok || !r.data?.totpCode) return null;2877 return { code: r.data.totpCode, remaining: r.data.totpRemaining ?? 30 };2878 }2879 let current = await fetchCode();2880 if (!current) return;28812882 trayEl = document.createElement("div");2883 trayEl.className = "sp-tray";28842885 const label = document.createElement("span");2886 label.className = "sp-tray-label";2887 label.textContent = "2FA";2888 trayEl.appendChild(label);28892890 const codeEl = document.createElement("div");2891 codeEl.className = "sp-totp-code";2892 trayEl.appendChild(codeEl);28932894 const ringNs = "http://www.w3.org/2000/svg";2895 const ring = document.createElementNS(ringNs, "svg");2896 ring.setAttribute("class", "sp-totp-ring");2897 ring.setAttribute("viewBox", "0 0 24 24");2898 const trackC = document.createElementNS(ringNs, "circle");2899 trackC.setAttribute("class", "ring-track");2900 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");2901 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");2902 const fillC = document.createElementNS(ringNs, "circle");2903 fillC.setAttribute("class", "ring-fill");2904 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");2905 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");2906 fillC.setAttribute("stroke-linecap", "round");2907 fillC.setAttribute("stroke-dasharray", "62.83");2908 ring.append(trackC, fillC);2909 trayEl.appendChild(ring);29102911 const copyBtn = document.createElement("button");2912 copyBtn.className = "sp-totp-copy";2913 copyBtn.type = "button";2914 copyBtn.textContent = "Copy";2915 trayEl.appendChild(copyBtn);29162917 function render() {2918 const { code, remaining } = current!;2919 2920 codeEl.textContent = code.length === 62921 ? `${code.slice(0, 3)} ${code.slice(3)}`2922 : code;2923 2924 2925 const fraction = (30 - remaining) / 30;2926 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));2927 }2928 render();2929 trayInterval = setInterval(async () => {2930 if (!current) return;2931 current = { code: current.code, remaining: current.remaining - 1 };2932 2933 if (current.remaining <= 0) {2934 const next = await fetchCode();2935 current = next ?? { code: current.code, remaining: 30 };2936 }2937 render();2938 }, 1000);29392940 copyBtn.addEventListener("click", async (e) => {2941 e.stopPropagation();2942 const code = current?.code;2943 if (!code) return;2944 let alreadyWritten = false;2945 try {2946 await navigator.clipboard.writeText(code);2947 alreadyWritten = true;2948 } catch (err) {2949 console.warn("[Peach] clipboard write denied:", err);2950 }2951 const resp = await chrome.runtime.sendMessage({2952 type: "COPY",2953 payload: { text: code, label: "TOTP", alreadyWritten },2954 }).catch(() => null) as { ok?: boolean } | null;2955 copyBtn.textContent = alreadyWritten || resp?.ok ? "Copied" : "Blocked";2956 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);2957 });29582959 platter.appendChild(trayEl);2960 }29612962 const editBtn = document.createElement("button");2963 editBtn.className = "sp-action-btn sp-action-edit";2964 editBtn.type = "button";2965 editBtn.title = "Edit entry";2966 editBtn.textContent = "Edit";2967 editBtn.addEventListener("click", (e) => {2968 e.stopPropagation();2969 closeTray();2970 void renderInlineEdit(entry);2971 });2972 actions.appendChild(editBtn);29732974 2975 2976 2977 if (entry.hasTotp && !options.capability) void openTOTPTray();29782979 platter.appendChild(actionsGroup);2980 container.appendChild(platter);2981 }29822983 2984 async function renderInlineEdit(summary: SelectorEntry) {2985 currentMode = "edit";2986 container.innerHTML = "";29872988 const wrap = document.createElement("div");2989 wrap.className = "create-form-wrap";2990 const head = document.createElement("div");2991 head.className = "create-head";2992 const back = document.createElement("button");2993 back.className = "back-btn";2994 back.type = "button";2995 back.title = "Cancel";2996 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>`;2997 back.addEventListener("click", () => renderSilverPlatter(summary));2998 const label = document.createElement("div");2999 label.className = "create-label";3000 label.textContent = "Edit entry";3001 head.append(back, label);3002 wrap.appendChild(head);30033004 const body = document.createElement("div");3005 body.className = "create-body";3006 const loading = document.createElement("div");3007 loading.className = "empty-hint";3008 loading.textContent = "loading…";3009 body.appendChild(loading);3010 wrap.appendChild(body);3011 container.appendChild(wrap);30123013 const response = await chrome.runtime.sendMessage({3014 type: "GET_ENTRY_FOR_FILL",3015 payload: { id: summary.id, capability: options.capability },3016 }).catch(() => null) as {3017 ok?: boolean;3018 data?: {3019 title?: string;3020 username?: string;3021 email?: string;3022 password?: string;3023 url?: string;3024 notes?: string;3025 };3026 } | null;3027 if (!response?.ok || !response.data) {3028 loading.textContent = "couldn't load this entry";3029 return;3030 }3031 body.innerHTML = "";30323033 function field(labelText: string, value: string, type: "text" | "password" = "text") {3034 const group = document.createElement("div");3035 group.className = "f-group";3036 const fieldLabel = document.createElement("div");3037 fieldLabel.className = "f-label";3038 fieldLabel.textContent = labelText;3039 const input = document.createElement("input");3040 input.className = "f-input";3041 input.type = type;3042 input.value = value;3043 input.autocomplete = type === "password" ? "new-password" : "off";3044 group.append(fieldLabel, input);3045 body.appendChild(group);3046 return input;3047 }30483049 const entry = response.data;3050 const titleInput = field("title", entry.title ?? summary.title);3051 const urlInput = field("url", entry.url ?? summary.url ?? "");3052 const usernameInput = field("username", entry.username ?? summary.username ?? "");3053 const emailInput = field("email", entry.email ?? summary.email ?? "");3054 const passwordInput = field("password", entry.password ?? "", "password");30553056 const generator = createSmartGenerator({3057 policy: detectPasswordPolicy(),3058 initialPassword: entry.password,3059 onChange: (password) => { passwordInput.value = password; },3060 });3061 generator.root.hidden = true;3062 const generatorToggle = document.createElement("button");3063 generatorToggle.className = "gen-toggle-link";3064 generatorToggle.type = "button";3065 generatorToggle.textContent = "⚙ generate";3066 generatorToggle.addEventListener("click", () => {3067 generator.root.hidden = !generator.root.hidden;3068 generatorToggle.textContent = generator.root.hidden ? "⚙ generate" : "☰ hide generator";3069 });3070 body.append(generatorToggle, generator.root);30713072 const notesGroup = document.createElement("div");3073 notesGroup.className = "f-group";3074 const notesLabel = document.createElement("div");3075 notesLabel.className = "f-label";3076 notesLabel.textContent = "notes";3077 const notesInput = document.createElement("textarea");3078 notesInput.className = "f-input";3079 notesInput.rows = 3;3080 notesInput.value = entry.notes ?? "";3081 notesGroup.append(notesLabel, notesInput);3082 body.appendChild(notesGroup);30833084 const actions = document.createElement("div");3085 actions.className = "f-row";3086 const save = document.createElement("button");3087 save.className = "save-btn";3088 save.type = "button";3089 save.textContent = "Save changes";3090 save.style.flex = "1";3091 const cancel = document.createElement("button");3092 cancel.className = "gen-btn";3093 cancel.type = "button";3094 cancel.textContent = "cancel";3095 cancel.addEventListener("click", () => renderSilverPlatter(summary));3096 actions.append(save, cancel);3097 body.appendChild(actions);30983099 save.addEventListener("click", async () => {3100 save.disabled = true;3101 save.textContent = "Saving…";3102 const patch = {3103 title: titleInput.value.trim(),3104 url: urlInput.value.trim(),3105 username: usernameInput.value.trim(),3106 email: emailInput.value.trim() || undefined,3107 password: passwordInput.value,3108 notes: notesInput.value,3109 };3110 const update = await chrome.runtime.sendMessage({3111 type: "UPDATE_ENTRY",3112 payload: { id: summary.id, patch },3113 }).catch(() => null) as { ok?: boolean; error?: string } | null;3114 if (!update?.ok) {3115 save.disabled = false;3116 save.textContent = "Save changes";3117 const error = document.createElement("div");3118 error.className = "empty-hint";3119 error.textContent = update?.error ?? "couldn't save this entry";3120 body.appendChild(error);3121 setTimeout(() => error.remove(), 3000);3122 return;3123 }3124 renderSilverPlatter({3125 ...summary,3126 title: patch.title,3127 url: patch.url,3128 username: patch.username,3129 email: patch.email,3130 });3131 });31323133 queueMicrotask(() => titleInput.focus());3134 }31353136 function renderLocked() {3137 container.innerHTML = "";31383139 3140 const domHdr = document.createElement("div");3141 domHdr.className = "domain-header";3142 const eyebrow = document.createElement("div");3143 eyebrow.className = "domain-eyebrow";3144 eyebrow.textContent = "vault locked";3145 const domName = document.createElement("div");3146 domName.className = "domain-name";3147 domName.textContent = securityOriginLabel;3148 domHdr.append(eyebrow, domName);3149 container.appendChild(domHdr);31503151 const wrap = document.createElement("div");3152 wrap.className = "locked-wrap";31533154 3155 const head = document.createElement("div"); head.className = "locked-head";3156 const icon = document.createElement("div"); icon.className = "locked-icon";3157 icon.innerHTML = keySvgStr();3158 const title = document.createElement("div"); title.className = "locked-title";3159 title.textContent = "unlock to autofill";3160 head.append(icon, title);31613162 3163 3164 3165 3166 3167 3168 3169 3170 const sub = document.createElement("div");3171 sub.className = "locked-sub";3172 sub.textContent = "Unlock in Peach — your master password is never typed on this page.";31733174 const openBtn = document.createElement("button");3175 openBtn.className = "locked-btn";3176 openBtn.type = "button";3177 openBtn.textContent = "Open Peach to unlock";3178 openBtn.addEventListener("click", () => {3179 chrome.runtime.sendMessage({ type: "OPEN_POPUP" }).catch(() => {});3180 openBtn.textContent = "opening Peach…";3181 openBtn.disabled = true;3182 3183 3184 setTimeout(() => {3185 if (openBtn.isConnected) {3186 openBtn.textContent = "Open Peach to unlock";3187 openBtn.disabled = false;3188 }3189 }, 2500);3190 });31913192 wrap.append(head, sub, openBtn);3193 container.appendChild(wrap);3194 }31953196 function handleKeyDown(e: KeyboardEvent) {3197 3198 3199 3200 if (!e.isTrusted) return;3201 if (options.locked) {3202 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3203 return;3204 }3205 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter" || currentMode === "identity-helper" || currentMode === "edit") {3206 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3207 return;3208 }3209 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");3210 const count = allEntryEls.length;3211 const activeElement = shadow.activeElement ?? document.activeElement;3212 if (activeElement === searchInput) {3213 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }3214 if (count === 0) return;3215 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;3216 }3217 if (count === 0) return;3218 switch (e.key) {3219 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;3220 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;3221 case "Enter": {3222 if (focusedIndex >= 0) {3223 const el = allEntryEls[focusedIndex] as HTMLElement;3224 const id = el.dataset.id;3225 if (id) { onSelect(id); dismiss(); }3226 else if (el.classList.contains("is-new") || !id) {3227 3228 showCreateForm();3229 }3230 }3231 break;3232 }3233 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;3234 }3235 }32363237 document.addEventListener("keydown", handleKeyDown);32383239 3240 3241 3242 3243 3244 const silverPlatterTypeHandler = (e: KeyboardEvent) => {3245 if (!e.isTrusted) return; 3246 if (currentMode !== "silver-platter") return;3247 if (e.ctrlKey || e.metaKey || e.altKey) return;3248 if (e.key.length !== 1) return;3249 e.preventDefault();3250 e.stopImmediatePropagation();3251 query = e.key;3252 3253 void ensureSearchableLoaded(renderList);3254 renderList();3255 };3256 document.addEventListener("keydown", silverPlatterTypeHandler, true);3257 cleanupHooks.push(() => document.removeEventListener("keydown", silverPlatterTypeHandler, true));32583259 const identityHelperTypeHandler = (e: KeyboardEvent) => {3260 if (!e.isTrusted) return; 3261 if (currentMode !== "identity-helper") return;3262 if (e.ctrlKey || e.metaKey || e.altKey) return;3263 const active = (shadow.activeElement ?? document.activeElement) as Element | null;3264 if (active === searchInput) return;3265 if (3266 active instanceof HTMLInputElement ||3267 active instanceof HTMLTextAreaElement ||3268 (active instanceof HTMLElement && active.isContentEditable)3269 ) {3270 return;3271 }3272 if (e.key.length !== 1) return;3273 e.preventDefault();3274 e.stopImmediatePropagation();3275 query = e.key;3276 renderIdentityHelper();3277 };3278 document.addEventListener("keydown", identityHelperTypeHandler, true);3279 cleanupHooks.push(() => document.removeEventListener("keydown", identityHelperTypeHandler, true));32803281 (modalAncestor ?? document.body).appendChild(host);32823283 3284 3285 3286 3287 3288 3289 3290 function establishesFixedContainingBlock(s: CSSStyleDeclaration): boolean {3291 const has = (p: string) => { const v = s.getPropertyValue(p); return v !== "" && v !== "none"; };3292 return (3293 has("transform") || has("perspective") || has("translate") ||3294 has("rotate") || has("scale") || has("filter") ||3295 has("backdrop-filter") || has("-webkit-backdrop-filter") ||3296 /\b(transform|perspective|filter)\b/.test(s.getPropertyValue("will-change")) ||3297 /\b(paint|layout|strict|content)\b/.test(s.getPropertyValue("contain"))3298 );3299 }3300 function containingBlockOffset(): { dx: number; dy: number } {3301 let el: HTMLElement | null = host.parentElement;3302 while (el && el !== document.documentElement) {3303 const s = getComputedStyle(el);3304 if (establishesFixedContainingBlock(s)) {3305 const r = el.getBoundingClientRect();3306 return {3307 dx: r.left + (parseFloat(s.borderLeftWidth) || 0),3308 dy: r.top + (parseFloat(s.borderTopWidth) || 0),3309 };3310 }3311 el = el.parentElement;3312 }3313 return { dx: 0, dy: 0 };3314 }33153316 function updatePosition() {3317 const r = anchorEl.getBoundingClientRect();3318 const { dx, dy } = containingBlockOffset();3319 host.style.top = `${r.bottom - dy + 5}px`;3320 host.style.left = `${r.left - dx}px`;3321 host.style.width = `${popupWidth}px`;3322 }3323 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);3324 updatePosition();33253326 3327 3328 3329 if (modalAncestor) {3330 modalAncestor.addEventListener("scroll", updatePosition, { passive: true });3331 cleanupHooks.push(() => modalAncestor.removeEventListener("scroll", updatePosition));3332 }33333334 3335 3336 3337 const removalObserver = new MutationObserver(() => {3338 if (!document.contains(anchorEl)) dismiss();3339 });3340 removalObserver.observe(document.body, { childList: true, subtree: true });3341 cleanupHooks.push(() => removalObserver.disconnect());33423343 if (options.locked) {3344 renderLocked();3345 } else if (options.isSignup && options.savedIdentities) {3346 3347 3348 renderIdentityHelper();3349 } else if (entries.length === 1 && !options.isSignup) {3350 3351 3352 renderSilverPlatter(entries[0]!);3353 } else {3354 renderList();3355 }33563357 let exitScheduled = false;3358 function dismiss(userAction = false) {3359 3360 3361 3362 if (exitScheduled) return;3363 exitScheduled = true;3364 3365 3366 3367 stopObserving();3368 document.removeEventListener("click", outsideClick, { capture: true });3369 document.removeEventListener("keydown", handleKeyDown);3370 for (const cleanup of cleanupHooks) {3371 try { cleanup(); } catch { }3372 }3373 if (userAction) options.onDismiss?.();3374 container.classList.add("is-exiting");3375 setTimeout(() => host.remove(), 130);3376 }33773378 function outsideClick(ev: MouseEvent) {3379 const path = ev.composedPath();3380 3381 3382 3383 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3384 }3385 3386 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);33873388 return dismiss;3389}33903391export function showTOTPSelector(3392 anchorEl: HTMLInputElement,3393 entries: SelectorEntry[],3394 onFill: (id: string) => void,3395 options: { domain?: string; onDismiss?: () => void } = {}3396): () => void {3397 const host = document.createElement("div");3398 host.style.cssText = "position: absolute; z-index: 2147483647;";3399 host.style.setProperty("--peach-accent", accentHex);3400 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));3401 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));3402 const shadow = host.attachShadow({ mode: "closed" });34033404 const style = shadow.appendChild(document.createElement("style"));3405 style.textContent = SELECTOR_STYLES;34063407 const rect = anchorEl.getBoundingClientRect();3408 host.style.top = `${rect.bottom + window.scrollY + 5}px`;3409 host.style.left = `${rect.left + window.scrollX}px`;3410 const popupWidth = Math.min(Math.max(rect.width, 300), 360);3411 host.style.width = `${popupWidth}px`;34123413 const container = shadow.appendChild(document.createElement("div"));3414 container.className = "peach-inline-popup";3415 container.setAttribute("data-theme", popupTheme);34163417 const domain = options.domain ?? location.hostname.replace(/^www\./, "");3418 const current = entries.filter((entry) => entry.hasTotp);3419 let focusedIndex = current.length === 1 ? 0 : -1;34203421 function updatePosition() {3422 const r = anchorEl.getBoundingClientRect();3423 host.style.top = `${r.bottom + window.scrollY + 5}px`;3424 host.style.left = `${r.left + window.scrollX}px`;3425 host.style.width = `${popupWidth}px`;3426 }34273428 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);34293430 function choose(entry: SelectorEntry) {3431 onFill(entry.id);3432 dismiss();3433 }34343435 function render() {3436 container.innerHTML = "";34373438 const domHdr = document.createElement("div");3439 domHdr.className = "domain-header";3440 const headText = document.createElement("div");3441 headText.className = "domain-header-text";3442 const eyebrow = document.createElement("div");3443 eyebrow.className = "domain-eyebrow";3444 eyebrow.textContent = "2FA code";3445 const domName = document.createElement("div");3446 domName.className = "domain-name";3447 domName.textContent = domain;3448 headText.append(eyebrow, domName);3449 domHdr.appendChild(headText);3450 container.appendChild(domHdr);34513452 if (current.length === 1) {3453 const entry = current[0]!;3454 const platter = document.createElement("div");3455 platter.className = "silver-platter";34563457 const title = document.createElement("div");3458 title.className = "sp-title";3459 title.textContent = entry.title;3460 platter.appendChild(title);34613462 const sub = document.createElement("div");3463 sub.className = "sp-username";3464 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3465 platter.appendChild(sub);34663467 const fill = document.createElement("button");3468 fill.className = "sp-fill-btn";3469 fill.type = "button";3470 fill.textContent = "Fill 2FA code";3471 fill.addEventListener("click", () => choose(entry));3472 platter.appendChild(fill);34733474 container.appendChild(platter);3475 requestAnimationFrame(() => fill.focus({ preventScroll: true }));3476 return;3477 }34783479 const results = document.createElement("div");3480 results.className = "results";3481 container.appendChild(results);34823483 for (let i = 0; i < current.length; i++) {3484 const entry = current[i]!;3485 const row = document.createElement("div");3486 row.className = `entry${i === focusedIndex ? " focused" : ""}`;3487 row.dataset.index = String(i);3488 row.setAttribute("tabindex", "-1");34893490 const icon = document.createElement("div");3491 icon.className = "entry-icon";3492 icon.innerHTML = keySvgStr();3493 row.appendChild(icon);3494 const info = document.createElement("div");3495 info.className = "entry-info";3496 const title = document.createElement("div");3497 title.className = "entry-title";3498 title.textContent = entry.title;3499 const sub = document.createElement("div");3500 sub.className = "entry-sub";3501 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3502 info.append(title, sub);3503 row.appendChild(info);35043505 const badge = document.createElement("span");3506 badge.className = "totp-badge";3507 badge.textContent = "Fill";3508 row.appendChild(badge);3509 row.addEventListener("click", () => choose(entry));3510 results.appendChild(row);3511 }3512 }35133514 function setFocused(index: number) {3515 focusedIndex = index;3516 container.querySelectorAll(".entry").forEach((el, i) => {3517 el.classList.toggle("focused", i === focusedIndex);3518 if (i === focusedIndex) (el as HTMLElement).scrollIntoView({ block: "nearest" });3519 });3520 }35213522 function handleKeyDown(e: KeyboardEvent) {3523 if (e.key === "Escape" || e.key === "Tab") {3524 e.preventDefault();3525 dismiss(true);3526 return;3527 }3528 if (current.length === 0) return;3529 if (e.key === "Enter") {3530 e.preventDefault();3531 choose(current[Math.max(0, focusedIndex)]!);3532 return;3533 }3534 if (current.length === 1) return;3535 if (e.key === "ArrowDown") {3536 e.preventDefault();3537 setFocused(focusedIndex < current.length - 1 ? focusedIndex + 1 : 0);3538 } else if (e.key === "ArrowUp") {3539 e.preventDefault();3540 setFocused(focusedIndex > 0 ? focusedIndex - 1 : current.length - 1);3541 }3542 }35433544 let exitScheduled = false;3545 function dismiss(userAction = false) {3546 if (exitScheduled) return;3547 exitScheduled = true;3548 stopObserving();3549 document.removeEventListener("click", outsideClick, { capture: true });3550 document.removeEventListener("keydown", handleKeyDown);3551 if (userAction) options.onDismiss?.();3552 container.classList.add("is-exiting");3553 setTimeout(() => host.remove(), 130);3554 }35553556 function outsideClick(ev: MouseEvent) {3557 const path = ev.composedPath();3558 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3559 }35603561 document.addEventListener("keydown", handleKeyDown);3562 document.body.appendChild(host);3563 render();3564 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);35653566 return dismiss;3567}35683569export interface CardSelectorEntry {3570 id: string;3571 title: string;3572 cardholderName?: string;3573 last4?: string;3574 expMonth?: string;3575 expYear?: string;3576 network?: import("../shared/types").CardNetwork;3577}35783579interface CardSelectorOptions {3580 domain?: string;3581 capability?: string;3582 3583 onFill: (entry: import("../shared/types").VaultEntry) => void;3584 3585 onDismiss?: () => void;3586}35873588358935903591359235933594export function showPasskeyProGate(): void {3595 const domain = location.hostname.replace(/^www\./, "");35963597 const host = document.createElement("div");3598 host.setAttribute("data-peach-passkey-gate", "1");3599 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";3600 const shadow = host.attachShadow({ mode: "closed" });36013602 const style = document.createElement("style");3603 style.textContent = `3604 :host { all: initial; }3605 .gate-wrap {3606 position: fixed;3607 top: 50%;3608 left: 50%;3609 transform: translate(-50%, -50%);3610 width: 280px;3611 pointer-events: auto;3612 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;3613 }3614 @keyframes g-in {3615 from { opacity:0; transform: translate(-50%,-52%); }3616 to { opacity:1; transform: translate(-50%,-50%); }3617 }3618 .gate-wrap.is-exiting {3619 animation: g-out 130ms ease-in forwards;3620 }3621 @keyframes g-out {3622 to { opacity:0; transform: translate(-50%,-52%); }3623 }3624 .gate {3625 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3626 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3627 border-radius: 12px;3628 box-shadow: 0 12px 48px rgba(0,0,0,0.75);3629 padding: 16px;3630 font-family: Inter, sans-serif;3631 }3632 .gate-header {3633 display: flex;3634 align-items: center;3635 gap: 7px;3636 margin-bottom: 12px;3637 padding-bottom: 10px;3638 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3639 }3640 .gate-domain {3641 font-size: 12px;3642 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3643 font-family: "IBM Plex Mono", monospace;3644 overflow: hidden;3645 text-overflow: ellipsis;3646 white-space: nowrap;3647 }3648 .gate-title {3649 font-size: 14px;3650 font-weight: 500;3651 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3652 margin-bottom: 6px;3653 }3654 .gate-subtitle {3655 font-size: 12px;3656 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};3657 line-height: 1.5;3658 margin-bottom: 14px;3659 }3660 .gate-btn {3661 display: block;3662 width: 100%;3663 padding: 9px 0;3664 border-radius: 8px;3665 border: none;3666 cursor: pointer;3667 font-size: 13px;3668 font-family: Inter, sans-serif;3669 font-weight: 500;3670 transition: opacity 100ms;3671 margin-bottom: 7px;3672 text-align: center;3673 }3674 .gate-btn:last-child { margin-bottom: 0; }3675 .gate-btn--accent {3676 background: ${accentHex};3677 color: #fff;3678 }3679 .gate-btn--accent:hover { opacity: 0.88; }3680 .gate-btn--ghost {3681 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3682 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};3683 }3684 .gate-btn--ghost:hover { opacity: 0.75; }3685 .backdrop {3686 position: fixed;3687 inset: 0;3688 pointer-events: auto;3689 }3690 `;3691 shadow.appendChild(style);36923693 const backdrop = document.createElement("div");3694 backdrop.className = "backdrop";3695 shadow.appendChild(backdrop);36963697 const wrap = document.createElement("div");3698 wrap.className = "gate-wrap";3699 shadow.appendChild(wrap);37003701 const gate = document.createElement("div");3702 gate.className = "gate";3703 wrap.appendChild(gate);37043705 3706 const header = document.createElement("div");3707 header.className = "gate-header";3708 const icon = document.createElement("span");3709 icon.innerHTML = peachIconSvg();3710 const domainEl = document.createElement("span");3711 domainEl.className = "gate-domain";3712 domainEl.textContent = domain;3713 header.append(icon, domainEl);3714 gate.appendChild(header);37153716 const title = document.createElement("div");3717 title.className = "gate-title";3718 title.textContent = "Passkeys are part of Peach Pro.";3719 gate.appendChild(title);37203721 const subtitle = document.createElement("div");3722 subtitle.className = "gate-subtitle";3723 subtitle.textContent = "Store and use passkeys across all your devices, securely.";3724 gate.appendChild(subtitle);37253726 const upgradeBtn = document.createElement("button");3727 upgradeBtn.className = "gate-btn gate-btn--accent";3728 upgradeBtn.type = "button";3729 upgradeBtn.textContent = "Upgrade to Pro";3730 gate.appendChild(upgradeBtn);37313732 const laterBtn = document.createElement("button");3733 laterBtn.className = "gate-btn gate-btn--ghost";3734 laterBtn.type = "button";3735 laterBtn.textContent = "Not right now";3736 gate.appendChild(laterBtn);37373738 function dismiss() {3739 wrap.classList.add("is-exiting");3740 setTimeout(() => host.remove(), 130);3741 }37423743 upgradeBtn.addEventListener("click", () => {3744 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});3745 dismiss();3746 });3747 laterBtn.addEventListener("click", dismiss);3748 backdrop.addEventListener("click", dismiss);37493750 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {3751 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }3752 });37533754 document.body.appendChild(host);3755}375637573758375937603761376237633764export function showPasskeyRegistrationOffer(args: {3765 rpId: string;3766 rpName?: string;3767 userName?: string;3768 userDisplayName?: string;3769}): void {3770 const domain = location.hostname.replace(/^www\./, "");3771 const displayName = args.rpName || domain;37723773 const host = document.createElement("div");3774 host.setAttribute("data-peach-passkey-offer", "1");3775 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";3776 const shadow = host.attachShadow({ mode: "closed" });37773778 const style = document.createElement("style");3779 style.textContent = `3780 :host { all: initial; }3781 .offer-wrap {3782 position: fixed;3783 top: 16px;3784 right: 16px;3785 width: 320px;3786 pointer-events: auto;3787 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;3788 }3789 @keyframes o-in {3790 from { opacity: 0; transform: translateY(-6px); }3791 to { opacity: 1; transform: translateY(0); }3792 }3793 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }3794 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }3795 .offer {3796 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3797 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3798 border-radius: 12px;3799 box-shadow: 0 12px 48px rgba(0,0,0,0.55);3800 padding: 14px 14px 12px;3801 font-family: Inter, sans-serif;3802 position: relative;3803 }3804 .offer-close {3805 position: absolute;3806 top: 8px;3807 right: 8px;3808 width: 18px;3809 height: 18px;3810 border: none;3811 background: transparent;3812 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};3813 cursor: pointer;3814 font-size: 14px;3815 line-height: 1;3816 padding: 0;3817 }3818 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }3819 .offer-header {3820 display: flex;3821 align-items: center;3822 gap: 7px;3823 margin-bottom: 8px;3824 padding-bottom: 8px;3825 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3826 }3827 .offer-domain {3828 font-size: 12px;3829 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3830 font-family: "IBM Plex Mono", monospace;3831 overflow: hidden;3832 text-overflow: ellipsis;3833 white-space: nowrap;3834 }3835 .offer-title {3836 font-size: 13px;3837 font-weight: 500;3838 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3839 margin-bottom: 4px;3840 }3841 .offer-subtitle {3842 font-size: 11px;3843 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};3844 line-height: 1.45;3845 margin-bottom: 12px;3846 }3847 .offer-btn {3848 display: block;3849 width: 100%;3850 padding: 8px 0;3851 border-radius: 7px;3852 border: none;3853 cursor: pointer;3854 font-size: 12px;3855 font-family: Inter, sans-serif;3856 font-weight: 500;3857 transition: opacity 100ms;3858 margin-bottom: 6px;3859 text-align: center;3860 }3861 .offer-btn:last-child { margin-bottom: 0; }3862 .offer-btn--accent { background: ${accentHex}; color: #fff; }3863 .offer-btn--accent:hover { opacity: 0.88; }3864 .offer-btn--ghost {3865 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3866 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};3867 }3868 .offer-btn--ghost:hover { opacity: 0.75; }3869 `;3870 shadow.appendChild(style);38713872 const wrap = document.createElement("div");3873 wrap.className = "offer-wrap";3874 shadow.appendChild(wrap);38753876 const offer = document.createElement("div");3877 offer.className = "offer";3878 wrap.appendChild(offer);38793880 const closeBtn = document.createElement("button");3881 closeBtn.className = "offer-close";3882 closeBtn.type = "button";3883 closeBtn.setAttribute("aria-label", "Dismiss");3884 closeBtn.textContent = "✕";3885 offer.appendChild(closeBtn);38863887 const header = document.createElement("div");3888 header.className = "offer-header";3889 const icon = document.createElement("span");3890 icon.innerHTML = peachIconSvg();3891 const domainEl = document.createElement("span");3892 domainEl.className = "offer-domain";3893 domainEl.textContent = domain;3894 header.append(icon, domainEl);3895 offer.appendChild(header);38963897 const title = document.createElement("div");3898 title.className = "offer-title";3899 title.textContent = "This site supports passkeys.";3900 offer.appendChild(title);39013902 const subtitle = document.createElement("div");3903 subtitle.className = "offer-subtitle";3904 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";3905 offer.appendChild(subtitle);39063907 const saveBtn = document.createElement("button");3908 saveBtn.className = "offer-btn offer-btn--accent";3909 saveBtn.type = "button";3910 saveBtn.textContent = "Save passkey to Peach";3911 offer.appendChild(saveBtn);39123913 const laterBtn = document.createElement("button");3914 laterBtn.className = "offer-btn offer-btn--ghost";3915 laterBtn.type = "button";3916 laterBtn.textContent = "Not now";3917 offer.appendChild(laterBtn);39183919 function dismiss(decline: boolean) {3920 if (decline) {3921 chrome.runtime.sendMessage({3922 type: "PASSKEY_OFFER_DECLINE",3923 payload: { domain },3924 }).catch(() => {});3925 }3926 wrap.classList.add("is-exiting");3927 setTimeout(() => host.remove(), 130);3928 }39293930 saveBtn.addEventListener("click", () => {3931 chrome.runtime.sendMessage({3932 type: "INITIATE_FIREFOX_PASSKEY_CREATE",3933 payload: {3934 rpId: args.rpId,3935 rpName: args.rpName ?? displayName,3936 userName: args.userName ?? "user",3937 userDisplayName: args.userDisplayName,3938 },3939 }).catch(() => {});3940 dismiss(false);3941 });3942 laterBtn.addEventListener("click", () => dismiss(true));3943 closeBtn.addEventListener("click", () => dismiss(true));39443945 document.body.appendChild(host);3946}3947394839493950395139523953395439553956export function showPasskeyAvailable(args: {3957 rpId: string;3958 userName: string;3959 fromBrowser?: "chrome" | "firefox";3960 currentBrowser?: "chrome" | "firefox" | "unknown";3961}): void {3962 const domain = location.hostname.replace(/^www\./, "");3963 const crossBrowser =3964 args.fromBrowser &&3965 args.currentBrowser &&3966 args.currentBrowser !== "unknown" &&3967 args.fromBrowser !== args.currentBrowser;39683969 const host = document.createElement("div");3970 host.setAttribute("data-peach-passkey-available", "1");3971 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";3972 const shadow = host.attachShadow({ mode: "closed" });39733974 const style = document.createElement("style");3975 style.textContent = `3976 :host { all: initial; }3977 .avail-wrap {3978 position: fixed;3979 top: 16px;3980 right: 16px;3981 width: 320px;3982 pointer-events: auto;3983 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;3984 }3985 @keyframes a-in {3986 from { opacity: 0; transform: translateY(-6px); }3987 to { opacity: 1; transform: translateY(0); }3988 }3989 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }3990 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }3991 .avail {3992 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3993 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3994 border-radius: 12px;3995 box-shadow: 0 12px 48px rgba(0,0,0,0.55);3996 padding: 14px 14px 12px;3997 font-family: Inter, sans-serif;3998 position: relative;3999 }4000 .avail-close {4001 position: absolute;4002 top: 8px;4003 right: 8px;4004 width: 18px;4005 height: 18px;4006 border: none;4007 background: transparent;4008 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};4009 cursor: pointer;4010 font-size: 14px;4011 line-height: 1;4012 padding: 0;4013 }4014 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }4015 .avail-header {4016 display: flex;4017 align-items: center;4018 gap: 7px;4019 margin-bottom: 8px;4020 padding-bottom: 8px;4021 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};4022 }4023 .avail-domain {4024 font-size: 12px;4025 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};4026 font-family: "IBM Plex Mono", monospace;4027 }4028 .avail-label {4029 font-size: 9px;4030 letter-spacing: 0.16em;4031 text-transform: uppercase;4032 color: ${accentHex};4033 margin-bottom: 4px;4034 }4035 .avail-user {4036 font-size: 13px;4037 font-weight: 500;4038 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};4039 margin-bottom: 12px;4040 overflow: hidden;4041 text-overflow: ellipsis;4042 white-space: nowrap;4043 }4044 .avail-cross {4045 font-size: 11px;4046 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};4047 line-height: 1.45;4048 margin-bottom: 12px;4049 padding: 8px;4050 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};4051 border-radius: 6px;4052 }4053 .avail-btn {4054 display: block;4055 width: 100%;4056 padding: 8px 0;4057 border-radius: 7px;4058 border: none;4059 cursor: pointer;4060 font-size: 12px;4061 font-family: Inter, sans-serif;4062 font-weight: 500;4063 transition: opacity 100ms;4064 margin-bottom: 6px;4065 text-align: center;4066 }4067 .avail-btn:last-child { margin-bottom: 0; }4068 .avail-btn--accent { background: ${accentHex}; color: #fff; }4069 .avail-btn--accent:hover { opacity: 0.88; }4070 .avail-btn--ghost {4071 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};4072 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};4073 }4074 .avail-btn--ghost:hover { opacity: 0.75; }4075 `;4076 shadow.appendChild(style);40774078 const wrap = document.createElement("div");4079 wrap.className = "avail-wrap";4080 shadow.appendChild(wrap);40814082 const avail = document.createElement("div");4083 avail.className = "avail";4084 wrap.appendChild(avail);40854086 const closeBtn = document.createElement("button");4087 closeBtn.className = "avail-close";4088 closeBtn.type = "button";4089 closeBtn.setAttribute("aria-label", "Dismiss");4090 closeBtn.textContent = "✕";4091 avail.appendChild(closeBtn);40924093 const header = document.createElement("div");4094 header.className = "avail-header";4095 const icon = document.createElement("span");4096 icon.innerHTML = peachIconSvg();4097 const domainEl = document.createElement("span");4098 domainEl.className = "avail-domain";4099 domainEl.textContent = domain;4100 header.append(icon, domainEl);4101 avail.appendChild(header);41024103 const label = document.createElement("div");4104 label.className = "avail-label";4105 label.textContent = "PASSKEY";4106 avail.appendChild(label);41074108 const user = document.createElement("div");4109 user.className = "avail-user";4110 user.textContent = args.userName;4111 avail.appendChild(user);41124113 if (crossBrowser) {4114 const cross = document.createElement("div");4115 cross.className = "avail-cross";4116 cross.textContent =4117 args.fromBrowser === "chrome"4118 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."4119 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";4120 avail.appendChild(cross);4121 }41224123 const signInBtn = document.createElement("button");4124 signInBtn.className = "avail-btn avail-btn--accent";4125 signInBtn.type = "button";4126 signInBtn.textContent = "Sign in with passkey";4127 avail.appendChild(signInBtn);41284129 const passwordBtn = document.createElement("button");4130 passwordBtn.className = "avail-btn avail-btn--ghost";4131 passwordBtn.type = "button";4132 passwordBtn.textContent = "Use password instead";4133 avail.appendChild(passwordBtn);41344135 function dismiss() {4136 wrap.classList.add("is-exiting");4137 setTimeout(() => host.remove(), 130);4138 }41394140 signInBtn.addEventListener("click", () => {4141 4142 4143 4144 4145 4146 dismiss();4147 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});4148 });4149 passwordBtn.addEventListener("click", dismiss);4150 closeBtn.addEventListener("click", dismiss);41514152 document.body.appendChild(host);4153}415441554156415741584159416041614162export function showCardSelector(4163 anchorEl: HTMLInputElement,4164 cards: CardSelectorEntry[],4165 options: CardSelectorOptions4166): () => void {4167 if (cards.length === 0) return () => {};4168 const card = cards[0]!;4169 let capabilityActionClaimed = false;4170 const claimCapabilityAction = () => {4171 if (!options.capability) return true;4172 if (capabilityActionClaimed) return false;4173 capabilityActionClaimed = true;4174 return true;4175 };41764177 const host = document.createElement("div");4178 host.style.cssText = "position: absolute; z-index: 2147483647;";4179 host.style.setProperty("--peach-accent", accentHex);4180 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));4181 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));4182 const shadow = host.attachShadow({ mode: "closed" });41834184 const style = shadow.appendChild(document.createElement("style"));4185 style.textContent = SELECTOR_STYLES;41864187 const rect = anchorEl.getBoundingClientRect();4188 host.style.top = `${rect.bottom + window.scrollY + 5}px`;4189 host.style.left = `${rect.left + window.scrollX}px`;4190 host.style.width = `${Math.min(Math.max(rect.width, 320), 400)}px`;41914192 const container = shadow.appendChild(document.createElement("div"));4193 container.className = "peach-inline-popup";4194 container.setAttribute("data-theme", popupTheme);41954196 4197 const domHdr = document.createElement("div");4198 domHdr.className = "domain-header";4199 const eyebrow = document.createElement("div");4200 eyebrow.className = "domain-eyebrow";4201 eyebrow.textContent = "Pay with Peach";4202 const domName = document.createElement("div");4203 domName.className = "domain-name";4204 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");4205 domHdr.append(eyebrow, domName);4206 container.appendChild(domHdr);42074208 const platter = document.createElement("div");4209 platter.className = "card-platter";42104211 if (card.network && card.network !== "unknown") {4212 const net = document.createElement("div");4213 net.className = "cp-network";4214 net.textContent = card.network;4215 platter.appendChild(net);4216 }42174218 const nick = document.createElement("div");4219 nick.className = "cp-nickname";4220 nick.textContent = card.title;4221 platter.appendChild(nick);42224223 if (card.last4) {4224 const l4 = document.createElement("div");4225 l4.className = "cp-last4";4226 l4.textContent = `•••• ${card.last4}`;4227 platter.appendChild(l4);4228 }42294230 const meta = document.createElement("div");4231 meta.className = "cp-meta";4232 if (card.cardholderName) {4233 const name = document.createElement("span");4234 name.textContent = card.cardholderName;4235 meta.appendChild(name);4236 }4237 if (card.expMonth && card.expYear) {4238 if (card.cardholderName) {4239 const sep = document.createElement("span");4240 sep.className = "cp-meta-sep";4241 sep.textContent = "·";4242 meta.appendChild(sep);4243 }4244 const exp = document.createElement("span");4245 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;4246 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;4247 meta.appendChild(exp);4248 }4249 if (meta.childElementCount > 0) platter.appendChild(meta);42504251 const fillWrap = document.createElement("div");4252 fillWrap.className = "cp-fill-wrap";4253 const fillBtn = document.createElement("button");4254 fillBtn.className = "sp-fill-btn";4255 fillBtn.type = "button";4256 fillBtn.textContent = "Fill card details";4257 fillBtn.addEventListener("click", async () => {4258 if (fillBtn.disabled || !claimCapabilityAction()) return;4259 fillBtn.disabled = true;4260 fillBtn.classList.add("filled");4261 fillBtn.innerHTML = checkSvgStr();4262 const resp = await chrome.runtime.sendMessage({4263 type: "GET_ENTRY_FOR_FILL",4264 4265 payload: { id: card.id, capability: options.capability },4266 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4267 if (resp?.ok && resp.data) options.onFill(resp.data);4268 setTimeout(() => dismiss(), 400);4269 });4270 fillWrap.appendChild(fillBtn);4271 platter.appendChild(fillWrap);42724273 4274 const actions = document.createElement("div");4275 actions.className = "sp-actions";42764277 function flashBtn(b: HTMLButtonElement) {4278 b.classList.add("flashed");4279 setTimeout(() => b.classList.remove("flashed"), 700);4280 }42814282 4283 function flashCopyError(b: HTMLButtonElement) {4284 const original = b.textContent ?? "";4285 b.textContent = "Blocked";4286 b.classList.add("flashed");4287 setTimeout(() => {4288 b.classList.remove("flashed");4289 if (b.isConnected) b.textContent = original;4290 }, 1200);4291 }42924293 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {4294 if (!claimCapabilityAction()) return null;4295 const resp = await chrome.runtime.sendMessage({4296 type: "GET_ENTRY_FOR_FILL",4297 4298 payload: { id: card.id, capability: options.capability },4299 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4300 const full = resp?.ok && resp.data ? resp.data : null;4301 if (options.capability) dismiss();4302 return full;4303 }43044305 const copyNum = document.createElement("button");4306 copyNum.className = "sp-action-btn";4307 copyNum.type = "button";4308 copyNum.title = "Copy card number";4309 copyNum.textContent = "Copy number";4310 copyNum.addEventListener("click", async (e) => {4311 e.stopPropagation();4312 const full = await fetchFullCard();4313 if (!full?.cardNumber) return;4314 let alreadyWritten = false;4315 try {4316 await navigator.clipboard.writeText(full.cardNumber);4317 alreadyWritten = true;4318 } catch (err) {4319 console.warn("[Peach] clipboard write denied:", err);4320 }4321 const resp = await chrome.runtime.sendMessage({4322 type: "COPY",4323 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },4324 }).catch(() => null) as { ok?: boolean } | null;4325 if (alreadyWritten || resp?.ok) flashBtn(copyNum);4326 else flashCopyError(copyNum);4327 });4328 actions.appendChild(copyNum);43294330 const copyCvc = document.createElement("button");4331 copyCvc.className = "sp-action-btn";4332 copyCvc.type = "button";4333 copyCvc.title = "Copy CVC";4334 copyCvc.textContent = "Copy CVC";4335 copyCvc.addEventListener("click", async (e) => {4336 e.stopPropagation();4337 const full = await fetchFullCard();4338 if (!full?.cvc) return;4339 let alreadyWritten = false;4340 try {4341 await navigator.clipboard.writeText(full.cvc);4342 alreadyWritten = true;4343 } catch (err) {4344 console.warn("[Peach] clipboard write denied:", err);4345 }4346 const resp = await chrome.runtime.sendMessage({4347 type: "COPY",4348 payload: { text: full.cvc, label: "CVC", alreadyWritten },4349 }).catch(() => null) as { ok?: boolean } | null;4350 if (alreadyWritten || resp?.ok) flashBtn(copyCvc);4351 else flashCopyError(copyCvc);4352 });4353 actions.appendChild(copyCvc);43544355 const closeBtn = document.createElement("button");4356 closeBtn.className = "sp-action-btn";4357 closeBtn.type = "button";4358 closeBtn.title = "Dismiss";4359 closeBtn.textContent = "✕";4360 closeBtn.addEventListener("click", (e) => {4361 e.stopPropagation();4362 dismiss(true);4363 });4364 actions.appendChild(closeBtn);43654366 platter.appendChild(actions);4367 container.appendChild(platter);43684369 document.body.appendChild(host);4370 function updatePosition() {4371 const r = anchorEl.getBoundingClientRect();4372 host.style.top = `${r.bottom + window.scrollY + 5}px`;4373 host.style.left = `${r.left + window.scrollX}px`;4374 }4375 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);43764377 function onKeyDown(e: KeyboardEvent) {4378 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }4379 }4380 document.addEventListener("keydown", onKeyDown);43814382 let exitScheduled = false;4383 function dismiss(userAction = false) {4384 if (exitScheduled) return;4385 exitScheduled = true;4386 stopObserving();4387 document.removeEventListener("click", outsideClick, { capture: true });4388 document.removeEventListener("keydown", onKeyDown);4389 if (userAction) options.onDismiss?.();4390 container.classList.add("is-exiting");4391 setTimeout(() => host.remove(), 130);4392 }43934394 function outsideClick(ev: MouseEvent) {4395 const path = ev.composedPath();4396 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);4397 }4398 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);43994400 return dismiss;4401}