123456789101112import { ACCENT_COLORS } from "../shared/types";1314let accentHex = "#E8956D";15chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string } } | undefined) => {16 const name = resp?.data?.accentColor ?? "peach";17 accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";18});1920export interface SelectorEntry {21 id: string;22 title: string;23 username?: string;24 url?: string;25 hasTotp?: boolean;26}2728interface SelectorOptions {29 domain?: string;30 searchableEntries?: SelectorEntry[];31 32 isSignup?: boolean;33 3435363738 onFillFields?: (username: string, password: string) => void;39 4041424344 locked?: boolean;45 46474849 onUnlocked?: () => void;50 5152535455 onDismiss?: () => void;56}5758function getDomainFromUrl(url: string): string {59 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }60}6162function keySvgStr(): string {63 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>`;64}6566function plusSvgStr(): string {67 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>`;68}6970function peachIconSvg(): string {71 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 64 64" fill="none" aria-hidden="true"><path d="M32 54C16 54 8 44 8 31C8 18 17 10 27 10C28 7 30 5.5 32 5.5C34 5.5 36 7 37 10C47 10 56 18 56 31C56 44 48 54 32 54Z" fill="${accentHex}"/><path d="M37 8.5C40.5 3.5 48 5 46 10.5C42 11 37 8.5 37 8.5Z" fill="#7B9060"/></svg>`;72}7374function genSvgStr(): string {75 return `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round"><path d="M9.5 2A4.5 4.5 0 1 0 10.5 5.5"/><path d="M8.5 2H9.5V1"/></svg>`;76}7778function checkSvgStr(): string {79 return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="#7B9060" stroke-width="1.5" stroke-linecap="round"><path d="M4 10.5L8 14.5L16 6.5"/></svg>`;80}8182function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {83 const { hostname, origin } = location;84 const parts = hostname.split(".");85 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",86 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",87 "oidc", "federation", "saml", "cas", "ldap"];88 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());89 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;90 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);91 return { origin, title, loginSubdomain };92}93949596979899export function showDomainConfirmToast(100 pageDomain: string,101 entryId: string,102): void {103 const host = document.createElement("div");104 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";105 host.style.setProperty("--peach-accent", accentHex);106 const shadow = host.attachShadow({ mode: "closed" });107108 const style = document.createElement("style");109 style.textContent = `110 .bar {111 display:flex;align-items:center;gap:10px;padding:10px 14px;112 background:#131109;border:1px solid rgba(240,232,216,0.09);113 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);114 font-family:Inter,sans-serif;white-space:nowrap;115 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;116 }117 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }118 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }119 .yes {120 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;121 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;122 }123 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }124 .no {125 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;126 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;127 }128 `;129 shadow.appendChild(style);130131 const bar = document.createElement("div"); bar.className = "bar";132 const txt = document.createElement("span"); txt.className = "txt";133 txt.textContent = `Set ${pageDomain} as default for this entry?`;134 const yesBtn = document.createElement("button"); yesBtn.className = "yes";135 yesBtn.textContent = "Set default";136 yesBtn.addEventListener("click", async () => {137 await chrome.runtime.sendMessage({138 type: "UPDATE_ENTRY",139 payload: { id: entryId, patch: { url: location.origin } },140 });141 host.remove();142 });143 const noBtn = document.createElement("button"); noBtn.className = "no";144 noBtn.textContent = "Skip";145 noBtn.addEventListener("click", () => host.remove());146 bar.append(txt, yesBtn, noBtn);147 shadow.appendChild(bar);148 document.body.appendChild(host);149 setTimeout(() => host.remove(), 9000);150}151152function generatePassword(): string {153 const u = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", l = "abcdefghijklmnopqrstuvwxyz";154 const n = "0123456789", s = "!@#$%^&*";155 const all = u + l + n + s;156 const arr = new Uint8Array(20);157 crypto.getRandomValues(arr);158 const res = [u[arr[0]! % u.length]!, l[arr[1]! % l.length]!, n[arr[2]! % n.length]!, s[arr[3]! % s.length]!,159 ...Array.from(arr.slice(4), b => all[b % all.length]!)];160 for (let i = res.length - 1; i > 0; i--) {161 const j = arr[i % arr.length]! % (i + 1);162 [res[i], res[j]] = [res[j]!, res[i]!];163 }164 return res.join("");165}166167export function observeAnchorPosition(168 anchorEl: HTMLElement,169 onUpdate: () => void170): () => void {171 let rafId = 0;172 let burstUntil = 0;173174 const runUpdate = () => {175 rafId = 0;176 onUpdate();177 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);178 };179 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };180 const scheduleBurst = (ms = 220) => {181 burstUntil = Math.max(burstUntil, performance.now() + ms);182 scheduleUpdate();183 };184185 const resizeObserver = new ResizeObserver(() => scheduleBurst());186 resizeObserver.observe(anchorEl);187 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);188189 const mutationObserver = new MutationObserver(() => scheduleBurst());190 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });191 if (anchorEl.parentElement) {192 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });193 }194195 const onScroll = () => scheduleUpdate();196 window.addEventListener("scroll", onScroll, true);197 window.addEventListener("resize", onScroll);198 window.visualViewport?.addEventListener("resize", onScroll);199 window.visualViewport?.addEventListener("scroll", onScroll);200 anchorEl.addEventListener("focus", () => scheduleBurst());201 anchorEl.addEventListener("input", () => scheduleBurst());202203 scheduleBurst(260);204205 return () => {206 if (rafId) cancelAnimationFrame(rafId);207 resizeObserver.disconnect();208 mutationObserver.disconnect();209 window.removeEventListener("scroll", onScroll, true);210 window.removeEventListener("resize", onScroll);211 window.visualViewport?.removeEventListener("resize", onScroll);212 window.visualViewport?.removeEventListener("scroll", onScroll);213 };214}215216export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {217 const button = document.createElement("button");218 button.type = "button";219 button.setAttribute("aria-label", "Peach autofill");220 button.title = "Fill with Peach";221 button.innerHTML = peachIconSvg();222 223 224 225 226 button.style.cssText = `227 position: fixed; z-index: 2147483646;228 width: 26px; height: 26px; cursor: pointer;229 pointer-events: auto; border: none; padding: 3px;230 display: inline-flex; align-items: center; justify-content: center;231 background: transparent; border-radius: 999px;232 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;233 opacity: 0;234 `;235 button.addEventListener("mouseenter", () => {236 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;237 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;238 button.style.transform = "scale(1.08)";239 });240 button.addEventListener("mouseleave", () => {241 button.style.background = "transparent";242 button.style.boxShadow = "none";243 button.style.transform = "none";244 });245 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });246 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });247248 document.body.appendChild(button);249250 function updatePosition() {251 const rect = field.getBoundingClientRect();252 if (rect.width === 0 || rect.height === 0) {253 button.style.opacity = "0"; button.style.pointerEvents = "none";254 return;255 }256 const sz = 26;257 const inset = Math.max(5, Math.min(10, rect.height * 0.22));258 const top = rect.top + rect.height / 2 - sz / 2;259 260 261 const minLeft = rect.left + 4;262 const left = Math.max(minLeft, rect.right - sz - inset);263 264 if (rect.width < 40) {265 button.style.opacity = "0"; button.style.pointerEvents = "none";266 return;267 }268 button.style.top = `${top}px`;269 button.style.left = `${left}px`;270 button.style.opacity = "1"; button.style.pointerEvents = "auto";271 }272 updatePosition();273 requestAnimationFrame(() => { button.style.opacity = "1"; });274 const stopObserving = observeAnchorPosition(field, updatePosition);275 return () => { stopObserving(); button.remove(); };276}277278279const SELECTOR_STYLES = `280 :host { all: initial; }281 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }282 .selector {283 background: #0f0e0b;284 border: 1px solid rgba(240,232,216,0.10);285 border-radius: 13px;286 overflow: hidden;287 box-shadow: 0 4px 16px rgba(0,0,0,0.45), 0 16px 48px rgba(0,0,0,0.7);288 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;289 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;290 }291 @keyframes sl-in {292 from { opacity:0; transform:translateY(-6px) scale(0.97) }293 to { opacity:1; transform:none }294 }295296 /* Domain header */297 .domain-header {298 padding: 9px 13px 7px;299 border-bottom: 1px solid rgba(240,232,216,0.055);300 display: flex; align-items: center; justify-content: space-between;301 }302 .domain-eyebrow {303 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;304 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;305 }306 .domain-name {307 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);308 font-family: "IBM Plex Mono", monospace;309 }310311 /* Search */312 .search-wrap {313 padding: 9px 12px 7px;314 border-bottom: 1px solid rgba(240,232,216,0.04);315 }316 .search {317 width: 100%; background: rgba(240,232,216,0.03);318 border: 1px solid rgba(240,232,216,0.08);319 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;320 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;321 transition: border-color 120ms, box-shadow 120ms;322 }323 .search:focus {324 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);325 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);326 }327 .search::placeholder { color: rgba(240,232,216,0.2); }328329 /* Results */330 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }331 .results::-webkit-scrollbar { display: none; }332333 /* Entry rows */334 .entry {335 display: flex; align-items: center; padding: 8px 12px;336 cursor: pointer; border-bottom: 1px solid rgba(240,232,216,0.04);337 transition: background 70ms; position: relative; outline: none;338 }339 .entry:last-child { border-bottom: none; }340 .entry:hover { background: rgba(240,232,216,0.04); }341 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }342 .entry.focused::before {343 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;344 width: 2px; border-radius: 2px; background: color-mix(in srgb, var(--peach-accent) 55%, transparent);345 }346347 .entry-icon {348 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;349 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.07);350 display: flex; align-items: center; justify-content: center;351 overflow: hidden; margin-right: 10px;352 }353 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }354 .entry-icon.is-new {355 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);356 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);357 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);358 }359360 .entry-info { flex: 1; min-width: 0; }361 .entry-title {362 font-size: 12.5px; color: #F0E8D8;363 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;364 line-height: 1.3;365 }366 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }367 .entry-sub {368 font-size: 10px; color: rgba(240,232,216,0.32);369 font-family: "IBM Plex Mono", monospace;370 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;371 margin-top: 1px;372 }373374 .totp-badge {375 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;376 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);377 color: color-mix(in srgb, var(--peach-accent) 75%, transparent); font-family: "IBM Plex Mono", monospace;378 flex-shrink: 0; margin-left: 8px;379 }380381 .divider { height: 1px; background: rgba(240,232,216,0.05); margin: 2px 0; }382383 .empty-hint {384 padding: 14px 13px; font-size: 11px; color: rgba(240,232,216,0.28);385 font-family: "IBM Plex Mono", monospace;386 }387388 /* ── Create form ─────────────────────────────────────── */389 .create-form-wrap {390 animation: cf-in 180ms cubic-bezier(0.16,1,0.3,1) both;391 }392 @keyframes cf-in {393 from { opacity:0; transform:translateX(8px) }394 to { opacity:1; transform:none }395 }396 .create-head {397 display: flex; align-items: center; gap: 8px;398 padding: 10px 12px; border-bottom: 1px solid rgba(240,232,216,0.055);399 }400 .back-btn {401 background: transparent; border: none; cursor: pointer;402 color: rgba(240,232,216,0.38); padding: 3px; border-radius: 5px;403 display: flex; align-items: center; line-height: 0;404 transition: color 100ms, background 100ms;405 }406 .back-btn:hover { color: rgba(240,232,216,0.75); background: rgba(240,232,216,0.05); }407 .create-label { font-size: 12px; color: rgba(240,232,216,0.65); font-weight: 500; flex: 1; }408 .create-body { padding: 12px; display: grid; gap: 9px; }409 .f-group { display: grid; gap: 3px; }410 .f-label {411 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;412 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;413 }414 .f-input {415 background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);416 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;417 font-size: 11px; font-family: "IBM Plex Mono", monospace;418 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;419 }420 .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); }421 .f-input::placeholder { color: rgba(240,232,216,0.18); }422 .f-input.url-f { font-size: 10px; color: rgba(240,232,216,0.5); letter-spacing: 0.01em; }423 .f-row { display: flex; gap: 6px; }424 .f-row .f-input { flex: 1; min-width: 0; }425 .gen-btn {426 padding: 0 10px; border-radius: 7px; border: none;427 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);428 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);429 font-size: 10px; font-family: "IBM Plex Mono", monospace;430 cursor: pointer; white-space: nowrap; flex-shrink: 0;431 display: flex; align-items: center; gap: 4px;432 transition: background 100ms;433 }434 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }435 .save-btn {436 width: 100%; padding: 8px; border-radius: 8px; border: none;437 background: var(--peach-accent); color: #fff; font-size: 12px;438 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;439 transition: background 120ms; letter-spacing: 0.01em;440 }441 .save-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }442 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }443 .url-hint {444 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);445 font-family: "IBM Plex Mono", monospace; font-style: italic;446 }447448 /* ── Success ─────────────────────────────────────────── */449 .success-wrap {450 display: flex; flex-direction: column; align-items: center;451 padding: 24px 16px 20px; gap: 8px;452 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;453 }454 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }455 .success-ring {456 width: 40px; height: 40px; border-radius: 50%;457 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);458 display: flex; align-items: center; justify-content: center;459 }460 .success-text { font-size: 12.5px; color: #F0E8D8; font-weight: 500; }461 .success-sub { font-size: 10px; color: rgba(240,232,216,0.3); font-family:"IBM Plex Mono",monospace; }462463 /* ── Locked state ────────────────────────────────────── */464 .locked-wrap {465 display: flex; flex-direction: column; align-items: stretch;466 padding: 14px 13px 13px; gap: 9px;467 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;468 }469 .locked-head {470 display: flex; align-items: center; gap: 8px; padding-bottom: 2px;471 }472 .locked-icon {473 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;474 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.08);475 display: flex; align-items: center; justify-content: center;476 }477 .locked-title {478 font-size: 11.5px; color: rgba(240,232,216,0.55); font-weight: 500;479 }480 .locked-input {481 width: 100%; background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);482 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;483 font-size: 11px; font-family: "IBM Plex Mono", monospace;484 outline: none; transition: border-color 120ms, box-shadow 120ms;485 }486 .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); }487 .locked-input::placeholder { color: rgba(240,232,216,0.2); }488 .locked-btn {489 width: 100%; padding: 8px; border-radius: 8px; border: none;490 background: var(--peach-accent); color: #fff; font-size: 12px;491 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;492 transition: background 120ms; letter-spacing: 0.01em;493 }494 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }495 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; }496 .locked-error {497 font-size: 10px; color: rgba(220,80,60,0.85);498 font-family: "IBM Plex Mono", monospace;499 }500`;501502export function showAutofillSelector(503 anchorEl: HTMLInputElement,504 entries: SelectorEntry[],505 onSelect: (id: string) => void,506 options: SelectorOptions = {}507): () => void {508 const host = document.createElement("div");509 host.style.cssText = "position: absolute; z-index: 2147483647;";510 host.style.setProperty("--peach-accent", accentHex);511 const shadow = host.attachShadow({ mode: "closed" });512513 const style = shadow.appendChild(document.createElement("style"));514 style.textContent = SELECTOR_STYLES;515516 const rect = anchorEl.getBoundingClientRect();517 host.style.top = `${rect.bottom + window.scrollY + 5}px`;518 host.style.left = `${rect.left + window.scrollX}px`;519 host.style.width = `${Math.max(rect.width, 260)}px`;520521 const container = shadow.appendChild(document.createElement("div"));522 container.className = "selector";523524 const searchableEntries = options.searchableEntries ?? [];525 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();526527 let query = "";528 let focusedIndex = -1;529 let currentMode: "list" | "create" | "success" = "list";530 let searchInput: HTMLInputElement | null = null;531532 533 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";534 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");535 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";536537 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {538 const seen = new Set<string>();539 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });540 }541542 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {543 const q = value.trim().toLowerCase();544 const deduped = uniqueEntries(source);545 if (!q) return deduped.slice(0, 8);546 return deduped.filter(e =>547 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)548 ).slice(0, 12);549 }550551 function activeEntries(): SelectorEntry[] {552 553 if (query.trim()) {554 const merged = uniqueEntries([...entries, ...searchableEntries]);555 return filterEntries(merged, query);556 }557 558 if (entries.length > 0) return entries;559 return filterEntries(searchableEntries, "").slice(0, 8);560 }561562 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {563 const icon = document.createElement("div");564 icon.className = `entry-icon${isNew ? " is-new" : ""}`;565 if (isNew) {566 icon.innerHTML = plusSvgStr();567 } else {568 569 570 571 void url;572 icon.innerHTML = keySvgStr();573 }574 return icon;575 }576577 function renderList() {578 container.innerHTML = "";579 focusedIndex = -1;580 searchInput = null;581582 const current = activeEntries();583584 585 const domHdr = document.createElement("div");586 domHdr.className = "domain-header";587 const eyebrow = document.createElement("div");588 eyebrow.className = "domain-eyebrow";589 eyebrow.textContent = options.isSignup590 ? "Sign up as"591 : entries.length > 0 ? "Saved for this site" : "No saved login";592 const domName = document.createElement("div");593 domName.className = "domain-name";594 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");595 domHdr.append(eyebrow, domName);596 container.appendChild(domHdr);597598 599 const sw = document.createElement("div"); sw.className = "search-wrap";600 searchInput = document.createElement("input");601 searchInput.className = "search";602 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";603 searchInput.value = query;604 searchInput.addEventListener("input", () => {605 query = searchInput?.value ?? "";606 renderList();607 searchInput?.focus();608 searchInput?.setSelectionRange(query.length, query.length);609 });610 sw.appendChild(searchInput);611 container.appendChild(sw);612613 614 const results = document.createElement("div");615 results.className = "results";616 container.appendChild(results);617618 if (current.length === 0 && entries.length > 0) {619 620 }621622 for (let i = 0; i < current.length; i++) {623 const e = current[i]!;624 const entryEl = document.createElement("div");625 entryEl.className = "entry";626 entryEl.dataset.id = e.id;627 entryEl.dataset.index = String(i);628 entryEl.setAttribute("tabindex", "-1");629630 entryEl.appendChild(makeEntryIcon(e.url));631632 const info = document.createElement("div"); info.className = "entry-info";633 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;634 const subEl = document.createElement("div"); subEl.className = "entry-sub";635 const domain = e.url ? getDomainFromUrl(e.url) : "";636 subEl.textContent = domain || e.username || "";637 info.append(titleEl, subEl);638 entryEl.appendChild(info);639640 if (e.hasTotp) {641 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";642 entryEl.appendChild(badge);643 }644645 entryEl.addEventListener("click", () => {646 if (options.isSignup && options.onFillFields) {647 648 options.onFillFields(e.username ?? "", "");649 dismiss();650 } else {651 onSelect(e.id);652 dismiss();653 }654 });655 results.appendChild(entryEl);656 }657658 659 if (current.length > 0) {660 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));661 }662663 664 const newEl = document.createElement("div");665 newEl.className = "entry";666 newEl.setAttribute("tabindex", "-1");667 newEl.appendChild(makeEntryIcon(undefined, true));668669 const newInfo = document.createElement("div"); newInfo.className = "entry-info";670 const newTitle = document.createElement("div");671 newTitle.className = "entry-title is-new";672 newTitle.textContent = `New entry for ${sTitle}`;673 const newSub = document.createElement("div");674 newSub.className = "entry-sub";675 newSub.textContent = sOrigin;676 newInfo.append(newTitle, newSub);677 newEl.appendChild(newInfo);678 newEl.addEventListener("click", showCreateForm);679 results.appendChild(newEl);680681 682 if (searchInput) {683 requestAnimationFrame(() => searchInput?.focus());684 }685 }686687 function setFocused(idx: number) {688 const all = container.querySelectorAll<HTMLElement>(".entry");689 all.forEach((el, i) => el.classList.toggle("focused", i === idx));690 focusedIndex = idx;691 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });692 }693694 function showCreateForm() {695 currentMode = "create";696 container.innerHTML = "";697698 const wrap = document.createElement("div");699 wrap.className = "create-form-wrap";700701 702 const head = document.createElement("div"); head.className = "create-head";703 const backBtn = document.createElement("button"); backBtn.className = "back-btn";704 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>`;705 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });706 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";707 head.append(backBtn, lbl);708709 710 const body = document.createElement("div"); body.className = "create-body";711712 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {713 const g = document.createElement("div"); g.className = "f-group";714 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;715 const inp = document.createElement("input"); inp.className = "f-input";716 g.append(l, inp); return { g, inp };717 }718719 720 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";721 body.appendChild(tg.g);722723 724 const ug = fGroup("website");725 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";726 if (loginSubdomain) {727 const h = document.createElement("div"); h.className = "url-hint";728 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);729 }730 body.appendChild(ug.g);731732 733 const xg = fGroup("username / email");734 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";735 xg.inp.setAttribute("autocomplete", "off");736 body.appendChild(xg.g);737738 739 const pg = fGroup("password");740 const pwInp = document.createElement("input"); pwInp.className = "f-input";741 pwInp.type = "text"; pwInp.value = prePassword || generatePassword();742 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';743 pwInp.style.letterSpacing = "0.04em";744 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");745 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";746 genBtn.innerHTML = genSvgStr() + " regen";747 genBtn.addEventListener("click", () => { pwInp.value = generatePassword(); });748 const pwRow = document.createElement("div"); pwRow.className = "f-row";749 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);750 body.appendChild(pg.g);751752 753 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";754 saveBtn.textContent = "Save entry";755 saveBtn.addEventListener("click", async () => {756 const title = tg.inp.value.trim();757 const pw = pwInp.value.trim();758 const username = xg.inp.value.trim();759 if (!title || !pw) {760 if (!title) { tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus(); }761 else { pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus(); }762 return;763 }764 saveBtn.disabled = true; saveBtn.textContent = "Saving…";765 try {766 const res = await chrome.runtime.sendMessage({767 type: "ADD_ENTRY",768 payload: { entry: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" } },769 });770 if ((res as { ok?: boolean }).ok) {771 772 if (options.isSignup && options.onFillFields) {773 options.onFillFields(username, pw);774 }775 showSuccess(title);776 } else {777 saveBtn.disabled = false; saveBtn.textContent = "Save entry";778 }779 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }780 });781 body.appendChild(saveBtn);782783 wrap.append(head, body);784 container.appendChild(wrap);785 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });786 }787788 function showSuccess(title: string) {789 currentMode = "success";790 container.innerHTML = "";791 const wrap = document.createElement("div"); wrap.className = "success-wrap";792 const ring = document.createElement("div"); ring.className = "success-ring";793 ring.innerHTML = checkSvgStr();794 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;795 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;796 wrap.append(ring, t, s);797 container.appendChild(wrap);798 setTimeout(() => dismiss(), 1600);799 }800801 function renderLocked() {802 container.innerHTML = "";803804 805 const domHdr = document.createElement("div");806 domHdr.className = "domain-header";807 const eyebrow = document.createElement("div");808 eyebrow.className = "domain-eyebrow";809 eyebrow.textContent = "vault locked";810 const domName = document.createElement("div");811 domName.className = "domain-name";812 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");813 domHdr.append(eyebrow, domName);814 container.appendChild(domHdr);815816 const wrap = document.createElement("div");817 wrap.className = "locked-wrap";818819 820 const head = document.createElement("div"); head.className = "locked-head";821 const icon = document.createElement("div"); icon.className = "locked-icon";822 icon.innerHTML = keySvgStr();823 const title = document.createElement("div"); title.className = "locked-title";824 title.textContent = "unlock to autofill";825 head.append(icon, title);826827 828 const pwInp = document.createElement("input");829 pwInp.className = "locked-input";830 pwInp.type = "password";831 pwInp.placeholder = "master password";832 pwInp.setAttribute("autocomplete", "current-password");833834 835 const errEl = document.createElement("div");836 errEl.className = "locked-error";837 errEl.style.display = "none";838839 840 const unlockBtn = document.createElement("button");841 unlockBtn.className = "locked-btn";842 unlockBtn.type = "button";843 unlockBtn.textContent = "unlock";844845 async function tryUnlock() {846 const pw = pwInp.value.trim();847 if (!pw) { pwInp.focus(); return; }848 unlockBtn.disabled = true;849 unlockBtn.textContent = "unlocking…";850 errEl.style.display = "none";851 try {852 const res = await chrome.runtime.sendMessage({853 type: "UNLOCK",854 payload: { password: pw },855 });856 if ((res as { ok?: boolean }).ok) {857 dismiss();858 options.onUnlocked?.();859 } else {860 errEl.textContent = "incorrect password";861 errEl.style.display = "block";862 pwInp.value = "";863 pwInp.focus();864 unlockBtn.disabled = false;865 unlockBtn.textContent = "unlock";866 }867 } catch {868 errEl.textContent = "could not reach Peach";869 errEl.style.display = "block";870 unlockBtn.disabled = false;871 unlockBtn.textContent = "unlock";872 }873 }874875 pwInp.addEventListener("keydown", (e) => { if (e.key === "Enter") void tryUnlock(); });876 unlockBtn.addEventListener("click", () => void tryUnlock());877878 wrap.append(head, pwInp, errEl, unlockBtn);879 container.appendChild(wrap);880 requestAnimationFrame(() => pwInp.focus());881 }882883 function handleKeyDown(e: KeyboardEvent) {884 if (options.locked) {885 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }886 return;887 }888 if (currentMode === "create" || currentMode === "success") {889 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }890 return;891 }892 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");893 const count = allEntryEls.length;894 if (document.activeElement === searchInput) {895 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }896 if (count === 0) return;897 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;898 }899 if (count === 0) return;900 switch (e.key) {901 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;902 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;903 case "Enter": {904 if (focusedIndex >= 0) {905 const el = allEntryEls[focusedIndex] as HTMLElement;906 const id = el.dataset.id;907 if (id) { onSelect(id); dismiss(); }908 else if (el.classList.contains("is-new") || !id) {909 910 showCreateForm();911 }912 }913 break;914 }915 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;916 }917 }918919 document.addEventListener("keydown", handleKeyDown);920921 document.body.appendChild(host);922 if (options.locked) {923 renderLocked();924 } else {925 renderList();926 }927928 function updatePosition() {929 const r = anchorEl.getBoundingClientRect();930 host.style.top = `${r.bottom + window.scrollY + 5}px`;931 host.style.left = `${r.left + window.scrollX}px`;932 host.style.width = `${Math.max(r.width, 260)}px`;933 }934 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);935936 function dismiss(userAction = false) {937 stopObserving();938 host.remove();939 document.removeEventListener("click", outsideClick, { capture: true });940 document.removeEventListener("keydown", handleKeyDown);941 if (userAction) options.onDismiss?.();942 }943944 function outsideClick(ev: MouseEvent) {945 const path = ev.composedPath();946 947 948 949 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);950 }951 952 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);953954 return dismiss;955}