123456789101112import { ACCENT_COLORS, type PasswordPolicy, type PasswordHistoryItem, type SavedIdentity } from "../shared/types";13import { generatePassword } from "../shared/generate";14import { detectPasswordPolicy } from "./policy";15import {16 filterSavedIdentities,17 identityFilterLabel,18 resolveCustomIdentityKind,19 type IdentityFilterMode,20 type IdentityKind,21} from "./identity-helper";22232425function timeSince(when: number): string {26 const secs = Math.max(0, Math.floor((Date.now() - when) / 1000));27 if (secs < 60) return "just now";28 const mins = Math.floor(secs / 60);29 if (mins < 60) return `${mins}m ago`;30 const hours = Math.floor(mins / 60);31 if (hours < 24) return `${hours}h ago`;32 const days = Math.floor(hours / 24);33 if (days < 30) return `${days}d ago`;34 const months = Math.floor(days / 30);35 if (months < 12) return `${months}mo ago`;36 return `${Math.floor(months / 12)}y ago`;37}3839let accentHex = "#E8956D";40let popupTheme: "dark" | "light" = "dark";41chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string; theme?: string } } | undefined) => {42 const data = resp?.data;43 const name = data?.accentColor ?? "peach";44 accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";45 popupTheme = data?.theme === "light" ? "light" : "dark";46});4748495051function withAlpha(hex: string, alpha: string): string {52 53 54 55 return /^#[0-9a-f]{6}$/i.test(hex) ? hex + alpha : hex;56}5758export interface SelectorEntry {59 id: string;60 title: string;61 username?: string;62 email?: string;63 url?: string;64 hasTotp?: boolean;65 66 hasPasskey?: boolean;67}6869interface SelectorOptions {70 domain?: string;71 searchableEntries?: SelectorEntry[];72 737475767778 loadSearchableEntries?: () => Promise<SelectorEntry[]>;79 80 isSignup?: boolean;81 82838485 identityKind?: IdentityKind;86 87888990 savedIdentities?: SavedIdentity[];91 9293949596 onFillFields?: (username: string, password: string, email?: string) => void;97 9899100101102103 hasEmailField?: boolean;104 105106107108109 locked?: boolean;110 111112113114 onUnlocked?: () => void;115 116117118119120 onDismiss?: () => void;121}122123function getDomainFromUrl(url: string): string {124 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }125}126127function keySvgStr(): string {128 return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none" stroke="rgba(240,232,216,0.35)" stroke-width="0.95" stroke-linecap="round"><rect x="2" y="5.5" width="8" height="6" rx="1.5"/><path d="M4 5.5V4.25A2 2 0 0 1 6 2.25A2 2 0 0 1 8 4.25V5.5"/></svg>`;129}130131function plusSvgStr(): string {132 return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><path d="M6 1.5v9M1.5 6h9"/></svg>`;133}134135function searchSvgStr(): string {136 return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><circle cx="5.25" cy="5.25" r="3.5"/><path d="M7.9 7.9l2.35 2.35"/></svg>`;137}138139function backSvgStr(): string {140 return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 2.5L3.5 6l4 3.5"/></svg>`;141}142143function peachIconSvg(): string {144 145 146 147 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 240 240" fill="none" aria-hidden="true"><path d="M 124 64 C 134 32 168 16 200 22 C 198 56 168 72 130 66 Z" fill="#92AC65"/><path d="M 130 64 C 152 46 178 30 196 24" stroke="#5E7A42" stroke-width="2.6" fill="none" stroke-linecap="round"/><path d="M 120 60 C 75 45 28 75 22 130 C 18 185 65 222 125 222 C 185 222 222 185 220 130 C 218 78 170 45 120 60 Z" fill="#FFB082"/><path d="M 120 60 C 75 45 28 75 22 130 C 18 185 65 222 125 222 C 105 195 75 150 100 90 C 105 75 112 65 120 60 Z" fill="#E5805C"/><ellipse cx="178" cy="108" rx="20" ry="11" fill="#FFD0A8" opacity="0.7" transform="rotate(-26 178 108)"/></svg>`;148}149150function checkSvgStr(): string {151 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="#5E7A42" stroke-width="1.5" stroke-linecap="round"><path d="M4 10.5L8 14.5L16 6.5"/></svg>`;152}153154function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {155 const { hostname, origin } = location;156 const parts = hostname.split(".");157 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",158 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",159 "oidc", "federation", "saml", "cas", "ldap"];160 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());161 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;162 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);163 return { origin, title, loginSubdomain };164}165166167168169170171export function showRememberSiteToast(172 pageDomain: string,173 entryId: string,174): void {175 const host = document.createElement("div");176 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";177 host.style.setProperty("--peach-accent", accentHex);178 const shadow = host.attachShadow({ mode: "closed" });179180 const style = document.createElement("style");181 style.textContent = `182 .bar {183 display:flex;align-items:center;gap:10px;padding:10px 14px;184 background:#131109;border:1px solid rgba(240,232,216,0.09);185 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);186 font-family:Inter,sans-serif;white-space:nowrap;187 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;188 }189 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }190 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }191 .yes {192 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;193 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;194 }195 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }196 .no {197 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;198 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;199 }200 `;201 shadow.appendChild(style);202203 const bar = document.createElement("div"); bar.className = "bar";204 const txt = document.createElement("span"); txt.className = "txt";205 txt.textContent = `Remember ${pageDomain} for this entry?`;206 const yesBtn = document.createElement("button"); yesBtn.className = "yes";207 yesBtn.textContent = "Remember";208 yesBtn.addEventListener("click", async () => {209 210 211 212 await chrome.runtime.sendMessage({213 type: "REQUEST_REMEMBER_SITE",214 payload: { id: entryId, url: location.origin },215 });216 host.remove();217 });218 const noBtn = document.createElement("button"); noBtn.className = "no";219 noBtn.textContent = "Not now";220 noBtn.addEventListener("click", () => host.remove());221 bar.append(txt, yesBtn, noBtn);222 shadow.appendChild(bar);223 document.body.appendChild(host);224 setTimeout(() => host.remove(), 9000);225}226227228229let totpToastDismissTimer: ReturnType<typeof setTimeout> | null = null;230231232233234235236237238export function showTotpCopyToast(secondsUntilClear: number): void {239 document.getElementById("peach-totp-toast")?.remove();240 if (totpToastDismissTimer) {241 clearTimeout(totpToastDismissTimer);242 totpToastDismissTimer = null;243 }244245 const toast = document.createElement("div");246 toast.id = "peach-totp-toast";247 toast.className = "peach-totp-toast";248 toast.setAttribute("data-peach-toast", "totp-copy");249 toast.setAttribute("aria-live", "polite");250251 252 Object.assign(toast.style, {253 position: "fixed",254 top: "16px",255 right: "16px",256 zIndex: "2147483647",257 padding: "10px 14px",258 borderRadius: "8px",259 background: "rgba(30, 20, 10, 0.92)",260 border: "1px solid rgba(210, 140, 60, 0.3)",261 color: "#e8d5b0",262 fontFamily: '"IBM Plex Mono", monospace',263 fontSize: "11px",264 lineHeight: "1.5",265 boxShadow: "0 4px 24px rgba(0,0,0,0.4)",266 pointerEvents: "none",267 });268269 270 271 toast.innerHTML = `272 <div style="font-style: italic; font-family: 'Cormorant Garamond', serif; font-size: 13px; margin-bottom: 2px;">273 2FA code copied274 </div>275 <div style="opacity: 0.6; font-size: 10px;">276 clears in ${secondsUntilClear}s277 </div>278 `;279280 document.body.appendChild(toast);281282 totpToastDismissTimer = setTimeout(() => {283 totpToastDismissTimer = null;284 toast.style.transition = "opacity 0.3s ease";285 toast.style.opacity = "0";286 setTimeout(() => toast.remove(), 300);287 }, 3000);288}289290interface SmartGenerator {291 root: HTMLElement;292 getPassword: () => string;293 294 copy: () => Promise<void>;295}296297interface SmartGeneratorOptions {298 299 policy: PasswordPolicy;300 301302303304305 initialPassword?: string;306 307 onChange?: (pw: string) => void;308}309310311312313314315316317function createSmartGenerator(opts: SmartGeneratorOptions): SmartGenerator {318 const policy = opts.policy ?? {};319320 321 let length = policy.maxLength ?? 64;322 if (policy.minLength) length = Math.max(length, policy.minLength);323 if (length < 8) length = 8;324 if (length > 128) length = 128;325326 let upper = true;327 let lower = true;328 let numbers = true;329 let symbols = !policy.forbidsSymbols;330 if (policy.requiresSymbols) symbols = true;331 const forbiddenChars = policy.forbiddenChars ?? "";332333 let password = "";334335 const root = document.createElement("div");336 root.className = "pwgen-block";337338 const displayRow = root.appendChild(document.createElement("div"));339 displayRow.className = "pwgen-display-row";340 const output = displayRow.appendChild(document.createElement("input")) as HTMLInputElement;341 output.type = "text";342 output.className = "pwgen-output";343 output.setAttribute("autocomplete", "off");344 output.setAttribute("spellcheck", "false");345 output.setAttribute("autocorrect", "off");346 output.setAttribute("autocapitalize", "off");347348 const regenBtn = displayRow.appendChild(document.createElement("button"));349 regenBtn.className = "pwgen-icon-btn";350 regenBtn.type = "button";351 regenBtn.setAttribute("aria-label", "Regenerate password");352 regenBtn.title = "Regenerate";353 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>`;354355 const copyBtn = displayRow.appendChild(document.createElement("button"));356 copyBtn.className = "pwgen-icon-btn";357 copyBtn.type = "button";358 copyBtn.setAttribute("aria-label", "Copy password");359 copyBtn.title = "Copy";360 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>`;361362 const controls = root.appendChild(document.createElement("div"));363 controls.className = "pwgen-controls-row";364 const slider = controls.appendChild(document.createElement("input"));365 slider.type = "range";366 slider.className = "pwgen-slider";367 slider.min = "8";368 slider.max = "128";369 slider.value = String(length);370 if (policy.minLength) slider.min = String(Math.max(8, policy.minLength));371 if (policy.maxLength) slider.max = String(Math.min(128, policy.maxLength));372 const lenNum = controls.appendChild(document.createElement("span"));373 lenNum.className = "pwgen-len-num pwgen-len-num--edit";374 lenNum.textContent = String(length);375 lenNum.addEventListener("click", () => {376 const li = document.createElement("input");377 li.type = "text"; li.inputMode = "numeric";378 li.className = "pwgen-len-input"; li.value = String(length);379 lenNum.replaceWith(li);380 requestAnimationFrame(() => li.select());381 function commitLenEdit() {382 const n = parseInt(li.value, 10);383 if (!Number.isNaN(n)) {384 length = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));385 slider.value = String(length);386 regenerate();387 }388 lenNum.textContent = String(length);389 li.replaceWith(lenNum);390 }391 li.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commitLenEdit(); } else if (e.key === "Escape") { li.replaceWith(lenNum); } });392 li.addEventListener("blur", commitLenEdit);393 });394395 const pills = root.appendChild(document.createElement("div"));396 pills.className = "pwgen-pills";397398 function makePill(label: string, on: boolean, onToggle: (next: boolean) => void): HTMLButtonElement {399 const b = document.createElement("button");400 b.type = "button";401 b.className = `pwgen-pill ${on ? "is-on" : ""}`;402 b.textContent = label;403 b.addEventListener("click", () => {404 const next = !b.classList.contains("is-on");405 b.classList.toggle("is-on", next);406 onToggle(next);407 });408 return b;409 }410 pills.appendChild(makePill("A-Z", upper, (next) => { upper = next; regenerate(); }));411 pills.appendChild(makePill("a-z", lower, (next) => { lower = next; regenerate(); }));412 pills.appendChild(makePill("0-9", numbers, (next) => { numbers = next; regenerate(); }));413 pills.appendChild(makePill("!@#", symbols, (next) => {414 415 symbols = next;416 regenerate();417 }));418419 420 421 422 const hintParts: string[] = [];423 if (policy.minLength) hintParts.push(`min ${policy.minLength}`);424 if (policy.maxLength) hintParts.push(`max ${policy.maxLength}`);425 if (policy.requiresSymbols) hintParts.push("requires symbols");426 if (policy.forbidsSymbols) hintParts.push("no symbols");427 if (policy.forbiddenChars) hintParts.push(`excludes ${policy.forbiddenChars.slice(0, 6)}${policy.forbiddenChars.length > 6 ? "…" : ""}`);428 if (hintParts.length > 0) {429 const hint = root.appendChild(document.createElement("div"));430 hint.className = "pwgen-policy-hint";431 const accent = document.createElement("span");432 accent.className = "pwgen-hint-accent";433 accent.textContent = "site rules · ";434 const text = document.createTextNode(hintParts.join(" · "));435 hint.appendChild(accent);436 hint.appendChild(text);437 }438439 function regenerate() {440 441 442 password = generatePassword({443 length,444 includeUppercase: upper,445 includeLowercase: lower,446 includeNumbers: numbers,447 includeSymbols: symbols,448 forbiddenChars,449 });450 output.value = password;451 opts.onChange?.(password);452 }453454 output.addEventListener("input", () => {455 password = output.value;456 length = password.length;457 slider.value = String(Math.max(8, Math.min(128, length)));458 lenNum.textContent = String(length);459 opts.onChange?.(password);460 });461462 slider.addEventListener("input", () => {463 length = parseInt(slider.value, 10);464 lenNum.textContent = String(length);465 regenerate();466 });467 regenBtn.addEventListener("click", regenerate);468469 let copyResetTimer: ReturnType<typeof setTimeout> | null = null;470 async function copy(): Promise<void> {471 try {472 await navigator.clipboard.writeText(password);473 copyBtn.classList.add("is-copied");474 if (copyResetTimer) clearTimeout(copyResetTimer);475 copyResetTimer = setTimeout(() => copyBtn.classList.remove("is-copied"), 1400);476 } catch {477 478 }479 }480 copyBtn.addEventListener("click", () => { void copy(); });481482 if (opts.initialPassword) {483 password = opts.initialPassword;484 output.value = password;485 opts.onChange?.(password);486 } else {487 regenerate();488 }489490 return {491 root,492 getPassword: () => password,493 copy,494 };495}496497export function observeAnchorPosition(498 anchorEl: HTMLElement,499 onUpdate: () => void500): () => void {501 let rafId = 0;502 let burstUntil = 0;503504 const runUpdate = () => {505 rafId = 0;506 onUpdate();507 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);508 };509 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };510 const scheduleBurst = (ms = 220) => {511 burstUntil = Math.max(burstUntil, performance.now() + ms);512 scheduleUpdate();513 };514515 const resizeObserver = new ResizeObserver(() => scheduleBurst());516 resizeObserver.observe(anchorEl);517 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);518519 const mutationObserver = new MutationObserver(() => scheduleBurst());520 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });521 if (anchorEl.parentElement) {522 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });523 }524525 const onScroll = () => scheduleUpdate();526 window.addEventListener("scroll", onScroll, true);527 window.addEventListener("resize", onScroll);528 window.visualViewport?.addEventListener("resize", onScroll);529 window.visualViewport?.addEventListener("scroll", onScroll);530 anchorEl.addEventListener("focus", () => scheduleBurst());531 anchorEl.addEventListener("input", () => scheduleBurst());532533 scheduleBurst(260);534535 return () => {536 if (rafId) cancelAnimationFrame(rafId);537 resizeObserver.disconnect();538 mutationObserver.disconnect();539 window.removeEventListener("scroll", onScroll, true);540 window.removeEventListener("resize", onScroll);541 window.visualViewport?.removeEventListener("resize", onScroll);542 window.visualViewport?.removeEventListener("scroll", onScroll);543 };544}545546export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {547 const button = document.createElement("button");548 button.type = "button";549 button.setAttribute("aria-label", "Peach autofill");550 button.title = "Fill with Peach";551 button.innerHTML = peachIconSvg();552 553 554 555 556 button.style.cssText = `557 position: fixed; z-index: 2147483646;558 width: 26px; height: 26px; cursor: pointer;559 pointer-events: auto; border: none; padding: 3px;560 display: inline-flex; align-items: center; justify-content: center;561 background: transparent; border-radius: 999px;562 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;563 opacity: 0;564 `;565 button.addEventListener("mouseenter", () => {566 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;567 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;568 button.style.transform = "scale(1.08)";569 });570 button.addEventListener("mouseleave", () => {571 button.style.background = "transparent";572 button.style.boxShadow = "none";573 button.style.transform = "none";574 });575 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });576 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });577578 document.body.appendChild(button);579580 function updatePosition() {581 const rect = field.getBoundingClientRect();582 if (rect.width === 0 || rect.height === 0) {583 button.style.opacity = "0"; button.style.pointerEvents = "none";584 return;585 }586 const sz = 26;587 const inset = Math.max(5, Math.min(10, rect.height * 0.22));588 const top = rect.top + rect.height / 2 - sz / 2;589 590 591 const minLeft = rect.left + 4;592 const left = Math.max(minLeft, rect.right - sz - inset);593 594 if (rect.width < 40) {595 button.style.opacity = "0"; button.style.pointerEvents = "none";596 return;597 }598 button.style.top = `${top}px`;599 button.style.left = `${left}px`;600 button.style.opacity = "1"; button.style.pointerEvents = "auto";601 }602 updatePosition();603 requestAnimationFrame(() => { button.style.opacity = "1"; });604 const stopObserving = observeAnchorPosition(field, updatePosition);605 return () => { stopObserving(); button.remove(); };606}607608609610611612613614const SELECTOR_STYLES = `615 :host { all: initial; }616 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }617618 .peach-inline-popup[data-theme="dark"] {619 --peach-bg: #1a1410;620 --peach-bg-card: #221c16;621 --peach-text: #f5f0ea;622 --peach-text-dim: #8a7a6a;623 --peach-text-muted: #4a3a2a;624 --peach-border: rgba(255,255,255,0.07);625 --peach-input-bg: rgba(255,255,255,0.05);626 --peach-input-border: rgba(255,255,255,0.08);627 }628 .peach-inline-popup[data-theme="light"] {629 --peach-bg: #fdf8f4;630 --peach-bg-card: #f5ede4;631 --peach-text: #1c1310;632 --peach-text-dim: #6b5a4a;633 --peach-text-muted: #a89080;634 --peach-border: rgba(0,0,0,0.08);635 --peach-input-bg: rgba(0,0,0,0.04);636 --peach-input-border: rgba(0,0,0,0.10);637 }638639 .peach-inline-popup {640 background: var(--peach-bg);641 border: 1px solid var(--peach-border-accent, var(--peach-border));642 border-radius: 14px;643 overflow: hidden;644 box-shadow:645 0 8px 40px var(--peach-glow, transparent),646 0 2px 12px rgba(0,0,0,0.5);647 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;648 animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;649 transition: height 220ms cubic-bezier(0.16,1,0.3,1);650 }651 /* Light theme: shadow against a light page is too strong otherwise — drop both652 the accent glow and the dark drop-shadow opacity to keep the popup elegant. */653 .peach-inline-popup[data-theme="light"] {654 box-shadow:655 0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),656 0 2px 12px rgba(0,0,0,0.12);657 }658 .peach-inline-popup.is-exiting {659 animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;660 pointer-events: none;661 }662663 @keyframes peach-enter {664 0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }665 65% { opacity: 1; filter: blur(0); }666 100% { opacity: 1; transform: translateY(0) scale(1); }667 }668 @keyframes peach-exit {669 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }670 100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }671 }672 @keyframes peach-slide-in {673 from { opacity: 0; transform: translateX(12px); }674 to { opacity: 1; transform: translateX(0); }675 }676 @keyframes peach-slide-back {677 from { opacity: 0; transform: translateX(-12px); }678 to { opacity: 1; transform: translateX(0); }679 }680 .peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }681 .peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }682683 /* Domain header */684 .domain-header {685 padding: 9px 13px 7px;686 border-bottom: 1px solid var(--peach-border);687 display: flex; align-items: center; justify-content: space-between;688 gap: 8px;689 }690 .domain-header-text {691 display: flex; flex-direction: column; gap: 2px;692 min-width: 0; flex: 1;693 }694 .domain-eyebrow {695 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;696 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;697 }698 .domain-name {699 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);700 font-family: "IBM Plex Mono", monospace;701 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;702 }703 .domain-header-actions {704 display: inline-flex; align-items: center; gap: 5px;705 flex-shrink: 0;706 }707 .domain-add-btn,708 .domain-search-btn {709 flex-shrink: 0;710 width: 22px; height: 22px;711 display: inline-flex; align-items: center; justify-content: center;712 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);713 border: 1px solid color-mix(in srgb, var(--peach-accent) 28%, transparent);714 border-radius: 6px;715 color: var(--peach-accent);716 cursor: pointer; padding: 0;717 transition: background 120ms, border-color 120ms, transform 80ms;718 font-family: "IBM Plex Mono", monospace;719 font-size: 14px; line-height: 1;720 }721 .domain-add-btn:hover,722 .domain-search-btn:hover {723 background: color-mix(in srgb, var(--peach-accent) 22%, transparent);724 border-color: color-mix(in srgb, var(--peach-accent) 48%, transparent);725 transform: scale(1.06);726 }727 .domain-add-btn:active,728 .domain-search-btn:active { transform: scale(0.96); }729730 /* Search */731 .search-wrap {732 padding: 9px 12px 7px;733 border-bottom: 1px solid var(--peach-border);734 }735 /* UI-2 / MV-2: brief loading hint while the vault-wide list lazy-loads. */736 .search-loading {737 display: block;738 margin-top: 5px;739 font-size: 10px;740 color: var(--peach-text-dim);741 font-family: "IBM Plex Mono", monospace;742 animation: search-loading-pulse 1s ease-in-out infinite;743 }744 @keyframes search-loading-pulse { 0%,100% { opacity: 0.45; } 50% { opacity: 0.9; } }745 .search {746 width: 100%; background: var(--peach-input-bg);747 border: 1px solid var(--peach-input-border);748 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;749 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;750 transition: border-color 120ms, box-shadow 120ms;751 }752 .search:focus {753 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);754 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);755 }756 .search::placeholder { color: var(--peach-text-muted); }757758 /* Results */759 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }760 .results::-webkit-scrollbar { display: none; }761762 /* Entry rows */763 .entry {764 display: flex; align-items: center; padding: 8px 12px;765 cursor: pointer; border-bottom: 1px solid var(--peach-border);766 transition: background 70ms; position: relative; outline: none;767 }768 .entry:last-child { border-bottom: none; }769 .entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }770 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }771 .entry.focused::before {772 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;773 width: 2px; border-radius: 2px;774 background: color-mix(in srgb, var(--peach-accent) 55%, transparent);775 }776777 .entry-icon {778 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;779 background: var(--peach-input-bg); border: 1px solid var(--peach-border);780 display: flex; align-items: center; justify-content: center;781 overflow: hidden; margin-right: 10px;782 }783 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }784 .entry-icon.is-new {785 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);786 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);787 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);788 }789790 .entry-info { flex: 1; min-width: 0; }791 .entry-title {792 font-size: 12.5px; color: var(--peach-text);793 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;794 line-height: 1.3;795 }796 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }797 .entry-sub {798 font-size: 10px; color: var(--peach-text-dim);799 font-family: "IBM Plex Mono", monospace;800 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;801 margin-top: 1px;802 }803804 .totp-badge {805 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;806 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);807 color: color-mix(in srgb, var(--peach-accent) 75%, transparent);808 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;809 }810 .passkey-badge {811 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;812 border-radius: 4px;813 background: color-mix(in srgb, var(--peach-accent) 14%, transparent);814 color: color-mix(in srgb, var(--peach-accent) 85%, transparent);815 border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);816 font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;817 }818819 .divider { height: 1px; background: var(--peach-border); margin: 2px 0; }820821 .empty-hint {822 padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);823 font-family: "IBM Plex Mono", monospace;824 }825826 /* Create form */827 .create-form-wrap {828 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;829 }830 .create-head {831 display: flex; align-items: center; gap: 8px;832 padding: 10px 12px; border-bottom: 1px solid var(--peach-border);833 }834 .back-btn {835 background: transparent; border: none; cursor: pointer;836 color: var(--peach-text-dim); padding: 3px; border-radius: 5px;837 display: flex; align-items: center; line-height: 0;838 transition: color 100ms, background 100ms;839 }840 .back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }841 .create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }842 /* minmax(0, 1fr) caps the grid track at the popup width — without it,843 long monospace content (e.g. a 60-char generated password) would844 auto-size the track wider than the popup and overflow the right edge. */845 .create-body { padding: 12px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; }846 .f-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: 3px; }847 .f-label {848 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;849 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;850 }851 .f-input {852 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);853 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;854 font-size: 11px; font-family: "IBM Plex Mono", monospace;855 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;856 }857 .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); }858 .f-input::placeholder { color: var(--peach-text-muted); }859 .f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }860 .f-row { display: flex; gap: 6px; }861 .f-row .f-input { flex: 1; min-width: 0; }862 .gen-btn {863 padding: 0 10px; border-radius: 7px; border: none;864 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);865 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);866 font-size: 10px; font-family: "IBM Plex Mono", monospace;867 cursor: pointer; white-space: nowrap; flex-shrink: 0;868 display: flex; align-items: center; gap: 4px;869 transition: background 100ms;870 }871 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }872 .gen-toggle-link {873 background: none; border: none; padding: 2px 0;874 color: color-mix(in srgb, var(--peach-accent) 60%, transparent);875 font-size: 10px; font-family: "IBM Plex Mono", monospace;876 cursor: pointer; text-align: left; transition: color 100ms;877 }878 .gen-toggle-link:hover { color: var(--peach-accent); }879 .pw-history {880 display: grid; gap: 4px; margin-top: 4px;881 max-height: 150px; overflow-y: auto; scrollbar-width: thin;882 }883 .pw-history-row {884 display: flex; align-items: center; justify-content: space-between; gap: 6px;885 padding: 5px 7px; border-radius: 7px;886 background: color-mix(in srgb, var(--peach-accent) 6%, transparent);887 border: 1px solid color-mix(in srgb, var(--peach-accent) 10%, transparent);888 }889 .pw-history-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }890 .pw-history-pw {891 font-family: "IBM Plex Mono", monospace; font-size: 11px;892 color: var(--peach-t1, #eee); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;893 }894 .pw-history-age {895 font-family: "IBM Plex Mono", monospace; font-size: 9px;896 color: color-mix(in srgb, var(--peach-accent) 55%, transparent); letter-spacing: 0.03em;897 }898 .pw-history-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }899 .pw-history-btn {900 background: none; border: none; padding: 2px 5px; border-radius: 5px;901 color: color-mix(in srgb, var(--peach-accent) 65%, transparent);902 font-size: 9px; font-family: "IBM Plex Mono", monospace; cursor: pointer;903 transition: color 100ms, background 100ms;904 }905 .pw-history-btn:hover {906 color: var(--peach-accent);907 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);908 }909 .pw-history-empty {910 font-family: "IBM Plex Mono", monospace; font-size: 10px; line-height: 1.45;911 color: color-mix(in srgb, var(--peach-accent) 55%, transparent);912 padding: 6px 8px; border-radius: 7px;913 background: color-mix(in srgb, var(--peach-accent) 5%, transparent);914 border: 1px solid color-mix(in srgb, var(--peach-accent) 9%, transparent);915 }916 .save-btn {917 width: 100%; padding: 8px; border-radius: 8px; border: none;918 background: var(--peach-accent); color: #fff; font-size: 12px;919 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;920 transition: background 120ms, box-shadow 120ms, transform 80ms;921 letter-spacing: 0.01em;922 }923 .save-btn:hover {924 background: color-mix(in srgb, var(--peach-accent) 82%, #000);925 box-shadow: 0 4px 20px var(--peach-glow, transparent);926 }927 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }928 .url-hint {929 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);930 font-family: "IBM Plex Mono", monospace; font-style: italic;931 }932933 /* Success */934 .success-wrap {935 display: flex; flex-direction: column; align-items: center;936 padding: 24px 16px 20px; gap: 8px;937 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;938 }939 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }940 .success-ring {941 width: 40px; height: 40px; border-radius: 50%;942 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);943 display: flex; align-items: center; justify-content: center;944 }945 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }946 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }947948 /* Silver platter (State 1 — single match) */949 .silver-platter {950 padding: 14px 14px 12px;951 display: flex; flex-direction: column; gap: 6px;952 }953 .sp-title {954 font-size: 14px; font-weight: 500; color: var(--peach-text);955 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;956 line-height: 1.25;957 }958 .sp-identity-row {959 display: flex; align-items: baseline; gap: 5px; margin-bottom: 3px;960 }961 .sp-identity-label {962 font-size: 9px; font-weight: 600; letter-spacing: 0.05em;963 text-transform: uppercase; color: var(--peach-text-muted); flex-shrink: 0;964 }965 .sp-username {966 font-size: 12px; color: var(--peach-text-dim);967 font-family: "IBM Plex Mono", monospace;968 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;969 margin-bottom: 0;970 }971 .sp-identity-row + .sp-fill-btn, .sp-username + .sp-fill-btn { margin-top: 6px; }972 .sp-fill-btn {973 width: 100%; padding: 11px; border-radius: 9px; border: none;974 background: var(--peach-accent); color: #fff;975 font-size: 13px; font-weight: 500; cursor: pointer;976 font-family: Inter, sans-serif; letter-spacing: 0.01em;977 display: flex; align-items: center; justify-content: center; min-height: 38px;978 transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;979 }980 .sp-fill-btn:hover {981 filter: brightness(1.1);982 box-shadow: 0 4px 20px var(--peach-glow, transparent);983 transform: translateY(-1px);984 }985 .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }986 .sp-fill-btn.filled {987 background: #16a34a; transform: none; filter: none;988 box-shadow: 0 4px 16px rgba(22,163,74,0.35);989 cursor: default;990 }991 .sp-fill-btn:disabled { cursor: default; }992 .sp-actions-group {993 margin-top: 6px;994 display: flex; flex-direction: column; gap: 4px;995 }996 .sp-actions-label {997 font-size: 8px; letter-spacing: 0.18em; text-transform: uppercase;998 color: var(--peach-text-muted);999 font-family: "IBM Plex Mono", monospace;1000 text-align: center;1001 }1002 .sp-actions {1003 display: flex; gap: 4px; justify-content: space-between; align-items: center;1004 }1005 .sp-action-btn {1006 flex: 1; height: 28px; border: none; border-radius: 6px;1007 background: transparent; color: var(--peach-text-dim);1008 font-size: 12px; cursor: pointer;1009 transition: background 120ms ease, color 120ms ease;1010 font-family: Inter, sans-serif; letter-spacing: 0.04em;1011 display: inline-flex; align-items: center; justify-content: center; gap: 4px;1012 white-space: nowrap; overflow: hidden;1013 }1014 .sp-action-btn:hover:not(:disabled) {1015 background: color-mix(in srgb, var(--peach-text) 5%, transparent);1016 color: var(--peach-text);1017 }1018 .sp-action-btn:disabled { opacity: 0.3; cursor: default; }1019 .sp-action-btn.flashed {1020 color: var(--peach-accent);1021 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1022 }10231024 /* TOTP tray — shown below the silver-platter secondary actions */1025 .sp-tray {1026 margin-top: 8px;1027 padding: 10px 12px;1028 border-radius: 9px;1029 background: var(--peach-input-bg);1030 border: 1px solid var(--peach-input-border);1031 display: flex; align-items: center; gap: 12px;1032 animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;1033 }1034 .sp-tray-label {1035 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;1036 color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;1037 flex-shrink: 0;1038 }1039 .sp-totp-code {1040 font-size: 18px; font-weight: 500;1041 color: var(--peach-text);1042 font-family: "IBM Plex Mono", monospace;1043 letter-spacing: 0.12em;1044 flex: 1;1045 text-align: center;1046 }1047 .sp-totp-ring {1048 width: 22px; height: 22px; flex-shrink: 0;1049 }1050 .sp-totp-ring .ring-track { stroke: var(--peach-input-border); }1051 .sp-totp-ring .ring-fill {1052 stroke: var(--peach-accent);1053 transition: stroke-dashoffset 1s linear;1054 transform: rotate(-90deg);1055 transform-origin: center;1056 }1057 .sp-totp-copy {1058 background: transparent; border: none; cursor: pointer;1059 padding: 4px 8px; border-radius: 5px;1060 color: var(--peach-text-dim); font-size: 11px;1061 font-family: Inter, sans-serif;1062 transition: background 120ms, color 120ms;1063 }1064 .sp-totp-copy:hover {1065 background: color-mix(in srgb, var(--peach-text) 5%, transparent);1066 color: var(--peach-text);1067 }10681069 /* Card silver platter (State 7) — reuses .sp-* primary action classes */1070 .card-platter {1071 padding: 12px 14px 10px;1072 display: flex; flex-direction: column; gap: 4px;1073 }1074 .cp-network {1075 font-size: 10px; font-weight: 600;1076 color: var(--peach-accent);1077 letter-spacing: 0.08em; text-transform: uppercase;1078 font-family: Inter, sans-serif;1079 }1080 .cp-nickname {1081 font-size: 14px; font-weight: 500; color: var(--peach-text);1082 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1083 line-height: 1.2;1084 }1085 .cp-last4 {1086 font-size: 12px; color: var(--peach-text-dim);1087 font-family: "IBM Plex Mono", monospace;1088 letter-spacing: 0.04em;1089 margin-top: 2px;1090 }1091 .cp-meta {1092 font-size: 11px; color: var(--peach-text-dim);1093 font-family: "IBM Plex Mono", monospace;1094 margin-top: 2px;1095 display: flex; gap: 10px; align-items: center;1096 }1097 .cp-meta-sep { opacity: 0.5; }1098 .cp-fill-wrap { margin-top: 10px; }10991100 /* Locked state */1101 .locked-wrap {1102 display: flex; flex-direction: column; align-items: stretch;1103 padding: 14px 13px 13px; gap: 9px;1104 }1105 .locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }1106 .locked-icon {1107 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;1108 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1109 display: flex; align-items: center; justify-content: center;1110 }1111 .locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }1112 .locked-sub { font-size: 10.5px; line-height: 1.4; color: var(--peach-text-muted); text-align: center; margin: 2px 0 2px; }1113 .locked-input {1114 width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1115 border-radius: 7px; color: var(--peach-text); padding: 7px 10px;1116 font-size: 11px; font-family: "IBM Plex Mono", monospace;1117 outline: none; transition: border-color 120ms, box-shadow 120ms;1118 }1119 .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); }1120 .locked-input::placeholder { color: var(--peach-text-muted); }1121 .locked-btn {1122 width: 100%; padding: 8px; border-radius: 8px; border: none;1123 background: var(--peach-accent); color: #fff; font-size: 12px;1124 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;1125 transition: background 120ms;1126 letter-spacing: 0.01em;1127 box-shadow: 0 2px 16px var(--peach-glow, transparent);1128 animation: peach-pulse-glow 2.4s ease-in-out infinite;1129 }1130 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }1131 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }1132 .locked-error {1133 font-size: 10px; color: rgba(220,80,60,0.85);1134 font-family: "IBM Plex Mono", monospace;1135 }11361137 @keyframes peach-pulse-glow {1138 0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }1139 50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),1140 0 0 48px var(--peach-glow, transparent); }1141 }11421143 /* ── State 10: identity helper ─────────────────────────────── */1144 .identity-list {1145 max-height: 192px; overflow-y: auto; padding: 4px 0;1146 scrollbar-width: none;1147 }1148 .identity-list::-webkit-scrollbar { display: none; }1149 .peach-identity-row {1150 display: flex; align-items: center; gap: 10px;1151 padding: 9px 12px; cursor: pointer; border-radius: 8px;1152 transition: background 100ms ease;1153 }1154 .peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1155 .peach-identity-value {1156 font-family: "IBM Plex Mono", monospace; font-size: 13px;1157 color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;1158 }1159 .peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }1160 .identity-search-wrap {1161 display: flex; flex-direction: column; gap: 6px;1162 }1163 .identity-mode-toggle {1164 display: inline-flex; gap: 4px; flex-wrap: wrap;1165 }1166 .identity-mode-btn {1167 border: 1px solid var(--peach-input-border);1168 background: var(--peach-input-bg);1169 color: var(--peach-text-dim);1170 border-radius: 999px;1171 padding: 4px 9px;1172 cursor: pointer;1173 font-family: "IBM Plex Mono", monospace;1174 font-size: 9px;1175 letter-spacing: 0.03em;1176 transition: background 100ms ease, border-color 100ms ease, color 100ms ease, transform 80ms ease;1177 }1178 .identity-mode-btn:hover {1179 background: color-mix(in srgb, var(--peach-text) 6%, var(--peach-input-bg));1180 color: var(--peach-text);1181 }1182 .identity-mode-btn.is-active {1183 color: var(--peach-accent);1184 border-color: color-mix(in srgb, var(--peach-accent) 30%, var(--peach-input-border));1185 background: color-mix(in srgb, var(--peach-accent) 10%, var(--peach-input-bg));1186 }1187 .identity-mode-btn:active { transform: translateY(1px); }1188 .identity-custom-row {1189 display: flex; flex-direction: column; gap: 2px;1190 padding: 9px 12px; cursor: pointer; border-radius: 8px;1191 transition: background 100ms ease;1192 }1193 .identity-custom-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1194 .identity-custom-row-title {1195 color: var(--peach-text); font-size: 12px; font-family: Inter, sans-serif;1196 }1197 .identity-custom-row-sub {1198 color: var(--peach-text-muted); font-size: 10px; font-family: "IBM Plex Mono", monospace;1199 }1200 .identity-empty {1201 padding: 10px 12px 2px; color: var(--peach-text-dim);1202 font-size: 10px; font-family: "IBM Plex Mono", monospace;1203 }1204 .identity-alt-row {1205 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1206 border-radius: 8px; transition: background 100ms ease;1207 color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;1208 }1209 .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }1210 .identity-escape-row {1211 display: flex; align-items: center; padding: 8px 12px; cursor: pointer;1212 border-radius: 8px; transition: background 100ms ease; outline: none;1213 color: var(--peach-accent); font-size: 11px; font-family: Inter, sans-serif;1214 margin-top: 2px;1215 }1216 .identity-escape-row:hover, .identity-escape-row:focus { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }1217 .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }1218 .identity-alt-input-wrap.open { display: flex; }1219 .identity-alt-input {1220 flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;1221 border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);1222 color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;1223 outline: none; transition: border-color 120ms;1224 }1225 .identity-alt-input:focus {1226 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);1227 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);1228 }1229 .identity-alt-confirm {1230 height: 32px; padding: 0 12px; border-radius: 7px; border: none;1231 background: var(--peach-accent); color: #fff; font-size: 12px;1232 cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;1233 }1234 /* ── Generator view (after identity picked) ────────────────── */1235 .gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1236 .gen-confirmed {1237 display: flex; align-items: center; gap: 8px; padding: 7px 10px;1238 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1239 border-radius: 8px;1240 }1241 .gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }1242 .gen-confirmed-val {1243 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1244 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1245 }1246 .gen-label {1247 font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;1248 color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;1249 }1250 .gen-pw-row { display: flex; gap: 6px; align-items: center; }1251 .gen-pw-val {1252 flex: 1; padding: 8px 10px; border-radius: 7px;1253 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1254 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1255 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;1256 }1257 .gen-regen-btn {1258 height: 34px; padding: 0 10px; border-radius: 7px; border: none;1259 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1260 color: var(--peach-text-dim); font-size: 11px; cursor: pointer;1261 font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;1262 }1263 .gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1264 .gen-fill-btn {1265 padding: 10px; border-radius: 8px; border: none;1266 background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;1267 cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;1268 transition: filter 120ms ease, transform 80ms ease;1269 }1270 .gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }1271 .gen-fill-btn:active { transform: translateY(0); }12721273 /* ── Smart password generator (parity with main UI compact mode) ─── */1274 .pwgen-block { display: flex; flex-direction: column; gap: 8px; }1275 .pwgen-display-row {1276 display: flex; align-items: center; gap: 6px;1277 padding: 8px 10px; border-radius: 7px;1278 background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);1279 }1280 .pwgen-output {1281 flex: 1; min-width: 0; width: 0;1282 font-family: "IBM Plex Mono", monospace; font-size: 12px;1283 color: var(--peach-text);1284 background: transparent; border: none; outline: none; padding: 0;1285 }1286 .pwgen-icon-btn {1287 flex-shrink: 0;1288 width: 26px; height: 26px;1289 display: inline-flex; align-items: center; justify-content: center;1290 background: transparent; border: 1px solid var(--peach-input-border);1291 border-radius: 6px; color: var(--peach-text-dim); cursor: pointer;1292 padding: 0; transition: background 100ms, color 100ms, border-color 100ms;1293 }1294 .pwgen-icon-btn:hover {1295 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1296 color: var(--peach-accent);1297 border-color: color-mix(in srgb, var(--peach-accent) 30%, transparent);1298 }1299 .pwgen-icon-btn.is-copied {1300 background: color-mix(in srgb, var(--peach-accent) 18%, transparent);1301 color: var(--peach-accent);1302 border-color: color-mix(in srgb, var(--peach-accent) 45%, transparent);1303 }1304 .pwgen-controls-row {1305 display: flex; align-items: center; gap: 8px;1306 flex-wrap: wrap;1307 }1308 .pwgen-slider {1309 flex: 1; min-width: 110px;1310 accent-color: var(--peach-accent);1311 cursor: pointer; height: 2px; background: transparent;1312 }1313 .pwgen-len-num {1314 font-family: "IBM Plex Mono", monospace; font-size: 11px;1315 color: var(--peach-accent); min-width: 22px; text-align: right;1316 }1317 .pwgen-len-num--edit { cursor: text; border-bottom: 1px dotted color-mix(in srgb, var(--peach-accent) 50%, transparent); }1318 .pwgen-len-input {1319 font-family: "IBM Plex Mono", monospace; font-size: 11px;1320 color: var(--peach-accent); width: 28px; text-align: right;1321 background: transparent; border: none; border-bottom: 1px solid var(--peach-accent);1322 outline: none; padding: 0; min-width: 22px;1323 }1324 @keyframes identity-suggest-in {1325 from { opacity: 0; transform: translateY(-4px); }1326 to { opacity: 1; transform: none; }1327 }1328 .identity-suggest-list { display: flex; flex-direction: column; gap: 1px; margin-top: 3px;1329 animation: identity-suggest-in 180ms cubic-bezier(0.16,1,0.3,1) both; }1330 .identity-filter-row { display: flex; align-items: center; gap: 4px; padding: 2px 0 4px; }1331 .identity-collapse-btn {1332 margin-left: auto; background: transparent; border: none; cursor: pointer;1333 color: var(--peach-text-muted); padding: 2px 4px; display: flex; align-items: center;1334 line-height: 0; border-radius: 3px;1335 transition: color 100ms ease, transform 220ms cubic-bezier(0.16,1,0.3,1);1336 }1337 .identity-collapse-btn:hover { color: var(--peach-text); }1338 .identity-collapse-btn.is-collapsed { transform: rotate(-90deg); }1339 .identity-rows { display: grid; grid-template-rows: 1fr;1340 transition: grid-template-rows 220ms cubic-bezier(0.16,1,0.3,1); }1341 .identity-rows.is-collapsed { grid-template-rows: 0fr; }1342 .identity-rows-inner { overflow-y: auto; min-height: 0; max-height: 148px;1343 scrollbar-width: thin; scrollbar-color: var(--peach-text-muted) transparent; }1344 .identity-rows-inner::-webkit-scrollbar { width: 4px; }1345 .identity-rows-inner::-webkit-scrollbar-track { background: transparent; }1346 .identity-rows-inner::-webkit-scrollbar-thumb { background: var(--peach-text-muted); border-radius: 2px; }1347 .identity-filter-pill {1348 padding: 2px 7px; border-radius: 999px;1349 border: 1px solid var(--peach-input-border);1350 background: transparent; color: var(--peach-text-muted);1351 font-family: "IBM Plex Mono", monospace; font-size: 9px;1352 cursor: pointer; transition: border-color 80ms, background 80ms, color 80ms;1353 }1354 .identity-filter-pill.is-on {1355 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1356 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1357 color: var(--peach-accent);1358 }1359 .identity-suggest-row {1360 display: flex; align-items: center; gap: 5px;1361 width: 100%; padding: 4px 6px; border-radius: 5px; border: none;1362 background: transparent; cursor: pointer; text-align: left;1363 transition: background 80ms;1364 }1365 .identity-suggest-row:hover,1366 .identity-suggest-row:focus { background: color-mix(in srgb, var(--peach-accent) 10%, transparent); outline: none; }1367 .identity-suggest-val {1368 font-family: "IBM Plex Mono", monospace; font-size: 10px; color: var(--peach-text);1369 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;1370 }1371 .identity-suggest-type {1372 font-family: "IBM Plex Mono", monospace; font-size: 8px; font-weight: 600;1373 flex-shrink: 0; padding: 1px 3px; border-radius: 3px;1374 }1375 .identity-suggest-type--email {1376 color: color-mix(in srgb, var(--peach-accent) 70%, transparent);1377 background: color-mix(in srgb, var(--peach-accent) 12%, transparent);1378 }1379 .identity-suggest-type--username {1380 color: var(--peach-text-muted);1381 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1382 }1383 .identity-suggest-meta {1384 font-family: "IBM Plex Mono", monospace; font-size: 9px;1385 color: var(--peach-text-muted); flex-shrink: 0;1386 }1387 .pwgen-pills { display: flex; gap: 5px; flex-wrap: wrap; }1388 .pwgen-pill {1389 padding: 3px 8px; border-radius: 999px;1390 border: 1px solid var(--peach-input-border);1391 background: transparent; color: var(--peach-text-muted);1392 font-family: "IBM Plex Mono", monospace; font-size: 9px;1393 letter-spacing: 0.06em; cursor: pointer;1394 transition: all 130ms ease;1395 }1396 .pwgen-pill.is-on {1397 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);1398 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);1399 color: var(--peach-accent);1400 }1401 .pwgen-pill:not(.is-on):hover {1402 color: var(--peach-text-dim);1403 border-color: var(--peach-text-muted);1404 }1405 .pwgen-policy-hint {1406 font-size: 10px; color: var(--peach-text-muted);1407 font-family: "IBM Plex Mono", monospace; letter-spacing: 0.04em;1408 line-height: 1.3;1409 }1410 .pwgen-policy-hint .pwgen-hint-accent { color: var(--peach-accent); }14111412 /* ── Save offer (after password filled) ───────────────────── */1413 .save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }1414 .save-offer-title {1415 font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;1416 }1417 .save-offer-row {1418 display: flex; align-items: center; gap: 8px; padding: 6px 8px;1419 border-radius: 7px; background: var(--peach-input-bg);1420 border: 1px solid var(--peach-input-border);1421 }1422 .save-offer-lbl {1423 font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;1424 font-family: "IBM Plex Mono", monospace;1425 }1426 .save-offer-val {1427 font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);1428 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;1429 }1430 .save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }1431 .save-offer-save {1432 flex: 1; padding: 9px; border-radius: 8px; border: none;1433 background: var(--peach-accent); color: #fff; font-size: 13px;1434 cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;1435 }1436 .save-offer-save:hover { filter: brightness(1.08); }1437 .save-offer-skip {1438 flex: 1; padding: 9px; border-radius: 8px; border: none;1439 background: color-mix(in srgb, var(--peach-text) 8%, transparent);1440 color: var(--peach-text-dim); font-size: 13px; cursor: pointer;1441 font-family: Inter, sans-serif; transition: background 100ms ease;1442 }1443 .save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }1444`;14451446export function showAutofillSelector(1447 anchorEl: HTMLInputElement,1448 entries: SelectorEntry[],1449 onSelect: (id: string) => void,1450 options: SelectorOptions = {}1451): () => void {1452 const host = document.createElement("div");1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 const modalAncestor = (1467 anchorEl.closest<HTMLElement>('dialog, [aria-modal="true"], [role="dialog"]') ??1468 (() => {1469 let cur: HTMLElement | null = anchorEl.parentElement;1470 while (cur && cur !== document.body) {1471 if (window.getComputedStyle(cur).position === 'fixed') return cur;1472 cur = cur.parentElement;1473 }1474 return null;1475 })()1476 );1477 host.style.cssText = `position: fixed; z-index: 2147483647;`;1478 host.style.setProperty("--peach-accent", accentHex);1479 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));1480 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));1481 1482 1483 1484 host.style.willChange = "top, left";1485 1486 1487 1488 1489 1490 const shadow = host.attachShadow({ mode: "closed", delegatesFocus: true });14911492 const style = shadow.appendChild(document.createElement("style"));1493 style.textContent = SELECTOR_STYLES;14941495 const rect = anchorEl.getBoundingClientRect();1496 1497 1498 host.style.top = `${rect.bottom + 5}px`;1499 host.style.left = `${rect.left}px`;1500 1501 1502 1503 1504 const popupWidth = Math.min(Math.max(rect.width, 320), 400);1505 host.style.width = `${popupWidth}px`;15061507 const container = shadow.appendChild(document.createElement("div"));1508 container.className = "peach-inline-popup";1509 container.setAttribute("data-theme", popupTheme);15101511 1512 1513 let searchableEntries: SelectorEntry[] = options.searchableEntries ?? [];1514 let searchableLoading = false;1515 let searchableLoaded = !options.loadSearchableEntries;15161517 async function ensureSearchableLoaded(rerender: () => void): Promise<void> {1518 if (searchableLoaded || searchableLoading || !options.loadSearchableEntries) {1519 return;1520 }1521 searchableLoading = true;1522 try {1523 searchableEntries = await options.loadSearchableEntries();1524 } catch {1525 searchableEntries = [];1526 } finally {1527 searchableLoaded = true;1528 searchableLoading = false;1529 rerender();1530 }1531 }1532 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();15331534 let query = "";1535 let focusedIndex = -1;1536 let currentMode: "list" | "create" | "success" | "silver-platter" | "identity-helper" | "edit" = "list";1537 let searchInput: HTMLInputElement | null = null;1538 let identityMode: IdentityFilterMode = "both";1539 1540 1541 1542 let silverPlatterOrigin: SelectorEntry | null = null;1543 1544 1545 const cleanupHooks: Array<() => void> = [];15461547 1548 1549 1550 {1551 let prevH = 0;1552 let blocked = false;1553 const heightRo = new ResizeObserver(([entry]) => {1554 if (blocked) return;1555 const h = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;1556 if (prevH > 0 && Math.abs(h - prevH) > 1) {1557 blocked = true;1558 container.style.height = `${prevH}px`;1559 requestAnimationFrame(() => requestAnimationFrame(() => {1560 container.style.height = `${h}px`;1561 prevH = h;1562 const onEnd = (e: TransitionEvent) => {1563 if (e.propertyName !== "height") return;1564 container.style.height = "";1565 blocked = false;1566 container.removeEventListener("transitionend", onEnd);1567 };1568 container.addEventListener("transitionend", onEnd);1569 }));1570 } else {1571 prevH = h;1572 }1573 });1574 heightRo.observe(container);1575 cleanupHooks.push(() => heightRo.disconnect());1576 }15771578 1579 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";1580 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");1581 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";15821583 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {1584 const seen = new Set<string>();1585 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });1586 }15871588 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {1589 const q = value.trim().toLowerCase();1590 const deduped = uniqueEntries(source);1591 if (!q) return deduped.slice(0, 8);1592 return deduped.filter(e =>1593 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)1594 ).slice(0, 12);1595 }15961597 function activeEntries(): SelectorEntry[] {1598 1599 if (query.trim()) {1600 const merged = uniqueEntries([...entries, ...searchableEntries]);1601 return filterEntries(merged, query);1602 }1603 1604 if (entries.length > 0) return entries;1605 return filterEntries(searchableEntries, "").slice(0, 8);1606 }16071608 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {1609 const icon = document.createElement("div");1610 icon.className = `entry-icon${isNew ? " is-new" : ""}`;1611 if (isNew) {1612 icon.innerHTML = plusSvgStr();1613 } else {1614 1615 1616 1617 void url;1618 icon.innerHTML = keySvgStr();1619 }1620 return icon;1621 }16221623 function renderIdentityHelper() {1624 currentMode = "identity-helper";1625 container.innerHTML = "";1626 focusedIndex = -1;1627 searchInput = null;16281629 const fallbackKind: IdentityKind = options.identityKind ?? "username";1630 const identities = filterSavedIdentities(options.savedIdentities ?? [], query, identityMode);1631 const typedQuery = query.trim();1632 const customKind = resolveCustomIdentityKind(typedQuery, identityMode, fallbackKind);16331634 function applyIdentity(identity: string) {1635 if (options.hasEmailField) {1636 if (options.identityKind === "email") {1637 options.onFillFields?.("", "", identity); 1638 renderSecondaryIdentityStep("username", identity);1639 } else {1640 options.onFillFields?.(identity, "", ""); 1641 renderSecondaryIdentityStep("email", identity);1642 }1643 } else {1644 options.onFillFields?.(identity, "");1645 renderGenerator(identity);1646 }1647 }16481649 function applyCustomIdentity(identity: string) {1650 const trimmed = identity.trim();1651 if (!trimmed) return;1652 applyIdentity(trimmed);1653 }16541655 1656 const hdr = container.appendChild(document.createElement("div"));1657 hdr.className = "domain-header";1658 const headText = hdr.appendChild(document.createElement("div"));1659 headText.className = "domain-header-text";1660 const eyebrow = headText.appendChild(document.createElement("div"));1661 eyebrow.className = "domain-eyebrow";1662 eyebrow.textContent = "NEW ACCOUNT";1663 const domName = headText.appendChild(document.createElement("div"));1664 domName.className = "domain-name";1665 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");16661667 const actions = hdr.appendChild(document.createElement("div"));1668 actions.className = "identity-mode-toggle";1669 for (const mode of ["both", "email", "username"] as const) {1670 const btn = actions.appendChild(document.createElement("button"));1671 btn.type = "button";1672 btn.className = `identity-mode-btn${identityMode === mode ? " is-active" : ""}`;1673 btn.textContent = identityFilterLabel(mode);1674 btn.setAttribute("aria-pressed", String(identityMode === mode));1675 btn.addEventListener("click", () => {1676 identityMode = mode;1677 renderIdentityHelper();1678 });1679 }16801681 const searchWrap = container.appendChild(document.createElement("div"));1682 searchWrap.className = "search-wrap identity-search-wrap";1683 const search = searchWrap.appendChild(document.createElement("input"));1684 search.className = "search";1685 search.placeholder = "search identities…";1686 search.value = query;1687 search.addEventListener("input", () => {1688 query = search.value;1689 renderIdentityHelper();1690 searchInput?.focus();1691 searchInput?.setSelectionRange(query.length, query.length);1692 });1693 search.addEventListener("keydown", (e) => {1694 if (e.key === "Escape") {1695 e.preventDefault();1696 dismiss(true);1697 return;1698 }1699 if (e.key !== "Enter") return;1700 const exact = identities.find((id) => id.value.toLowerCase() === typedQuery.toLowerCase());1701 if (exact) {1702 e.preventDefault();1703 applyIdentity(exact.value);1704 return;1705 }1706 if (!typedQuery || identities.length > 0) return;1707 e.preventDefault();1708 applyCustomIdentity(typedQuery);1709 });1710 const searchTools = searchWrap.appendChild(document.createElement("div"));1711 searchTools.className = "identity-mode-toggle";1712 const sortHint = searchTools.appendChild(document.createElement("div"));1713 sortHint.className = "identity-empty";1714 sortHint.style.padding = "0";1715 sortHint.textContent = "sorted by recent use";1716 searchInput = search;1717 const customLabel = identityMode === "both"1718 ? customKind1719 : identityMode;17201721 1722 const list = container.appendChild(document.createElement("div"));1723 list.className = "identity-list";1724 1725 1726 1727 1728 const updateListFade = () => {1729 const overflowing = list.scrollHeight > list.clientHeight + 1;1730 const atBottom = list.scrollTop + list.clientHeight >= list.scrollHeight - 2;1731 const mask = overflowing && !atBottom1732 ? "linear-gradient(to bottom, black calc(100% - 24px), transparent 100%)"1733 : "none";1734 list.style.setProperty("mask-image", mask);1735 list.style.setProperty("-webkit-mask-image", mask);1736 };1737 list.addEventListener("scroll", updateListFade);1738 if (typedQuery && identities.length === 0) {1739 const empty = list.appendChild(document.createElement("div"));1740 empty.className = "identity-empty";1741 empty.textContent = identityMode === "both"1742 ? `No match for “${typedQuery}”.`1743 : `No ${identityFilterLabel(identityMode).toLowerCase()} match for “${typedQuery}”.`;1744 const custom = list.appendChild(document.createElement("div"));1745 custom.className = "identity-custom-row";1746 const customTitle = custom.appendChild(document.createElement("div"));1747 customTitle.className = "identity-custom-row-title";1748 customTitle.textContent = `Use “${typedQuery}” as a new ${customLabel}`;1749 const customSub = custom.appendChild(document.createElement("div"));1750 customSub.className = "identity-custom-row-sub";1751 customSub.textContent = "Press Enter or click to continue.";1752 custom.addEventListener("click", () => applyCustomIdentity(typedQuery));1753 }1754 for (const id of identities) {1755 const row = list.appendChild(document.createElement("div"));1756 row.className = "peach-identity-row";1757 row.setAttribute("tabindex", "-1");1758 const val = row.appendChild(document.createElement("div"));1759 val.className = "peach-identity-value";1760 val.textContent = id.value;1761 const meta = row.appendChild(document.createElement("div"));1762 meta.className = "peach-identity-meta";1763 meta.textContent = `used ${id.useCount}×`;1764 row.addEventListener("click", () => {1765 applyIdentity(id.value);1766 });1767 }17681769 updateListFade();17701771 1772 if (identities.length > 0) {1773 container.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));1774 }17751776 1777 const altRow = container.appendChild(document.createElement("div"));1778 altRow.className = "identity-alt-row";1779 altRow.textContent = `+ Use a different ${customLabel}…`;17801781 const altInputWrap = container.appendChild(document.createElement("div"));1782 altInputWrap.className = "identity-alt-input-wrap";1783 const altInput = altInputWrap.appendChild(document.createElement("input"));1784 altInput.className = "identity-alt-input";1785 altInput.type = "text";1786 altInput.placeholder = customLabel === "email" ? "[email protected]" : "your username";1787 altInput.setAttribute("autocomplete", "off");1788 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1789 altConfirm.className = "identity-alt-confirm";1790 altConfirm.type = "button";1791 altConfirm.textContent = "✓";17921793 function fillCustomIdentity() {1794 const v = altInput.value.trim();1795 if (!v) return;1796 applyCustomIdentity(v);1797 }17981799 altRow.addEventListener("click", () => {1800 altRow.style.display = "none";1801 altInputWrap.classList.add("open");1802 altInput.value = typedQuery || altInput.value;1803 altInput.focus();1804 altInput.setSelectionRange(altInput.value.length, altInput.value.length);1805 });1806 altConfirm.addEventListener("click", fillCustomIdentity);1807 altInput.addEventListener("keydown", (e) => {1808 if (e.key === "Enter") { e.preventDefault(); fillCustomIdentity(); }1809 });18101811 1812 1813 1814 1815 1816 1817 1818 if (options.loadSearchableEntries || (options.searchableEntries?.length ?? 0) > 0) {1819 const escapeRow = container.appendChild(document.createElement("div"));1820 escapeRow.className = "identity-escape-row";1821 escapeRow.textContent = "I already have an account →";1822 escapeRow.setAttribute("tabindex", "0");1823 const toSearch = () => { void ensureSearchableLoaded(renderList); renderList(); };1824 escapeRow.addEventListener("click", toSearch);1825 escapeRow.addEventListener("keydown", (e) => {1826 if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toSearch(); }1827 });1828 }18291830 requestAnimationFrame(() => {1831 search.focus();1832 search.setSelectionRange(query.length, query.length);1833 });1834 }18351836 18371838183918401841 function renderSecondaryIdentityStep(stepKind: "email" | "username", firstValue: string) {1842 currentMode = "identity-helper";1843 container.innerHTML = "";18441845 const wrap = container.appendChild(document.createElement("div"));1846 wrap.className = "gen-view";18471848 1849 const hdrRow = wrap.appendChild(document.createElement("div"));1850 hdrRow.className = "domain-header";1851 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1852 eyebrow2.className = "domain-eyebrow";1853 eyebrow2.textContent = "NEW ACCOUNT";1854 const domName2 = hdrRow.appendChild(document.createElement("div"));1855 domName2.className = "domain-name";1856 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");18571858 1859 const conf = wrap.appendChild(document.createElement("div"));1860 conf.className = "gen-confirmed";1861 const confCheck = conf.appendChild(document.createElement("span"));1862 confCheck.className = "gen-confirmed-check";1863 confCheck.textContent = "✓";1864 const confVal = conf.appendChild(document.createElement("div"));1865 confVal.className = "gen-confirmed-val";1866 confVal.textContent = firstValue;18671868 const stepLbl = wrap.appendChild(document.createElement("div"));1869 stepLbl.className = "gen-label";1870 stepLbl.textContent = stepKind === "email" ? "Now choose your email" : "Now choose your username";18711872 1873 const identities = (options.savedIdentities ?? [])1874 .filter(s => s.type === stepKind)1875 .sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0))1876 .slice(0, 6);18771878 const list = wrap.appendChild(document.createElement("div"));1879 list.className = "identity-list";1880 for (const id of identities) {1881 const row = list.appendChild(document.createElement("div"));1882 row.className = "peach-identity-row";1883 row.setAttribute("tabindex", "-1");1884 const val = row.appendChild(document.createElement("div"));1885 val.className = "peach-identity-value";1886 val.textContent = id.value;1887 const meta = row.appendChild(document.createElement("div"));1888 meta.className = "peach-identity-meta";1889 meta.textContent = `used ${id.useCount}×`;1890 row.addEventListener("click", () => applySecondIdentity(id.value));1891 }18921893 1894 const altInputWrap = wrap.appendChild(document.createElement("div"));1895 altInputWrap.className = "identity-alt-input-wrap open";1896 const altInput = altInputWrap.appendChild(document.createElement("input"));1897 altInput.className = "identity-alt-input";1898 altInput.type = stepKind === "email" ? "email" : "text";1899 altInput.placeholder = stepKind === "email" ? "[email protected]" : "your username";1900 altInput.setAttribute("autocomplete", "off");1901 const altConfirm = altInputWrap.appendChild(document.createElement("button"));1902 altConfirm.className = "identity-alt-confirm";1903 altConfirm.type = "button";1904 altConfirm.textContent = "→";19051906 function applySecondIdentity(value: string) {1907 const trimmed = value.trim();1908 if (!trimmed) return;1909 1910 const [email, username] = stepKind === "email"1911 ? [trimmed, firstValue]1912 : [firstValue, trimmed];1913 if (stepKind === "email") {1914 options.onFillFields?.("", "", trimmed); 1915 } else {1916 options.onFillFields?.(trimmed, "", ""); 1917 }1918 renderGenerator(email, username);1919 }19201921 altConfirm.addEventListener("click", () => applySecondIdentity(altInput.value));1922 altInput.addEventListener("keydown", (e) => {1923 if (e.key === "Enter") { e.preventDefault(); applySecondIdentity(altInput.value); }1924 });19251926 1927 const skipRow = wrap.appendChild(document.createElement("div"));1928 skipRow.className = "identity-escape-row";1929 skipRow.textContent = `Skip ${stepKind} →`;1930 skipRow.setAttribute("tabindex", "0");1931 skipRow.addEventListener("click", () => {1932 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1933 renderGenerator(email, username);1934 });1935 skipRow.addEventListener("keydown", (e) => {1936 if (e.key === "Enter" || e.key === " ") {1937 e.preventDefault();1938 const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1939 renderGenerator(email, username);1940 }1941 });19421943 requestAnimationFrame(() => { altInput.focus(); });1944 }19451946 function renderGenerator(filledEmail: string, filledUsername?: string) {1947 container.innerHTML = "";1948 const sitePolicy = detectPasswordPolicy();19491950 const wrap = container.appendChild(document.createElement("div"));1951 wrap.className = "gen-view";19521953 1954 const hdrRow = wrap.appendChild(document.createElement("div"));1955 hdrRow.className = "domain-header";1956 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1957 eyebrow2.className = "domain-eyebrow";1958 eyebrow2.textContent = "NEW ACCOUNT";1959 const domName2 = hdrRow.appendChild(document.createElement("div"));1960 domName2.className = "domain-name";1961 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");19621963 1964 function confirmedRow(value: string) {1965 const conf = wrap.appendChild(document.createElement("div"));1966 conf.className = "gen-confirmed";1967 const confCheck = conf.appendChild(document.createElement("span"));1968 confCheck.className = "gen-confirmed-check";1969 confCheck.textContent = "✓";1970 const confVal = conf.appendChild(document.createElement("div"));1971 confVal.className = "gen-confirmed-val";1972 confVal.textContent = value;1973 }1974 if (filledEmail) confirmedRow(filledEmail);1975 if (filledUsername && filledUsername !== filledEmail) confirmedRow(filledUsername);19761977 const lbl = wrap.appendChild(document.createElement("div"));1978 lbl.className = "gen-label";1979 lbl.textContent = "Generate a password";19801981 let currentPw = "";1982 const smartGen = createSmartGenerator({1983 policy: sitePolicy,1984 onChange: (pw) => { currentPw = pw; },1985 });1986 wrap.appendChild(smartGen.root);19871988 const fillBtn = wrap.appendChild(document.createElement("button"));1989 fillBtn.className = "gen-fill-btn";1990 fillBtn.type = "button";1991 fillBtn.textContent = "Copy & fill";1992 fillBtn.addEventListener("click", async () => {1993 if (!currentPw) return;1994 fillBtn.disabled = true;1995 fillBtn.textContent = "Saving…";1996 1997 options.onFillFields?.("", currentPw, "");1998 1999 2000 const saveUsername = filledUsername || filledEmail;2001 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;2002 try {2003 const res = await chrome.runtime.sendMessage({2004 type: "ADD_ENTRY",2005 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: currentPw, type: "login" },2006 });2007 if ((res as { ok?: boolean }).ok) {2008 showSuccess(sTitle);2009 } else {2010 renderSaveOffer(filledEmail, currentPw, filledUsername);2011 }2012 } catch {2013 renderSaveOffer(filledEmail, currentPw, filledUsername);2014 }2015 });2016 }20172018 function renderSaveOffer(filledEmail: string, filledPassword: string, filledUsername?: string) {2019 container.innerHTML = "";2020 const domain = options.domain ?? location.hostname.replace(/^www\./, "");20212022 const wrap = container.appendChild(document.createElement("div"));2023 wrap.className = "save-offer";20242025 const title = wrap.appendChild(document.createElement("div"));2026 title.className = "save-offer-title";2027 title.textContent = "Save this login to Peach?";20282029 function detailRow(label: string, value: string) {2030 const row = wrap.appendChild(document.createElement("div"));2031 row.className = "save-offer-row";2032 const lbl = row.appendChild(document.createElement("div"));2033 lbl.className = "save-offer-lbl";2034 lbl.textContent = label;2035 const val = row.appendChild(document.createElement("div"));2036 val.className = "save-offer-val";2037 val.textContent = value;2038 }20392040 detailRow("site", domain);2041 2042 if (filledUsername) {2043 detailRow("username", filledUsername);2044 if (filledEmail) detailRow("email", filledEmail);2045 } else {2046 if (filledEmail) detailRow("email", filledEmail);2047 }2048 detailRow("password", "••••••••••••••••");20492050 const btns = wrap.appendChild(document.createElement("div"));2051 btns.className = "save-offer-btns";20522053 const saveBtn = btns.appendChild(document.createElement("button"));2054 saveBtn.className = "save-offer-save";2055 saveBtn.type = "button";2056 saveBtn.textContent = "Save";2057 saveBtn.addEventListener("click", async () => {2058 saveBtn.disabled = true;2059 saveBtn.textContent = "Saving…";2060 try {2061 2062 2063 const saveUsername = filledUsername || filledEmail;2064 const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;2065 const res = await chrome.runtime.sendMessage({2066 type: "ADD_ENTRY",2067 payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: filledPassword, type: "login" },2068 });2069 if ((res as { ok?: boolean }).ok) {2070 showSuccess(sTitle);2071 } else {2072 saveBtn.disabled = false;2073 saveBtn.textContent = "Save";2074 }2075 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save"; }2076 });20772078 const skipBtn = btns.appendChild(document.createElement("button"));2079 skipBtn.className = "save-offer-skip";2080 skipBtn.type = "button";2081 skipBtn.textContent = "Not now";2082 skipBtn.addEventListener("click", () => { dismiss(true); });2083 }20842085 function renderList() {2086 container.innerHTML = "";2087 focusedIndex = -1;2088 searchInput = null;2089 currentMode = "list";20902091 const current = activeEntries();20922093 2094 2095 2096 const domHdr = document.createElement("div");2097 domHdr.className = "domain-header";2098 const headText = document.createElement("div");2099 headText.className = "domain-header-text";2100 const eyebrow = document.createElement("div");2101 eyebrow.className = "domain-eyebrow";2102 eyebrow.textContent = options.isSignup2103 ? "Sign up as"2104 : entries.length > 0 ? "Saved for this site" : "No saved login";2105 const domName = document.createElement("div");2106 domName.className = "domain-name";2107 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2108 headText.append(eyebrow, domName);2109 domHdr.appendChild(headText);21102111 if (!options.isSignup) {2112 const actions = document.createElement("div");2113 actions.className = "domain-header-actions";21142115 2116 2117 2118 if (silverPlatterOrigin) {2119 const backBtn = document.createElement("button");2120 backBtn.className = "domain-search-btn";2121 backBtn.type = "button";2122 backBtn.setAttribute("aria-label", "Back to suggested entry");2123 backBtn.title = "Back to suggested entry";2124 backBtn.innerHTML = backSvgStr();2125 backBtn.addEventListener("click", (e) => {2126 e.stopPropagation();2127 if (silverPlatterOrigin) renderSilverPlatter(silverPlatterOrigin);2128 });2129 actions.appendChild(backBtn);2130 }21312132 const addBtn = document.createElement("button");2133 addBtn.className = "domain-add-btn";2134 addBtn.type = "button";2135 addBtn.setAttribute("aria-label", "Create new entry");2136 addBtn.title = "Create new entry";2137 addBtn.textContent = "+";2138 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2139 actions.appendChild(addBtn);21402141 domHdr.appendChild(actions);2142 }21432144 container.appendChild(domHdr);21452146 2147 const sw = document.createElement("div"); sw.className = "search-wrap";2148 searchInput = document.createElement("input");2149 searchInput.className = "search";2150 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";2151 searchInput.value = query;2152 searchInput.addEventListener("input", () => {2153 query = searchInput?.value ?? "";2154 2155 2156 void ensureSearchableLoaded(renderList);2157 renderList();2158 searchInput?.focus();2159 searchInput?.setSelectionRange(query.length, query.length);2160 });2161 sw.appendChild(searchInput);2162 2163 2164 if (searchableLoading) {2165 const loadingHint = document.createElement("span");2166 loadingHint.className = "search-loading";2167 loadingHint.textContent = "searching…";2168 sw.appendChild(loadingHint);2169 }2170 container.appendChild(sw);21712172 2173 const results = document.createElement("div");2174 results.className = "results";2175 container.appendChild(results);21762177 if (current.length === 0 && entries.length > 0) {2178 2179 }21802181 for (let i = 0; i < current.length; i++) {2182 const e = current[i]!;2183 const entryEl = document.createElement("div");2184 entryEl.className = "entry";2185 entryEl.dataset.id = e.id;2186 entryEl.dataset.index = String(i);2187 entryEl.setAttribute("tabindex", "-1");21882189 entryEl.appendChild(makeEntryIcon(e.url));21902191 const info = document.createElement("div"); info.className = "entry-info";2192 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;2193 const subEl = document.createElement("div"); subEl.className = "entry-sub";2194 const domain = e.url ? getDomainFromUrl(e.url) : "";2195 subEl.textContent = domain || e.username || "";2196 info.append(titleEl, subEl);2197 entryEl.appendChild(info);21982199 if (e.hasTotp) {2200 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";2201 entryEl.appendChild(badge);2202 }2203 if (e.hasPasskey) {2204 const badge = document.createElement("span"); badge.className = "passkey-badge"; badge.textContent = "passkey";2205 entryEl.appendChild(badge);2206 }22072208 entryEl.addEventListener("click", () => {2209 if (options.isSignup && options.onFillFields) {2210 2211 options.onFillFields(e.username ?? "", "", e.email);2212 dismiss();2213 } else {2214 onSelect(e.id);2215 dismiss();2216 }2217 });2218 results.appendChild(entryEl);2219 }22202221 2222 if (current.length > 0) {2223 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));2224 }22252226 2227 const newEl = document.createElement("div");2228 newEl.className = "entry";2229 newEl.setAttribute("tabindex", "-1");2230 newEl.appendChild(makeEntryIcon(undefined, true));22312232 const newInfo = document.createElement("div"); newInfo.className = "entry-info";2233 const newTitle = document.createElement("div");2234 newTitle.className = "entry-title is-new";2235 newTitle.textContent = `New entry for ${sTitle}`;2236 const newSub = document.createElement("div");2237 newSub.className = "entry-sub";2238 newSub.textContent = sOrigin;2239 newInfo.append(newTitle, newSub);2240 newEl.appendChild(newInfo);2241 newEl.addEventListener("click", showCreateForm);2242 results.appendChild(newEl);22432244 2245 if (searchInput) {2246 requestAnimationFrame(() => {2247 searchInput?.focus();2248 2249 2250 searchInput?.setSelectionRange(query.length, query.length);2251 });2252 }2253 }22542255 function setFocused(idx: number) {2256 const all = container.querySelectorAll<HTMLElement>(".entry");2257 all.forEach((el, i) => el.classList.toggle("focused", i === idx));2258 focusedIndex = idx;2259 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });2260 }22612262 function attachIdentitySuggestions(g: HTMLDivElement, inp: HTMLInputElement, identities: SavedIdentity[], fieldType: "username" | "email"): void {2263 const typed = identities.filter(s => s.type === fieldType);2264 if (!typed.length) return;2265 let collapsed = false;2266 let suppressNextFocus = false;22672268 const list = g.appendChild(document.createElement("div"));2269 list.className = "identity-suggest-list";2270 list.style.display = "none";22712272 2273 const collapseRow = list.appendChild(document.createElement("div"));2274 collapseRow.className = "identity-filter-row";2275 collapseRow.style.justifyContent = "flex-end";2276 const collapseBtn = collapseRow.appendChild(document.createElement("button"));2277 collapseBtn.type = "button";2278 collapseBtn.className = "identity-collapse-btn";2279 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>`;2280 collapseBtn.addEventListener("mousedown", (e) => {2281 e.preventDefault();2282 collapsed = !collapsed;2283 collapseBtn.classList.toggle("is-collapsed", collapsed);2284 rowsWrapper.classList.toggle("is-collapsed", collapsed);2285 });22862287 const rowsWrapper = list.appendChild(document.createElement("div"));2288 rowsWrapper.className = "identity-rows";2289 const rows = rowsWrapper.appendChild(document.createElement("div"));2290 rows.className = "identity-rows-inner";22912292 2293 2294 2295 const updateRowsFade = () => {2296 const overflowing = rows.scrollHeight > rows.clientHeight + 1;2297 const atBottom = rows.scrollTop + rows.clientHeight >= rows.scrollHeight - 2;2298 const mask = overflowing && !atBottom2299 ? "linear-gradient(to bottom, black calc(100% - 18px), transparent 100%)"2300 : "none";2301 rows.style.setProperty("mask-image", mask);2302 rows.style.setProperty("-webkit-mask-image", mask);2303 };2304 rows.addEventListener("scroll", updateRowsFade);23052306 2307 function hideList() {2308 list.style.display = "none";2309 document.removeEventListener("mousedown", onDocMousedown, { capture: true });2310 }23112312 2313 2314 2315 2316 2317 function onDocMousedown(e: MouseEvent) {2318 if (e.composedPath().includes(host)) return;2319 hideList();2320 }23212322 function pick(value: string) {2323 inp.value = value;2324 hideList();2325 suppressNextFocus = true;2326 inp.focus();2327 2328 2329 2330 setTimeout(() => { suppressNextFocus = false; }, 120);2331 }23322333 function render() {2334 const q = inp.value.toLowerCase();2335 const filtered = typed.filter(s => !q || s.value.toLowerCase().includes(q)).slice(0, 8);2336 rows.innerHTML = "";2337 if (!filtered.length) { hideList(); return; }2338 for (const s of filtered) {2339 const row = rows.appendChild(document.createElement("button"));2340 row.type = "button"; row.className = "identity-suggest-row";2341 const val = row.appendChild(document.createElement("span"));2342 val.className = "identity-suggest-val"; val.textContent = s.value;2343 const meta = row.appendChild(document.createElement("span"));2344 meta.className = "identity-suggest-meta"; meta.textContent = `${s.useCount}×`;2345 row.addEventListener("mousedown", (e) => { e.preventDefault(); pick(s.value); });2346 row.addEventListener("keydown", (e) => {2347 if (e.key === "ArrowDown") {2348 e.preventDefault();2349 (row.nextElementSibling as HTMLElement | null)?.focus();2350 } else if (e.key === "ArrowUp") {2351 e.preventDefault();2352 const prev = row.previousElementSibling as HTMLElement | null;2353 if (prev) prev.focus();2354 else inp.focus();2355 } else if (e.key === "Enter" || e.key === " ") {2356 e.preventDefault();2357 pick(s.value);2358 } else if (e.key === "Escape") {2359 e.preventDefault();2360 hideList();2361 suppressNextFocus = true;2362 inp.focus();2363 }2364 });2365 2366 2367 2368 row.addEventListener("blur", (e) => {2369 const rt = (e as FocusEvent).relatedTarget as Node | null;2370 if (rt && (rt === inp || list.contains(rt))) return;2371 setTimeout(() => { hideList(); }, 150);2372 });2373 }2374 list.style.display = "";2375 updateRowsFade();2376 2377 2378 2379 document.addEventListener("mousedown", onDocMousedown, { capture: true });2380 }23812382 inp.addEventListener("focus", () => {2383 if (suppressNextFocus) { suppressNextFocus = false; return; }2384 render();2385 });2386 inp.addEventListener("input", render);2387 inp.addEventListener("keydown", (e) => {2388 if (list.style.display === "none") return;2389 if (e.key === "ArrowDown" && !collapsed) {2390 e.preventDefault();2391 (rows.firstElementChild as HTMLElement | null)?.focus();2392 } else if (e.key === "Escape") {2393 e.preventDefault();2394 hideList();2395 } else if (e.key === "Tab") {2396 2397 hideList();2398 }2399 });2400 2401 2402 2403 2404 24052406 2407 2408 cleanupHooks.push(hideList);2409 }24102411 async function showCreateForm() {2412 currentMode = "create";2413 container.innerHTML = "";24142415 2416 2417 2418 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;2419 if (!statusRes?.unlocked) {2420 renderLocked();2421 return;2422 }24232424 2425 2426 const sitePolicy = detectPasswordPolicy();24272428 2429 2430 let formIdentities: SavedIdentity[] = options.savedIdentities ?? [];2431 if (!formIdentities.length) {2432 try {2433 const idRes = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as { ok: boolean; data?: SavedIdentity[] } | null;2434 if (idRes?.ok && Array.isArray(idRes.data)) formIdentities = idRes.data;2435 } catch { }2436 }24372438 const wrap = document.createElement("div");2439 wrap.className = "create-form-wrap";24402441 2442 const head = document.createElement("div"); head.className = "create-head";2443 const backBtn = document.createElement("button"); backBtn.className = "back-btn";2444 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>`;2445 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });2446 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";2447 head.append(backBtn, lbl);24482449 2450 const body = document.createElement("div"); body.className = "create-body";24512452 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {2453 const g = document.createElement("div"); g.className = "f-group";2454 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;2455 const inp = document.createElement("input"); inp.className = "f-input";2456 g.append(l, inp); return { g, inp };2457 }24582459 2460 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";2461 body.appendChild(tg.g);24622463 2464 const ug = fGroup("website");2465 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";2466 if (loginSubdomain) {2467 const h = document.createElement("div"); h.className = "url-hint";2468 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);2469 }2470 body.appendChild(ug.g);24712472 2473 const xg = fGroup("username");2474 xg.inp.value = preUsername && !preUsername.includes("@") ? preUsername : "";2475 xg.inp.placeholder = "johndoe";2476 xg.inp.setAttribute("autocomplete", "off");2477 if (formIdentities.length) attachIdentitySuggestions(xg.g, xg.inp, formIdentities, "username");2478 body.appendChild(xg.g);24792480 2481 const eg = fGroup("email");2482 (eg.inp as HTMLInputElement).type = "email";2483 eg.inp.value = preUsername && preUsername.includes("@") ? preUsername : "";2484 eg.inp.placeholder = "[email protected]";2485 eg.inp.setAttribute("autocomplete", "off");2486 if (formIdentities.length) attachIdentitySuggestions(eg.g, eg.inp, formIdentities, "email");2487 body.appendChild(eg.g);24882489 2490 2491 2492 const pg = fGroup("password");2493 pg.inp.remove(); 2494 let currentPassword = prePassword || "";2495 const smartGen = createSmartGenerator({2496 policy: sitePolicy,2497 initialPassword: prePassword || undefined,2498 onChange: (pw) => { currentPassword = pw; },2499 });2500 pg.g.appendChild(smartGen.root);2501 body.appendChild(pg.g);25022503 2504 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";2505 saveBtn.textContent = "Save entry";2506 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";2507 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }2508 saveBtn.addEventListener("click", async () => {2509 const title = tg.inp.value.trim();2510 const pw = currentPassword.trim();2511 const username = xg.inp.value.trim();2512 const email = eg.inp.value.trim();2513 if (!title) {2514 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();2515 showFormError("Entry needs a title");2516 return;2517 }2518 if (!pw) {2519 showFormError("Entry needs a password — tap regen to make one");2520 return;2521 }2522 errEl.style.display = "none";2523 saveBtn.disabled = true; saveBtn.textContent = "Saving…";2524 try {2525 const res = await chrome.runtime.sendMessage({2526 type: "ADD_ENTRY",2527 payload: { title, url: ug.inp.value.trim(), username, email: email || undefined, password: pw, type: "login" },2528 });2529 if ((res as { ok?: boolean }).ok) {2530 2531 2532 options.onFillFields?.(username || email, pw);2533 showSuccess(title);2534 } else {2535 const err = (res as { error?: string })?.error;2536 const errMsg = err === "vault_locked"2537 ? "Vault locked — open Peach to save"2538 : err === "STORAGE_QUOTA_EXCEEDED"2539 ? "Out of storage — open Peach → Settings to free space"2540 : err2541 ? `Could not save: ${err}`2542 : "Could not save entry";2543 showFormError(errMsg);2544 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2545 }2546 } catch (e) {2547 showFormError(e instanceof Error && e.message ? `Could not reach Peach: ${e.message}` : "Could not reach Peach");2548 saveBtn.disabled = false; saveBtn.textContent = "Save entry";2549 }2550 });2551 body.appendChild(saveBtn);2552 body.appendChild(errEl);25532554 wrap.append(head, body);2555 container.appendChild(wrap);2556 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });2557 }25582559 function showSuccess(title: string) {2560 currentMode = "success";2561 container.innerHTML = "";2562 const wrap = document.createElement("div"); wrap.className = "success-wrap";2563 const ring = document.createElement("div"); ring.className = "success-ring";2564 ring.innerHTML = checkSvgStr();2565 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;2566 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;2567 wrap.append(ring, t, s);2568 container.appendChild(wrap);2569 setTimeout(() => dismiss(), 1600);2570 }25712572 function flashActionBtn(btn: HTMLButtonElement) {2573 btn.classList.add("flashed");2574 setTimeout(() => btn.classList.remove("flashed"), 700);2575 }25762577 2578 2579 function flashCopyError(btn: HTMLButtonElement) {2580 const original = btn.textContent ?? "";2581 btn.textContent = "Blocked";2582 btn.classList.add("flashed");2583 setTimeout(() => {2584 btn.classList.remove("flashed");2585 if (btn.isConnected) btn.textContent = original;2586 }, 1200);2587 }25882589 function renderSilverPlatter(entry: SelectorEntry) {2590 container.innerHTML = "";2591 searchInput = null;2592 focusedIndex = -1;2593 currentMode = "silver-platter";2594 silverPlatterOrigin = entry;25952596 const domHdr = document.createElement("div");2597 domHdr.className = "domain-header";2598 const headText = document.createElement("div");2599 headText.className = "domain-header-text";2600 const eyebrow = document.createElement("div");2601 eyebrow.className = "domain-eyebrow";2602 eyebrow.textContent = "Saved for this site";2603 const domName = document.createElement("div");2604 domName.className = "domain-name";2605 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");2606 headText.append(eyebrow, domName);2607 domHdr.appendChild(headText);2608 if (!options.isSignup) {2609 const actions = document.createElement("div");2610 actions.className = "domain-header-actions";26112612 2613 2614 2615 const searchBtn = document.createElement("button");2616 searchBtn.className = "domain-search-btn";2617 searchBtn.type = "button";2618 searchBtn.setAttribute("aria-label", "Search for a different entry");2619 searchBtn.title = "Search for a different entry";2620 searchBtn.innerHTML = searchSvgStr();2621 searchBtn.addEventListener("click", (e) => {2622 e.stopPropagation();2623 query = "";2624 renderList();2625 });2626 actions.appendChild(searchBtn);26272628 const addBtn = document.createElement("button");2629 addBtn.className = "domain-add-btn";2630 addBtn.type = "button";2631 addBtn.setAttribute("aria-label", "Create new entry");2632 addBtn.title = "Create new entry";2633 addBtn.textContent = "+";2634 addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });2635 actions.appendChild(addBtn);26362637 domHdr.appendChild(actions);2638 }2639 container.appendChild(domHdr);26402641 const platter = document.createElement("div");2642 platter.className = "silver-platter";26432644 const titleEl = document.createElement("div");2645 titleEl.className = "sp-title";2646 titleEl.textContent = entry.title;2647 platter.appendChild(titleEl);26482649 if (entry.email) {2650 const emailEl = document.createElement("div");2651 emailEl.className = "sp-identity-row";2652 const emailLbl = document.createElement("span");2653 emailLbl.className = "sp-identity-label";2654 emailLbl.textContent = "email";2655 const emailVal = document.createElement("span");2656 emailVal.className = "sp-username";2657 emailVal.textContent = entry.email;2658 emailEl.append(emailLbl, emailVal);2659 platter.appendChild(emailEl);2660 }26612662 if (entry.username) {2663 const userEl = document.createElement("div");2664 userEl.className = "sp-identity-row";2665 const userLbl = document.createElement("span");2666 userLbl.className = "sp-identity-label";2667 userLbl.textContent = entry.username.includes("@") ? "email" : "username";2668 const userVal = document.createElement("span");2669 userVal.className = "sp-username";2670 userVal.textContent = entry.username;2671 userEl.append(userLbl, userVal);2672 platter.appendChild(userEl);2673 }26742675 const fillBtn = document.createElement("button");2676 fillBtn.className = "sp-fill-btn";2677 fillBtn.type = "button";2678 fillBtn.textContent = "Fill credentials";2679 fillBtn.addEventListener("click", () => {2680 if (fillBtn.disabled) return;2681 fillBtn.disabled = true;2682 fillBtn.classList.add("filled");2683 fillBtn.innerHTML = checkSvgStr();2684 2685 2686 2687 2688 onSelect(entry.id);2689 setTimeout(() => dismiss(), 400);2690 });2691 platter.appendChild(fillBtn);26922693 const actionsGroup = document.createElement("div");2694 actionsGroup.className = "sp-actions-group";2695 const actionsLabel = document.createElement("div");2696 actionsLabel.className = "sp-actions-label";2697 actionsLabel.textContent = "Copy";2698 actionsGroup.appendChild(actionsLabel);26992700 const actions = document.createElement("div");2701 actions.className = "sp-actions";2702 actionsGroup.appendChild(actions);27032704 if (entry.email) {2705 const copyEmail = document.createElement("button");2706 copyEmail.className = "sp-action-btn";2707 copyEmail.type = "button";2708 copyEmail.title = "Copy email";2709 copyEmail.textContent = "Email";2710 copyEmail.addEventListener("click", async (e) => {2711 e.stopPropagation();2712 let alreadyWritten = false;2713 try {2714 await navigator.clipboard.writeText(entry.email!);2715 alreadyWritten = true;2716 } catch (err) {2717 console.warn("[Peach] clipboard write denied:", err);2718 }2719 const resp = await chrome.runtime.sendMessage({2720 type: "COPY",2721 payload: { text: entry.email, label: "Email", alreadyWritten },2722 }).catch(() => null) as { ok?: boolean } | null;2723 if (alreadyWritten || resp?.ok) flashActionBtn(copyEmail);2724 else flashCopyError(copyEmail);2725 });2726 actions.appendChild(copyEmail);2727 }27282729 const copyU = document.createElement("button");2730 copyU.className = "sp-action-btn";2731 copyU.type = "button";2732 const usernameIsEmail = entry.username?.includes("@") ?? false;2733 copyU.title = usernameIsEmail ? "Copy email" : "Copy username";2734 copyU.textContent = usernameIsEmail ? "Email" : "Username";2735 copyU.disabled = !entry.username;2736 copyU.addEventListener("click", async (e) => {2737 e.stopPropagation();2738 if (!entry.username) return;2739 let alreadyWritten = false;2740 try {2741 await navigator.clipboard.writeText(entry.username);2742 alreadyWritten = true;2743 } catch (err) {2744 console.warn("[Peach] clipboard write denied:", err);2745 }2746 const resp = await chrome.runtime.sendMessage({2747 type: "COPY",2748 payload: { text: entry.username, label: entry.username.includes("@") ? "Email" : "Username", alreadyWritten },2749 }).catch(() => null) as { ok?: boolean } | null;2750 if (alreadyWritten || resp?.ok) flashActionBtn(copyU);2751 else flashCopyError(copyU);2752 });2753 actions.appendChild(copyU);27542755 const copyP = document.createElement("button");2756 copyP.className = "sp-action-btn";2757 copyP.type = "button";2758 copyP.title = "Copy password";2759 copyP.textContent = "Password";2760 copyP.addEventListener("click", async (e) => {2761 e.stopPropagation();2762 const resp = await chrome.runtime.sendMessage({2763 type: "GET_ENTRY_FOR_FILL",2764 2765 payload: { id: entry.id, explicitSummon: true },2766 }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;2767 if (!resp?.ok || !resp.data?.password) return;2768 let alreadyWritten = false;2769 try {2770 await navigator.clipboard.writeText(resp.data.password);2771 alreadyWritten = true;2772 } catch (err) {2773 console.warn("[Peach] clipboard write denied:", err);2774 }2775 const copyResp = await chrome.runtime.sendMessage({2776 type: "COPY",2777 payload: { text: resp.data.password, label: "Password", alreadyWritten },2778 }).catch(() => null) as { ok?: boolean } | null;2779 if (alreadyWritten || copyResp?.ok) flashActionBtn(copyP);2780 else flashCopyError(copyP);2781 });2782 actions.appendChild(copyP);27832784 let trayEl: HTMLDivElement | null = null;2785 let trayInterval: ReturnType<typeof setInterval> | null = null;27862787 function closeTray() {2788 if (trayInterval) { clearInterval(trayInterval); trayInterval = null; }2789 trayEl?.remove();2790 trayEl = null;2791 }2792 2793 2794 cleanupHooks.push(closeTray);27952796 async function openTOTPTray() {2797 2798 2799 2800 2801 async function fetchCode(): Promise<{ code: string; remaining: number } | null> {2802 const r = await chrome.runtime.sendMessage({2803 type: "GET_ENTRY_FOR_FILL",2804 2805 payload: { id: entry.id, explicitSummon: true },2806 }).catch(() => null) as { ok: boolean; data?: { totpCode?: string; totpRemaining?: number } } | null;2807 if (!r?.ok || !r.data?.totpCode) return null;2808 return { code: r.data.totpCode, remaining: r.data.totpRemaining ?? 30 };2809 }2810 let current = await fetchCode();2811 if (!current) return;28122813 trayEl = document.createElement("div");2814 trayEl.className = "sp-tray";28152816 const label = document.createElement("span");2817 label.className = "sp-tray-label";2818 label.textContent = "2FA";2819 trayEl.appendChild(label);28202821 const codeEl = document.createElement("div");2822 codeEl.className = "sp-totp-code";2823 trayEl.appendChild(codeEl);28242825 const ringNs = "http://www.w3.org/2000/svg";2826 const ring = document.createElementNS(ringNs, "svg");2827 ring.setAttribute("class", "sp-totp-ring");2828 ring.setAttribute("viewBox", "0 0 24 24");2829 const trackC = document.createElementNS(ringNs, "circle");2830 trackC.setAttribute("class", "ring-track");2831 trackC.setAttribute("cx", "12"); trackC.setAttribute("cy", "12"); trackC.setAttribute("r", "10");2832 trackC.setAttribute("fill", "none"); trackC.setAttribute("stroke-width", "2");2833 const fillC = document.createElementNS(ringNs, "circle");2834 fillC.setAttribute("class", "ring-fill");2835 fillC.setAttribute("cx", "12"); fillC.setAttribute("cy", "12"); fillC.setAttribute("r", "10");2836 fillC.setAttribute("fill", "none"); fillC.setAttribute("stroke-width", "2");2837 fillC.setAttribute("stroke-linecap", "round");2838 fillC.setAttribute("stroke-dasharray", "62.83");2839 ring.append(trackC, fillC);2840 trayEl.appendChild(ring);28412842 const copyBtn = document.createElement("button");2843 copyBtn.className = "sp-totp-copy";2844 copyBtn.type = "button";2845 copyBtn.textContent = "Copy";2846 trayEl.appendChild(copyBtn);28472848 function render() {2849 const { code, remaining } = current!;2850 2851 codeEl.textContent = code.length === 62852 ? `${code.slice(0, 3)} ${code.slice(3)}`2853 : code;2854 2855 2856 const fraction = (30 - remaining) / 30;2857 fillC.setAttribute("stroke-dashoffset", String(62.83 * fraction));2858 }2859 render();2860 trayInterval = setInterval(async () => {2861 if (!current) return;2862 current = { code: current.code, remaining: current.remaining - 1 };2863 2864 if (current.remaining <= 0) {2865 const next = await fetchCode();2866 current = next ?? { code: current.code, remaining: 30 };2867 }2868 render();2869 }, 1000);28702871 copyBtn.addEventListener("click", async (e) => {2872 e.stopPropagation();2873 const code = current?.code;2874 if (!code) return;2875 let alreadyWritten = false;2876 try {2877 await navigator.clipboard.writeText(code);2878 alreadyWritten = true;2879 } catch (err) {2880 console.warn("[Peach] clipboard write denied:", err);2881 }2882 const resp = await chrome.runtime.sendMessage({2883 type: "COPY",2884 payload: { text: code, label: "TOTP", alreadyWritten },2885 }).catch(() => null) as { ok?: boolean } | null;2886 copyBtn.textContent = alreadyWritten || resp?.ok ? "Copied" : "Blocked";2887 setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = "Copy"; }, 1200);2888 });28892890 platter.appendChild(trayEl);2891 }28922893 const editBtn = document.createElement("button");2894 editBtn.className = "sp-action-btn sp-action-edit";2895 editBtn.type = "button";2896 editBtn.title = "Edit entry";2897 editBtn.textContent = "Edit";2898 editBtn.addEventListener("click", (e) => {2899 e.stopPropagation();2900 closeTray();2901 void renderInlineEdit(entry);2902 });2903 actions.appendChild(editBtn);29042905 2906 2907 2908 if (entry.hasTotp) void openTOTPTray();29092910 platter.appendChild(actionsGroup);2911 container.appendChild(platter);2912 }29132914 291529162917291829192920 async function renderInlineEdit(summary: SelectorEntry) {2921 currentMode = "edit";2922 container.innerHTML = "";2923 const wrap = document.createElement("div");2924 wrap.className = "create-form-wrap";29252926 const head = document.createElement("div");2927 head.className = "create-head";2928 const back = document.createElement("button");2929 back.className = "back-btn";2930 back.type = "button";2931 back.title = "Cancel";2932 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>`;2933 back.addEventListener("click", () => renderSilverPlatter(summary));2934 head.appendChild(back);2935 const headLbl = document.createElement("div");2936 headLbl.className = "create-label";2937 headLbl.textContent = "Edit entry";2938 head.appendChild(headLbl);2939 wrap.appendChild(head);29402941 const body = document.createElement("div");2942 body.className = "create-body";29432944 2945 const loading = document.createElement("div");2946 loading.className = "empty-hint";2947 loading.textContent = "loading…";2948 body.appendChild(loading);2949 wrap.appendChild(body);2950 container.appendChild(wrap);29512952 const [res, idRes] = await Promise.all([2953 2954 2955 chrome.runtime.sendMessage({2956 type: "GET_ENTRY_FOR_EDIT",2957 payload: { id: summary.id },2958 }).catch(() => null),2959 chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null),2960 ]);2961 const entryRes = res as { ok: boolean; data?: { title?: string; username?: string; email?: string; password?: string; url?: string; totp?: string; notes?: string; passwordHistory?: PasswordHistoryItem[] } } | null;2962 const editIdentities: SavedIdentity[] = (idRes as { ok?: boolean; data?: SavedIdentity[] } | null)?.data ?? [];29632964 if (!entryRes || !entryRes.ok || !entryRes.data) {2965 loading.textContent = "couldn't load this entry. try again from the popup.";2966 return;2967 }2968 body.innerHTML = "";29692970 function field(label: string, value: string, type: "text" | "password" = "text"): { g: HTMLDivElement; inp: HTMLInputElement } {2971 const group = document.createElement("div");2972 group.className = "f-group";2973 const lbl = document.createElement("div");2974 lbl.className = "f-label";2975 lbl.textContent = label;2976 const input = document.createElement("input");2977 input.className = "f-input";2978 input.type = type;2979 input.value = value;2980 input.autocomplete = "off";2981 group.append(lbl, input);2982 body.appendChild(group);2983 return { g: group, inp: input };2984 }29852986 const titleInput = field("title", entryRes.data.title ?? "").inp;2987 const urlInput = field("url", entryRes.data.url ?? "").inp;2988 urlInput.classList.add("url-f");2989 const usernameLabel = (entryRes.data.username ?? "").includes("@") ? "email" : "username";2990 const { g: usernameGroup, inp: usernameInput } = field(usernameLabel, entryRes.data.username ?? "");2991 if (editIdentities.length) attachIdentitySuggestions(usernameGroup, usernameInput, editIdentities, "username");2992 const { g: emailGroup, inp: emailInput } = field("email", entryRes.data.email ?? "");2993 if (editIdentities.length) attachIdentitySuggestions(emailGroup, emailInput, editIdentities, "email");29942995 2996 const pwGroup = document.createElement("div");2997 pwGroup.className = "f-group";2998 const pwLbl = document.createElement("div");2999 pwLbl.className = "f-label";3000 pwLbl.textContent = "password";3001 const pwRow = document.createElement("div");3002 pwRow.className = "f-row";3003 const pwInput = document.createElement("input");3004 pwInput.className = "f-input";3005 pwInput.type = "password";3006 pwInput.value = entryRes.data.password ?? "";3007 pwInput.autocomplete = "new-password";3008 const revealBtn = document.createElement("button");3009 revealBtn.className = "gen-btn";3010 revealBtn.type = "button";3011 revealBtn.textContent = "show";3012 revealBtn.addEventListener("click", () => {3013 const reveal = pwInput.type === "password";3014 pwInput.type = reveal ? "text" : "password";3015 revealBtn.textContent = reveal ? "hide" : "show";3016 });3017 pwRow.append(pwInput, revealBtn);3018 const editSmartGen = createSmartGenerator({ policy: detectPasswordPolicy(), onChange: (pw) => { pwInput.value = pw; } });3019 editSmartGen.root.style.display = "none";3020 let editGenVisible = false;3021 const genToggleBtn = document.createElement("button");3022 genToggleBtn.className = "gen-toggle-link";3023 genToggleBtn.type = "button";3024 genToggleBtn.textContent = "⚙ generate";3025 genToggleBtn.addEventListener("click", () => {3026 editGenVisible = !editGenVisible;3027 pwRow.style.display = editGenVisible ? "none" : "";3028 editSmartGen.root.style.display = editGenVisible ? "" : "none";3029 genToggleBtn.textContent = editGenVisible ? "☰ hide generator" : "⚙ generate";3030 });3031 pwGroup.append(pwLbl, pwRow, genToggleBtn, editSmartGen.root);30323033 3034 3035 3036 3037 3038 const history = entryRes.data.passwordHistory ?? [];3039 {3040 const histList = document.createElement("div");3041 histList.className = "pw-history";3042 histList.style.display = "none";3043 let histVisible = false;30443045 const restLabel = history.length ? `↺ history (${history.length})` : "↺ history";3046 const histToggle = document.createElement("button");3047 histToggle.className = "gen-toggle-link";3048 histToggle.type = "button";3049 histToggle.textContent = restLabel;3050 histToggle.addEventListener("click", () => {3051 histVisible = !histVisible;3052 histList.style.display = histVisible ? "" : "none";3053 histToggle.textContent = histVisible ? "↺ hide history" : restLabel;3054 });30553056 if (history.length === 0) {3057 const empty = document.createElement("div");3058 empty.className = "pw-history-empty";3059 empty.textContent = "No earlier passwords yet. When you change this password, the old one is saved here so you can copy or restore it.";3060 histList.appendChild(empty);3061 }30623063 for (const item of history) {3064 const row = document.createElement("div");3065 row.className = "pw-history-row";3066 const main = document.createElement("div");3067 main.className = "pw-history-main";3068 const pwText = document.createElement("div");3069 pwText.className = "pw-history-pw";3070 let revealed = false;3071 const mask = "••••••••••••";3072 pwText.textContent = mask;3073 const age = document.createElement("div");3074 age.className = "pw-history-age";3075 age.textContent = timeSince(item.changedAt);3076 main.append(pwText, age);30773078 const acts = document.createElement("div");3079 acts.className = "pw-history-actions";3080 const revealBtn2 = document.createElement("button");3081 revealBtn2.className = "pw-history-btn";3082 revealBtn2.type = "button";3083 revealBtn2.textContent = "show";3084 revealBtn2.addEventListener("click", () => {3085 revealed = !revealed;3086 pwText.textContent = revealed ? item.password : mask;3087 revealBtn2.textContent = revealed ? "hide" : "show";3088 });3089 const copyBtn2 = document.createElement("button");3090 copyBtn2.className = "pw-history-btn";3091 copyBtn2.type = "button";3092 copyBtn2.textContent = "copy";3093 copyBtn2.addEventListener("click", async () => {3094 let alreadyWritten = false;3095 try {3096 await navigator.clipboard.writeText(item.password);3097 alreadyWritten = true;3098 } catch (err) {3099 console.warn("[Peach] clipboard write denied:", err);3100 }3101 await chrome.runtime.sendMessage({3102 type: "COPY",3103 payload: { text: item.password, label: "Password", alreadyWritten },3104 }).catch(() => null);3105 copyBtn2.textContent = "copied";3106 setTimeout(() => { if (copyBtn2.isConnected) copyBtn2.textContent = "copy"; }, 1200);3107 });3108 const useBtn = document.createElement("button");3109 useBtn.className = "pw-history-btn";3110 useBtn.type = "button";3111 useBtn.textContent = "use";3112 useBtn.title = "Use this password again";3113 useBtn.addEventListener("click", () => {3114 pwInput.value = item.password;3115 pwInput.type = "text";3116 revealBtn.textContent = "hide";3117 if (editGenVisible) {3118 editGenVisible = false;3119 pwRow.style.display = "";3120 editSmartGen.root.style.display = "none";3121 genToggleBtn.textContent = "⚙ generate";3122 }3123 histVisible = false;3124 histList.style.display = "none";3125 histToggle.textContent = `↺ history (${history.length})`;3126 });3127 acts.append(revealBtn2, copyBtn2, useBtn);3128 row.append(main, acts);3129 histList.appendChild(row);3130 }31313132 pwGroup.append(histToggle, histList);3133 }31343135 body.appendChild(pwGroup);31363137 const totpInput = field("2fa secret", entryRes.data.totp ?? "").inp;31383139 3140 const notesGroup = document.createElement("div");3141 notesGroup.className = "f-group";3142 const notesLbl = document.createElement("div");3143 notesLbl.className = "f-label";3144 notesLbl.textContent = "notes";3145 const notesInput = document.createElement("textarea");3146 notesInput.className = "f-input";3147 notesInput.rows = 3;3148 notesInput.value = entryRes.data.notes ?? "";3149 notesInput.style.fontFamily = "Inter, sans-serif";3150 notesInput.style.resize = "vertical";3151 notesGroup.append(notesLbl, notesInput);3152 body.appendChild(notesGroup);31533154 3155 const actionsRow = document.createElement("div");3156 actionsRow.className = "f-row";3157 const saveBtn = document.createElement("button");3158 saveBtn.className = "save-btn";3159 saveBtn.type = "button";3160 saveBtn.textContent = "Save changes";3161 saveBtn.style.flex = "1";3162 const cancelBtn = document.createElement("button");3163 cancelBtn.className = "gen-btn";3164 cancelBtn.type = "button";3165 cancelBtn.textContent = "cancel";3166 cancelBtn.addEventListener("click", () => renderSilverPlatter(summary));3167 actionsRow.append(saveBtn, cancelBtn);3168 body.appendChild(actionsRow);31693170 saveBtn.addEventListener("click", async () => {3171 saveBtn.disabled = true;3172 const originalLabel = saveBtn.textContent;3173 saveBtn.textContent = "Saving…";3174 const patch: Record<string, unknown> = {3175 title: titleInput.value.trim(),3176 url: urlInput.value.trim(),3177 username: usernameInput.value.trim(),3178 email: emailInput.value.trim() || undefined,3179 password: pwInput.value,3180 totp: totpInput.value.trim(),3181 notes: notesInput.value,3182 };3183 const updateRes = (await chrome.runtime.sendMessage({3184 type: "UPDATE_ENTRY",3185 payload: { id: summary.id, patch },3186 }).catch(() => null)) as { ok: boolean; error?: string } | null;3187 if (!updateRes?.ok) {3188 saveBtn.disabled = false;3189 saveBtn.textContent = originalLabel;3190 const err = document.createElement("div");3191 err.className = "empty-hint";3192 err.style.color = "color-mix(in srgb, #e55 70%, transparent)";3193 err.textContent = updateRes?.error ?? "Couldn't save. Try the popup.";3194 body.appendChild(err);3195 setTimeout(() => err.remove(), 3000);3196 return;3197 }3198 3199 const refreshed: SelectorEntry = {3200 id: summary.id,3201 title: patch.title as string,3202 username: patch.username as string,3203 email: patch.email as string | undefined,3204 url: patch.url as string,3205 hasTotp: Boolean(patch.totp),3206 hasPasskey: summary.hasPasskey,3207 };3208 renderSilverPlatter(refreshed);3209 });32103211 queueMicrotask(() => titleInput.focus());3212 }32133214 function renderLocked() {3215 container.innerHTML = "";32163217 3218 const domHdr = document.createElement("div");3219 domHdr.className = "domain-header";3220 const eyebrow = document.createElement("div");3221 eyebrow.className = "domain-eyebrow";3222 eyebrow.textContent = "vault locked";3223 const domName = document.createElement("div");3224 domName.className = "domain-name";3225 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");3226 domHdr.append(eyebrow, domName);3227 container.appendChild(domHdr);32283229 const wrap = document.createElement("div");3230 wrap.className = "locked-wrap";32313232 3233 const head = document.createElement("div"); head.className = "locked-head";3234 const icon = document.createElement("div"); icon.className = "locked-icon";3235 icon.innerHTML = keySvgStr();3236 const title = document.createElement("div"); title.className = "locked-title";3237 title.textContent = "unlock to autofill";3238 head.append(icon, title);32393240 3241 3242 3243 3244 3245 3246 3247 3248 const sub = document.createElement("div");3249 sub.className = "locked-sub";3250 sub.textContent = "Unlock in Peach — your master password is never typed on this page.";32513252 const openBtn = document.createElement("button");3253 openBtn.className = "locked-btn";3254 openBtn.type = "button";3255 openBtn.textContent = "Open Peach to unlock";3256 openBtn.addEventListener("click", () => {3257 chrome.runtime.sendMessage({ type: "OPEN_POPUP" }).catch(() => {});3258 openBtn.textContent = "opening Peach…";3259 openBtn.disabled = true;3260 3261 3262 setTimeout(() => {3263 if (openBtn.isConnected) {3264 openBtn.textContent = "Open Peach to unlock";3265 openBtn.disabled = false;3266 }3267 }, 2500);3268 });32693270 wrap.append(head, sub, openBtn);3271 container.appendChild(wrap);3272 }32733274 function handleKeyDown(e: KeyboardEvent) {3275 3276 3277 3278 if (!e.isTrusted) return;3279 if (options.locked) {3280 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3281 return;3282 }3283 if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter" || currentMode === "identity-helper" || currentMode === "edit") {3284 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }3285 return;3286 }3287 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");3288 const count = allEntryEls.length;3289 const activeElement = shadow.activeElement ?? document.activeElement;3290 if (activeElement === searchInput) {3291 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }3292 if (count === 0) return;3293 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;3294 }3295 if (count === 0) return;3296 switch (e.key) {3297 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;3298 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;3299 case "Enter": {3300 if (focusedIndex >= 0) {3301 const el = allEntryEls[focusedIndex] as HTMLElement;3302 const id = el.dataset.id;3303 if (id) { onSelect(id); dismiss(); }3304 else if (el.classList.contains("is-new") || !id) {3305 3306 showCreateForm();3307 }3308 }3309 break;3310 }3311 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;3312 }3313 }33143315 document.addEventListener("keydown", handleKeyDown);33163317 3318 3319 3320 3321 3322 const silverPlatterTypeHandler = (e: KeyboardEvent) => {3323 if (!e.isTrusted) return; 3324 if (currentMode !== "silver-platter") return;3325 if (e.ctrlKey || e.metaKey || e.altKey) return;3326 if (e.key.length !== 1) return;3327 e.preventDefault();3328 e.stopImmediatePropagation();3329 query = e.key;3330 3331 void ensureSearchableLoaded(renderList);3332 renderList();3333 };3334 document.addEventListener("keydown", silverPlatterTypeHandler, true);3335 cleanupHooks.push(() => document.removeEventListener("keydown", silverPlatterTypeHandler, true));33363337 const identityHelperTypeHandler = (e: KeyboardEvent) => {3338 if (!e.isTrusted) return; 3339 if (currentMode !== "identity-helper") return;3340 if (e.ctrlKey || e.metaKey || e.altKey) return;3341 const active = (shadow.activeElement ?? document.activeElement) as Element | null;3342 if (active === searchInput) return;3343 if (3344 active instanceof HTMLInputElement ||3345 active instanceof HTMLTextAreaElement ||3346 (active instanceof HTMLElement && active.isContentEditable)3347 ) {3348 return;3349 }3350 if (e.key.length !== 1) return;3351 e.preventDefault();3352 e.stopImmediatePropagation();3353 query = e.key;3354 renderIdentityHelper();3355 };3356 document.addEventListener("keydown", identityHelperTypeHandler, true);3357 cleanupHooks.push(() => document.removeEventListener("keydown", identityHelperTypeHandler, true));33583359 (modalAncestor ?? document.body).appendChild(host);33603361 3362 3363 3364 3365 3366 3367 3368 function establishesFixedContainingBlock(s: CSSStyleDeclaration): boolean {3369 const has = (p: string) => { const v = s.getPropertyValue(p); return v !== "" && v !== "none"; };3370 return (3371 has("transform") || has("perspective") || has("translate") ||3372 has("rotate") || has("scale") || has("filter") ||3373 has("backdrop-filter") || has("-webkit-backdrop-filter") ||3374 /\b(transform|perspective|filter)\b/.test(s.getPropertyValue("will-change")) ||3375 /\b(paint|layout|strict|content)\b/.test(s.getPropertyValue("contain"))3376 );3377 }3378 function containingBlockOffset(): { dx: number; dy: number } {3379 let el: HTMLElement | null = host.parentElement;3380 while (el && el !== document.documentElement) {3381 const s = getComputedStyle(el);3382 if (establishesFixedContainingBlock(s)) {3383 const r = el.getBoundingClientRect();3384 return {3385 dx: r.left + (parseFloat(s.borderLeftWidth) || 0),3386 dy: r.top + (parseFloat(s.borderTopWidth) || 0),3387 };3388 }3389 el = el.parentElement;3390 }3391 return { dx: 0, dy: 0 };3392 }33933394 function updatePosition() {3395 const r = anchorEl.getBoundingClientRect();3396 const { dx, dy } = containingBlockOffset();3397 host.style.top = `${r.bottom - dy + 5}px`;3398 host.style.left = `${r.left - dx}px`;3399 host.style.width = `${popupWidth}px`;3400 }3401 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);3402 updatePosition();34033404 3405 3406 3407 if (modalAncestor) {3408 modalAncestor.addEventListener("scroll", updatePosition, { passive: true });3409 cleanupHooks.push(() => modalAncestor.removeEventListener("scroll", updatePosition));3410 }34113412 3413 3414 3415 const removalObserver = new MutationObserver(() => {3416 if (!document.contains(anchorEl)) dismiss();3417 });3418 removalObserver.observe(document.body, { childList: true, subtree: true });3419 cleanupHooks.push(() => removalObserver.disconnect());34203421 if (options.locked) {3422 renderLocked();3423 } else if (options.isSignup && options.savedIdentities) {3424 3425 3426 renderIdentityHelper();3427 } else if (entries.length === 1 && !options.isSignup) {3428 3429 3430 renderSilverPlatter(entries[0]!);3431 } else {3432 renderList();3433 }34343435 let exitScheduled = false;3436 function dismiss(userAction = false) {3437 3438 3439 3440 if (exitScheduled) return;3441 exitScheduled = true;3442 3443 3444 3445 stopObserving();3446 document.removeEventListener("click", outsideClick, { capture: true });3447 document.removeEventListener("keydown", handleKeyDown);3448 for (const cleanup of cleanupHooks) {3449 try { cleanup(); } catch { }3450 }3451 if (userAction) options.onDismiss?.();3452 container.classList.add("is-exiting");3453 setTimeout(() => host.remove(), 130);3454 }34553456 function outsideClick(ev: MouseEvent) {3457 const path = ev.composedPath();3458 3459 3460 3461 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3462 }3463 3464 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);34653466 return dismiss;3467}34683469export function showTOTPSelector(3470 anchorEl: HTMLInputElement,3471 entries: SelectorEntry[],3472 onFill: (id: string) => void,3473 options: { domain?: string; onDismiss?: () => void } = {}3474): () => void {3475 const host = document.createElement("div");3476 host.style.cssText = "position: absolute; z-index: 2147483647;";3477 host.style.setProperty("--peach-accent", accentHex);3478 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));3479 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));3480 const shadow = host.attachShadow({ mode: "closed" });34813482 const style = shadow.appendChild(document.createElement("style"));3483 style.textContent = SELECTOR_STYLES;34843485 const rect = anchorEl.getBoundingClientRect();3486 host.style.top = `${rect.bottom + window.scrollY + 5}px`;3487 host.style.left = `${rect.left + window.scrollX}px`;3488 const popupWidth = Math.min(Math.max(rect.width, 300), 360);3489 host.style.width = `${popupWidth}px`;34903491 const container = shadow.appendChild(document.createElement("div"));3492 container.className = "peach-inline-popup";3493 container.setAttribute("data-theme", popupTheme);34943495 const domain = options.domain ?? location.hostname.replace(/^www\./, "");3496 const current = entries.filter((entry) => entry.hasTotp);3497 let focusedIndex = current.length === 1 ? 0 : -1;34983499 function updatePosition() {3500 const r = anchorEl.getBoundingClientRect();3501 host.style.top = `${r.bottom + window.scrollY + 5}px`;3502 host.style.left = `${r.left + window.scrollX}px`;3503 host.style.width = `${popupWidth}px`;3504 }35053506 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);35073508 function choose(entry: SelectorEntry) {3509 onFill(entry.id);3510 dismiss();3511 }35123513 function render() {3514 container.innerHTML = "";35153516 const domHdr = document.createElement("div");3517 domHdr.className = "domain-header";3518 const headText = document.createElement("div");3519 headText.className = "domain-header-text";3520 const eyebrow = document.createElement("div");3521 eyebrow.className = "domain-eyebrow";3522 eyebrow.textContent = "2FA code";3523 const domName = document.createElement("div");3524 domName.className = "domain-name";3525 domName.textContent = domain;3526 headText.append(eyebrow, domName);3527 domHdr.appendChild(headText);3528 container.appendChild(domHdr);35293530 if (current.length === 1) {3531 const entry = current[0]!;3532 const platter = document.createElement("div");3533 platter.className = "silver-platter";35343535 const title = document.createElement("div");3536 title.className = "sp-title";3537 title.textContent = entry.title;3538 platter.appendChild(title);35393540 const sub = document.createElement("div");3541 sub.className = "sp-username";3542 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3543 platter.appendChild(sub);35443545 const fill = document.createElement("button");3546 fill.className = "sp-fill-btn";3547 fill.type = "button";3548 fill.textContent = "Fill 2FA code";3549 fill.addEventListener("click", () => choose(entry));3550 platter.appendChild(fill);35513552 container.appendChild(platter);3553 requestAnimationFrame(() => fill.focus({ preventScroll: true }));3554 return;3555 }35563557 const results = document.createElement("div");3558 results.className = "results";3559 container.appendChild(results);35603561 for (let i = 0; i < current.length; i++) {3562 const entry = current[i]!;3563 const row = document.createElement("div");3564 row.className = `entry${i === focusedIndex ? " focused" : ""}`;3565 row.dataset.index = String(i);3566 row.setAttribute("tabindex", "-1");35673568 const icon = document.createElement("div");3569 icon.className = "entry-icon";3570 icon.innerHTML = keySvgStr();3571 row.appendChild(icon);3572 const info = document.createElement("div");3573 info.className = "entry-info";3574 const title = document.createElement("div");3575 title.className = "entry-title";3576 title.textContent = entry.title;3577 const sub = document.createElement("div");3578 sub.className = "entry-sub";3579 sub.textContent = entry.username ?? entry.email ?? entry.url ?? "";3580 info.append(title, sub);3581 row.appendChild(info);35823583 const badge = document.createElement("span");3584 badge.className = "totp-badge";3585 badge.textContent = "Fill";3586 row.appendChild(badge);3587 row.addEventListener("click", () => choose(entry));3588 results.appendChild(row);3589 }3590 }35913592 function setFocused(index: number) {3593 focusedIndex = index;3594 container.querySelectorAll(".entry").forEach((el, i) => {3595 el.classList.toggle("focused", i === focusedIndex);3596 if (i === focusedIndex) (el as HTMLElement).scrollIntoView({ block: "nearest" });3597 });3598 }35993600 function handleKeyDown(e: KeyboardEvent) {3601 if (e.key === "Escape" || e.key === "Tab") {3602 e.preventDefault();3603 dismiss(true);3604 return;3605 }3606 if (current.length === 0) return;3607 if (e.key === "Enter") {3608 e.preventDefault();3609 choose(current[Math.max(0, focusedIndex)]!);3610 return;3611 }3612 if (current.length === 1) return;3613 if (e.key === "ArrowDown") {3614 e.preventDefault();3615 setFocused(focusedIndex < current.length - 1 ? focusedIndex + 1 : 0);3616 } else if (e.key === "ArrowUp") {3617 e.preventDefault();3618 setFocused(focusedIndex > 0 ? focusedIndex - 1 : current.length - 1);3619 }3620 }36213622 let exitScheduled = false;3623 function dismiss(userAction = false) {3624 if (exitScheduled) return;3625 exitScheduled = true;3626 stopObserving();3627 document.removeEventListener("click", outsideClick, { capture: true });3628 document.removeEventListener("keydown", handleKeyDown);3629 if (userAction) options.onDismiss?.();3630 container.classList.add("is-exiting");3631 setTimeout(() => host.remove(), 130);3632 }36333634 function outsideClick(ev: MouseEvent) {3635 const path = ev.composedPath();3636 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);3637 }36383639 document.addEventListener("keydown", handleKeyDown);3640 document.body.appendChild(host);3641 render();3642 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);36433644 return dismiss;3645}36463647export interface CardSelectorEntry {3648 id: string;3649 title: string;3650 cardholderName?: string;3651 last4?: string;3652 expMonth?: string;3653 expYear?: string;3654 network?: import("../shared/types").CardNetwork;3655}36563657interface CardSelectorOptions {3658 domain?: string;3659 3660 onFill: (entry: import("../shared/types").VaultEntry) => void;3661 3662 onDismiss?: () => void;3663}36643665366636673668366936703671export function showPasskeyProGate(): void {3672 const domain = location.hostname.replace(/^www\./, "");36733674 const host = document.createElement("div");3675 host.setAttribute("data-peach-passkey-gate", "1");3676 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";3677 const shadow = host.attachShadow({ mode: "closed" });36783679 const style = document.createElement("style");3680 style.textContent = `3681 :host { all: initial; }3682 .gate-wrap {3683 position: fixed;3684 top: 50%;3685 left: 50%;3686 transform: translate(-50%, -50%);3687 width: 280px;3688 pointer-events: auto;3689 animation: g-in 220ms cubic-bezier(0.16,1,0.3,1) both;3690 }3691 @keyframes g-in {3692 from { opacity:0; transform: translate(-50%,-52%); }3693 to { opacity:1; transform: translate(-50%,-50%); }3694 }3695 .gate-wrap.is-exiting {3696 animation: g-out 130ms ease-in forwards;3697 }3698 @keyframes g-out {3699 to { opacity:0; transform: translate(-50%,-52%); }3700 }3701 .gate {3702 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3703 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3704 border-radius: 12px;3705 box-shadow: 0 12px 48px rgba(0,0,0,0.75);3706 padding: 16px;3707 font-family: Inter, sans-serif;3708 }3709 .gate-header {3710 display: flex;3711 align-items: center;3712 gap: 7px;3713 margin-bottom: 12px;3714 padding-bottom: 10px;3715 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3716 }3717 .gate-domain {3718 font-size: 12px;3719 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3720 font-family: "IBM Plex Mono", monospace;3721 overflow: hidden;3722 text-overflow: ellipsis;3723 white-space: nowrap;3724 }3725 .gate-title {3726 font-size: 14px;3727 font-weight: 500;3728 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3729 margin-bottom: 6px;3730 }3731 .gate-subtitle {3732 font-size: 12px;3733 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.45)"};3734 line-height: 1.5;3735 margin-bottom: 14px;3736 }3737 .gate-btn {3738 display: block;3739 width: 100%;3740 padding: 9px 0;3741 border-radius: 8px;3742 border: none;3743 cursor: pointer;3744 font-size: 13px;3745 font-family: Inter, sans-serif;3746 font-weight: 500;3747 transition: opacity 100ms;3748 margin-bottom: 7px;3749 text-align: center;3750 }3751 .gate-btn:last-child { margin-bottom: 0; }3752 .gate-btn--accent {3753 background: ${accentHex};3754 color: #fff;3755 }3756 .gate-btn--accent:hover { opacity: 0.88; }3757 .gate-btn--ghost {3758 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3759 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.45)"};3760 }3761 .gate-btn--ghost:hover { opacity: 0.75; }3762 .backdrop {3763 position: fixed;3764 inset: 0;3765 pointer-events: auto;3766 }3767 `;3768 shadow.appendChild(style);37693770 const backdrop = document.createElement("div");3771 backdrop.className = "backdrop";3772 shadow.appendChild(backdrop);37733774 const wrap = document.createElement("div");3775 wrap.className = "gate-wrap";3776 shadow.appendChild(wrap);37773778 const gate = document.createElement("div");3779 gate.className = "gate";3780 wrap.appendChild(gate);37813782 3783 const header = document.createElement("div");3784 header.className = "gate-header";3785 const icon = document.createElement("span");3786 icon.innerHTML = peachIconSvg();3787 const domainEl = document.createElement("span");3788 domainEl.className = "gate-domain";3789 domainEl.textContent = domain;3790 header.append(icon, domainEl);3791 gate.appendChild(header);37923793 const title = document.createElement("div");3794 title.className = "gate-title";3795 title.textContent = "Passkeys are part of Peach Pro.";3796 gate.appendChild(title);37973798 const subtitle = document.createElement("div");3799 subtitle.className = "gate-subtitle";3800 subtitle.textContent = "Store and use passkeys across all your devices, securely.";3801 gate.appendChild(subtitle);38023803 const upgradeBtn = document.createElement("button");3804 upgradeBtn.className = "gate-btn gate-btn--accent";3805 upgradeBtn.type = "button";3806 upgradeBtn.textContent = "Upgrade to Pro";3807 gate.appendChild(upgradeBtn);38083809 const laterBtn = document.createElement("button");3810 laterBtn.className = "gate-btn gate-btn--ghost";3811 laterBtn.type = "button";3812 laterBtn.textContent = "Not right now";3813 gate.appendChild(laterBtn);38143815 function dismiss() {3816 wrap.classList.add("is-exiting");3817 setTimeout(() => host.remove(), 130);3818 }38193820 upgradeBtn.addEventListener("click", () => {3821 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: { feature: "passkeys" } }).catch(() => {});3822 dismiss();3823 });3824 laterBtn.addEventListener("click", dismiss);3825 backdrop.addEventListener("click", dismiss);38263827 document.addEventListener("keydown", function onEsc(e: KeyboardEvent) {3828 if (e.key === "Escape") { dismiss(); document.removeEventListener("keydown", onEsc); }3829 });38303831 document.body.appendChild(host);3832}383338343835383638373838383938403841export function showPasskeyRegistrationOffer(args: {3842 rpId: string;3843 rpName?: string;3844 userName?: string;3845 userDisplayName?: string;3846}): void {3847 const domain = location.hostname.replace(/^www\./, "");3848 const displayName = args.rpName || domain;38493850 const host = document.createElement("div");3851 host.setAttribute("data-peach-passkey-offer", "1");3852 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483645;pointer-events:none;";3853 const shadow = host.attachShadow({ mode: "closed" });38543855 const style = document.createElement("style");3856 style.textContent = `3857 :host { all: initial; }3858 .offer-wrap {3859 position: fixed;3860 top: 16px;3861 right: 16px;3862 width: 320px;3863 pointer-events: auto;3864 animation: o-in 220ms cubic-bezier(0.16,1,0.3,1) both;3865 }3866 @keyframes o-in {3867 from { opacity: 0; transform: translateY(-6px); }3868 to { opacity: 1; transform: translateY(0); }3869 }3870 .offer-wrap.is-exiting { animation: o-out 130ms ease-in forwards; }3871 @keyframes o-out { to { opacity: 0; transform: translateY(-6px); } }3872 .offer {3873 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};3874 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};3875 border-radius: 12px;3876 box-shadow: 0 12px 48px rgba(0,0,0,0.55);3877 padding: 14px 14px 12px;3878 font-family: Inter, sans-serif;3879 position: relative;3880 }3881 .offer-close {3882 position: absolute;3883 top: 8px;3884 right: 8px;3885 width: 18px;3886 height: 18px;3887 border: none;3888 background: transparent;3889 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};3890 cursor: pointer;3891 font-size: 14px;3892 line-height: 1;3893 padding: 0;3894 }3895 .offer-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }3896 .offer-header {3897 display: flex;3898 align-items: center;3899 gap: 7px;3900 margin-bottom: 8px;3901 padding-bottom: 8px;3902 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};3903 }3904 .offer-domain {3905 font-size: 12px;3906 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};3907 font-family: "IBM Plex Mono", monospace;3908 overflow: hidden;3909 text-overflow: ellipsis;3910 white-space: nowrap;3911 }3912 .offer-title {3913 font-size: 13px;3914 font-weight: 500;3915 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};3916 margin-bottom: 4px;3917 }3918 .offer-subtitle {3919 font-size: 11px;3920 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};3921 line-height: 1.45;3922 margin-bottom: 12px;3923 }3924 .offer-btn {3925 display: block;3926 width: 100%;3927 padding: 8px 0;3928 border-radius: 7px;3929 border: none;3930 cursor: pointer;3931 font-size: 12px;3932 font-family: Inter, sans-serif;3933 font-weight: 500;3934 transition: opacity 100ms;3935 margin-bottom: 6px;3936 text-align: center;3937 }3938 .offer-btn:last-child { margin-bottom: 0; }3939 .offer-btn--accent { background: ${accentHex}; color: #fff; }3940 .offer-btn--accent:hover { opacity: 0.88; }3941 .offer-btn--ghost {3942 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};3943 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};3944 }3945 .offer-btn--ghost:hover { opacity: 0.75; }3946 `;3947 shadow.appendChild(style);39483949 const wrap = document.createElement("div");3950 wrap.className = "offer-wrap";3951 shadow.appendChild(wrap);39523953 const offer = document.createElement("div");3954 offer.className = "offer";3955 wrap.appendChild(offer);39563957 const closeBtn = document.createElement("button");3958 closeBtn.className = "offer-close";3959 closeBtn.type = "button";3960 closeBtn.setAttribute("aria-label", "Dismiss");3961 closeBtn.textContent = "✕";3962 offer.appendChild(closeBtn);39633964 const header = document.createElement("div");3965 header.className = "offer-header";3966 const icon = document.createElement("span");3967 icon.innerHTML = peachIconSvg();3968 const domainEl = document.createElement("span");3969 domainEl.className = "offer-domain";3970 domainEl.textContent = domain;3971 header.append(icon, domainEl);3972 offer.appendChild(header);39733974 const title = document.createElement("div");3975 title.className = "offer-title";3976 title.textContent = "This site supports passkeys.";3977 offer.appendChild(title);39783979 const subtitle = document.createElement("div");3980 subtitle.className = "offer-subtitle";3981 subtitle.textContent = "Save one to Peach for faster, passwordless sign-in.";3982 offer.appendChild(subtitle);39833984 const saveBtn = document.createElement("button");3985 saveBtn.className = "offer-btn offer-btn--accent";3986 saveBtn.type = "button";3987 saveBtn.textContent = "Save passkey to Peach";3988 offer.appendChild(saveBtn);39893990 const laterBtn = document.createElement("button");3991 laterBtn.className = "offer-btn offer-btn--ghost";3992 laterBtn.type = "button";3993 laterBtn.textContent = "Not now";3994 offer.appendChild(laterBtn);39953996 function dismiss(decline: boolean) {3997 if (decline) {3998 chrome.runtime.sendMessage({3999 type: "PASSKEY_OFFER_DECLINE",4000 payload: { domain },4001 }).catch(() => {});4002 }4003 wrap.classList.add("is-exiting");4004 setTimeout(() => host.remove(), 130);4005 }40064007 saveBtn.addEventListener("click", () => {4008 chrome.runtime.sendMessage({4009 type: "INITIATE_FIREFOX_PASSKEY_CREATE",4010 payload: {4011 rpId: args.rpId,4012 rpName: args.rpName ?? displayName,4013 userName: args.userName ?? "user",4014 userDisplayName: args.userDisplayName,4015 },4016 }).catch(() => {});4017 dismiss(false);4018 });4019 laterBtn.addEventListener("click", () => dismiss(true));4020 closeBtn.addEventListener("click", () => dismiss(true));40214022 document.body.appendChild(host);4023}4024402540264027402840294030403140324033export function showPasskeyAvailable(args: {4034 rpId: string;4035 userName: string;4036 fromBrowser?: "chrome" | "firefox";4037 currentBrowser?: "chrome" | "firefox" | "unknown";4038}): void {4039 const domain = location.hostname.replace(/^www\./, "");4040 const crossBrowser =4041 args.fromBrowser &&4042 args.currentBrowser &&4043 args.currentBrowser !== "unknown" &&4044 args.fromBrowser !== args.currentBrowser;40454046 const host = document.createElement("div");4047 host.setAttribute("data-peach-passkey-available", "1");4048 host.style.cssText = "all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483644;pointer-events:none;";4049 const shadow = host.attachShadow({ mode: "closed" });40504051 const style = document.createElement("style");4052 style.textContent = `4053 :host { all: initial; }4054 .avail-wrap {4055 position: fixed;4056 top: 16px;4057 right: 16px;4058 width: 320px;4059 pointer-events: auto;4060 animation: a-in 220ms cubic-bezier(0.16,1,0.3,1) both;4061 }4062 @keyframes a-in {4063 from { opacity: 0; transform: translateY(-6px); }4064 to { opacity: 1; transform: translateY(0); }4065 }4066 .avail-wrap.is-exiting { animation: a-out 130ms ease-in forwards; }4067 @keyframes a-out { to { opacity: 0; transform: translateY(-6px); } }4068 .avail {4069 background: ${popupTheme === "light" ? "#f8f5f0" : "#1a1712"};4070 border: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.10)" : "rgba(240,232,216,0.09)"};4071 border-radius: 12px;4072 box-shadow: 0 12px 48px rgba(0,0,0,0.55);4073 padding: 14px 14px 12px;4074 font-family: Inter, sans-serif;4075 position: relative;4076 }4077 .avail-close {4078 position: absolute;4079 top: 8px;4080 right: 8px;4081 width: 18px;4082 height: 18px;4083 border: none;4084 background: transparent;4085 color: ${popupTheme === "light" ? "rgba(0,0,0,0.4)" : "rgba(240,232,216,0.4)"};4086 cursor: pointer;4087 font-size: 14px;4088 line-height: 1;4089 padding: 0;4090 }4091 .avail-close:hover { color: ${popupTheme === "light" ? "rgba(0,0,0,0.7)" : "rgba(240,232,216,0.7)"}; }4092 .avail-header {4093 display: flex;4094 align-items: center;4095 gap: 7px;4096 margin-bottom: 8px;4097 padding-bottom: 8px;4098 border-bottom: 1px solid ${popupTheme === "light" ? "rgba(0,0,0,0.07)" : "rgba(240,232,216,0.07)"};4099 }4100 .avail-domain {4101 font-size: 12px;4102 color: ${popupTheme === "light" ? "rgba(0,0,0,0.45)" : "rgba(240,232,216,0.45)"};4103 font-family: "IBM Plex Mono", monospace;4104 }4105 .avail-label {4106 font-size: 9px;4107 letter-spacing: 0.16em;4108 text-transform: uppercase;4109 color: ${accentHex};4110 margin-bottom: 4px;4111 }4112 .avail-user {4113 font-size: 13px;4114 font-weight: 500;4115 color: ${popupTheme === "light" ? "#1a1712" : "rgba(240,232,216,0.92)"};4116 margin-bottom: 12px;4117 overflow: hidden;4118 text-overflow: ellipsis;4119 white-space: nowrap;4120 }4121 .avail-cross {4122 font-size: 11px;4123 color: ${popupTheme === "light" ? "rgba(0,0,0,0.50)" : "rgba(240,232,216,0.50)"};4124 line-height: 1.45;4125 margin-bottom: 12px;4126 padding: 8px;4127 background: ${popupTheme === "light" ? "rgba(0,0,0,0.04)" : "rgba(240,232,216,0.04)"};4128 border-radius: 6px;4129 }4130 .avail-btn {4131 display: block;4132 width: 100%;4133 padding: 8px 0;4134 border-radius: 7px;4135 border: none;4136 cursor: pointer;4137 font-size: 12px;4138 font-family: Inter, sans-serif;4139 font-weight: 500;4140 transition: opacity 100ms;4141 margin-bottom: 6px;4142 text-align: center;4143 }4144 .avail-btn:last-child { margin-bottom: 0; }4145 .avail-btn--accent { background: ${accentHex}; color: #fff; }4146 .avail-btn--accent:hover { opacity: 0.88; }4147 .avail-btn--ghost {4148 background: ${popupTheme === "light" ? "rgba(0,0,0,0.06)" : "rgba(240,232,216,0.07)"};4149 color: ${popupTheme === "light" ? "rgba(0,0,0,0.55)" : "rgba(240,232,216,0.50)"};4150 }4151 .avail-btn--ghost:hover { opacity: 0.75; }4152 `;4153 shadow.appendChild(style);41544155 const wrap = document.createElement("div");4156 wrap.className = "avail-wrap";4157 shadow.appendChild(wrap);41584159 const avail = document.createElement("div");4160 avail.className = "avail";4161 wrap.appendChild(avail);41624163 const closeBtn = document.createElement("button");4164 closeBtn.className = "avail-close";4165 closeBtn.type = "button";4166 closeBtn.setAttribute("aria-label", "Dismiss");4167 closeBtn.textContent = "✕";4168 avail.appendChild(closeBtn);41694170 const header = document.createElement("div");4171 header.className = "avail-header";4172 const icon = document.createElement("span");4173 icon.innerHTML = peachIconSvg();4174 const domainEl = document.createElement("span");4175 domainEl.className = "avail-domain";4176 domainEl.textContent = domain;4177 header.append(icon, domainEl);4178 avail.appendChild(header);41794180 const label = document.createElement("div");4181 label.className = "avail-label";4182 label.textContent = "PASSKEY";4183 avail.appendChild(label);41844185 const user = document.createElement("div");4186 user.className = "avail-user";4187 user.textContent = args.userName;4188 avail.appendChild(user);41894190 if (crossBrowser) {4191 const cross = document.createElement("div");4192 cross.className = "avail-cross";4193 cross.textContent =4194 args.fromBrowser === "chrome"4195 ? "This passkey was created on Chrome. To use it on Firefox, re-register it on this device."4196 : "This passkey was created on Firefox. To use it on Chrome, re-register it on this device.";4197 avail.appendChild(cross);4198 }41994200 const signInBtn = document.createElement("button");4201 signInBtn.className = "avail-btn avail-btn--accent";4202 signInBtn.type = "button";4203 signInBtn.textContent = "Sign in with passkey";4204 avail.appendChild(signInBtn);42054206 const passwordBtn = document.createElement("button");4207 passwordBtn.className = "avail-btn avail-btn--ghost";4208 passwordBtn.type = "button";4209 passwordBtn.textContent = "Use password instead";4210 avail.appendChild(passwordBtn);42114212 function dismiss() {4213 wrap.classList.add("is-exiting");4214 setTimeout(() => host.remove(), 130);4215 }42164217 signInBtn.addEventListener("click", () => {4218 4219 4220 4221 4222 4223 dismiss();4224 chrome.runtime.sendMessage({ type: "OPEN_PRO_GATE", payload: {} }).catch(() => {});4225 });4226 passwordBtn.addEventListener("click", dismiss);4227 closeBtn.addEventListener("click", dismiss);42284229 document.body.appendChild(host);4230}423142324233423442354236423742384239export function showCardSelector(4240 anchorEl: HTMLInputElement,4241 cards: CardSelectorEntry[],4242 options: CardSelectorOptions4243): () => void {4244 if (cards.length === 0) return () => {};4245 const card = cards[0]!;42464247 const host = document.createElement("div");4248 host.style.cssText = "position: absolute; z-index: 2147483647;";4249 host.style.setProperty("--peach-accent", accentHex);4250 host.style.setProperty("--peach-glow", withAlpha(accentHex, "18"));4251 host.style.setProperty("--peach-border-accent", withAlpha(accentHex, "30"));4252 const shadow = host.attachShadow({ mode: "closed" });42534254 const style = shadow.appendChild(document.createElement("style"));4255 style.textContent = SELECTOR_STYLES;42564257 const rect = anchorEl.getBoundingClientRect();4258 host.style.top = `${rect.bottom + window.scrollY + 5}px`;4259 host.style.left = `${rect.left + window.scrollX}px`;4260 host.style.width = `${Math.min(Math.max(rect.width, 320), 400)}px`;42614262 const container = shadow.appendChild(document.createElement("div"));4263 container.className = "peach-inline-popup";4264 container.setAttribute("data-theme", popupTheme);42654266 4267 const domHdr = document.createElement("div");4268 domHdr.className = "domain-header";4269 const eyebrow = document.createElement("div");4270 eyebrow.className = "domain-eyebrow";4271 eyebrow.textContent = "Pay with Peach";4272 const domName = document.createElement("div");4273 domName.className = "domain-name";4274 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");4275 domHdr.append(eyebrow, domName);4276 container.appendChild(domHdr);42774278 const platter = document.createElement("div");4279 platter.className = "card-platter";42804281 if (card.network && card.network !== "unknown") {4282 const net = document.createElement("div");4283 net.className = "cp-network";4284 net.textContent = card.network;4285 platter.appendChild(net);4286 }42874288 const nick = document.createElement("div");4289 nick.className = "cp-nickname";4290 nick.textContent = card.title;4291 platter.appendChild(nick);42924293 if (card.last4) {4294 const l4 = document.createElement("div");4295 l4.className = "cp-last4";4296 l4.textContent = `•••• ${card.last4}`;4297 platter.appendChild(l4);4298 }42994300 const meta = document.createElement("div");4301 meta.className = "cp-meta";4302 if (card.cardholderName) {4303 const name = document.createElement("span");4304 name.textContent = card.cardholderName;4305 meta.appendChild(name);4306 }4307 if (card.expMonth && card.expYear) {4308 if (card.cardholderName) {4309 const sep = document.createElement("span");4310 sep.className = "cp-meta-sep";4311 sep.textContent = "·";4312 meta.appendChild(sep);4313 }4314 const exp = document.createElement("span");4315 const yy = card.expYear.length === 4 ? card.expYear.slice(-2) : card.expYear;4316 exp.textContent = `${card.expMonth.padStart(2, "0")}/${yy}`;4317 meta.appendChild(exp);4318 }4319 if (meta.childElementCount > 0) platter.appendChild(meta);43204321 const fillWrap = document.createElement("div");4322 fillWrap.className = "cp-fill-wrap";4323 const fillBtn = document.createElement("button");4324 fillBtn.className = "sp-fill-btn";4325 fillBtn.type = "button";4326 fillBtn.textContent = "Fill card details";4327 fillBtn.addEventListener("click", async () => {4328 if (fillBtn.disabled) return;4329 fillBtn.disabled = true;4330 fillBtn.classList.add("filled");4331 fillBtn.innerHTML = checkSvgStr();4332 const resp = await chrome.runtime.sendMessage({4333 type: "GET_ENTRY_FOR_FILL",4334 4335 payload: { id: card.id, explicitSummon: true },4336 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4337 if (resp?.ok && resp.data) options.onFill(resp.data);4338 setTimeout(() => dismiss(), 400);4339 });4340 fillWrap.appendChild(fillBtn);4341 platter.appendChild(fillWrap);43424343 4344 const actions = document.createElement("div");4345 actions.className = "sp-actions";43464347 function flashBtn(b: HTMLButtonElement) {4348 b.classList.add("flashed");4349 setTimeout(() => b.classList.remove("flashed"), 700);4350 }43514352 4353 function flashCopyError(b: HTMLButtonElement) {4354 const original = b.textContent ?? "";4355 b.textContent = "Blocked";4356 b.classList.add("flashed");4357 setTimeout(() => {4358 b.classList.remove("flashed");4359 if (b.isConnected) b.textContent = original;4360 }, 1200);4361 }43624363 async function fetchFullCard(): Promise<import("../shared/types").VaultEntry | null> {4364 const resp = await chrome.runtime.sendMessage({4365 type: "GET_ENTRY_FOR_FILL",4366 4367 payload: { id: card.id, explicitSummon: true },4368 }).catch(() => null) as { ok: boolean; data?: import("../shared/types").VaultEntry } | null;4369 return resp?.ok && resp.data ? resp.data : null;4370 }43714372 const copyNum = document.createElement("button");4373 copyNum.className = "sp-action-btn";4374 copyNum.type = "button";4375 copyNum.title = "Copy card number";4376 copyNum.textContent = "Copy number";4377 copyNum.addEventListener("click", async (e) => {4378 e.stopPropagation();4379 const full = await fetchFullCard();4380 if (!full?.cardNumber) return;4381 let alreadyWritten = false;4382 try {4383 await navigator.clipboard.writeText(full.cardNumber);4384 alreadyWritten = true;4385 } catch (err) {4386 console.warn("[Peach] clipboard write denied:", err);4387 }4388 const resp = await chrome.runtime.sendMessage({4389 type: "COPY",4390 payload: { text: full.cardNumber, label: "Card number", alreadyWritten },4391 }).catch(() => null) as { ok?: boolean } | null;4392 if (alreadyWritten || resp?.ok) flashBtn(copyNum);4393 else flashCopyError(copyNum);4394 });4395 actions.appendChild(copyNum);43964397 const copyCvc = document.createElement("button");4398 copyCvc.className = "sp-action-btn";4399 copyCvc.type = "button";4400 copyCvc.title = "Copy CVC";4401 copyCvc.textContent = "Copy CVC";4402 copyCvc.addEventListener("click", async (e) => {4403 e.stopPropagation();4404 const full = await fetchFullCard();4405 if (!full?.cvc) return;4406 let alreadyWritten = false;4407 try {4408 await navigator.clipboard.writeText(full.cvc);4409 alreadyWritten = true;4410 } catch (err) {4411 console.warn("[Peach] clipboard write denied:", err);4412 }4413 const resp = await chrome.runtime.sendMessage({4414 type: "COPY",4415 payload: { text: full.cvc, label: "CVC", alreadyWritten },4416 }).catch(() => null) as { ok?: boolean } | null;4417 if (alreadyWritten || resp?.ok) flashBtn(copyCvc);4418 else flashCopyError(copyCvc);4419 });4420 actions.appendChild(copyCvc);44214422 const closeBtn = document.createElement("button");4423 closeBtn.className = "sp-action-btn";4424 closeBtn.type = "button";4425 closeBtn.title = "Dismiss";4426 closeBtn.textContent = "✕";4427 closeBtn.addEventListener("click", (e) => {4428 e.stopPropagation();4429 dismiss(true);4430 });4431 actions.appendChild(closeBtn);44324433 platter.appendChild(actions);4434 container.appendChild(platter);44354436 document.body.appendChild(host);4437 function updatePosition() {4438 const r = anchorEl.getBoundingClientRect();4439 host.style.top = `${r.bottom + window.scrollY + 5}px`;4440 host.style.left = `${r.left + window.scrollX}px`;4441 }4442 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);44434444 function onKeyDown(e: KeyboardEvent) {4445 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }4446 }4447 document.addEventListener("keydown", onKeyDown);44484449 let exitScheduled = false;4450 function dismiss(userAction = false) {4451 if (exitScheduled) return;4452 exitScheduled = true;4453 stopObserving();4454 document.removeEventListener("click", outsideClick, { capture: true });4455 document.removeEventListener("keydown", onKeyDown);4456 if (userAction) options.onDismiss?.();4457 container.classList.add("is-exiting");4458 setTimeout(() => host.remove(), 130);4459 }44604461 function outsideClick(ev: MouseEvent) {4462 const path = ev.composedPath();4463 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);4464 }4465 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);44664467 return dismiss;4468}