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 72 73 74 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="#D96830"/><path d="M37 8.5C40.5 3.5 48 5 46 10.5C42 11 37 8.5 37 8.5Z" fill="#7B9060"/></svg>`;75}7677function genSvgStr(): string {78 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>`;79}8081function checkSvgStr(): string {82 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>`;83}8485function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {86 const { hostname, origin } = location;87 const parts = hostname.split(".");88 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",89 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",90 "oidc", "federation", "saml", "cas", "ldap"];91 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());92 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;93 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);94 return { origin, title, loginSubdomain };95}96979899100101102export function showDomainConfirmToast(103 pageDomain: string,104 entryId: string,105): void {106 const host = document.createElement("div");107 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";108 host.style.setProperty("--peach-accent", accentHex);109 const shadow = host.attachShadow({ mode: "closed" });110111 const style = document.createElement("style");112 style.textContent = `113 .bar {114 display:flex;align-items:center;gap:10px;padding:10px 14px;115 background:#131109;border:1px solid rgba(240,232,216,0.09);116 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);117 font-family:Inter,sans-serif;white-space:nowrap;118 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;119 }120 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }121 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }122 .yes {123 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;124 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;125 }126 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }127 .no {128 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;129 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;130 }131 `;132 shadow.appendChild(style);133134 const bar = document.createElement("div"); bar.className = "bar";135 const txt = document.createElement("span"); txt.className = "txt";136 txt.textContent = `Set ${pageDomain} as default for this entry?`;137 const yesBtn = document.createElement("button"); yesBtn.className = "yes";138 yesBtn.textContent = "Set default";139 yesBtn.addEventListener("click", async () => {140 await chrome.runtime.sendMessage({141 type: "UPDATE_ENTRY",142 payload: { id: entryId, patch: { url: location.origin } },143 });144 host.remove();145 });146 const noBtn = document.createElement("button"); noBtn.className = "no";147 noBtn.textContent = "Skip";148 noBtn.addEventListener("click", () => host.remove());149 bar.append(txt, yesBtn, noBtn);150 shadow.appendChild(bar);151 document.body.appendChild(host);152 setTimeout(() => host.remove(), 9000);153}154155function generatePassword(): string {156 const u = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", l = "abcdefghijklmnopqrstuvwxyz";157 const n = "0123456789", s = "!@#$%^&*";158 const all = u + l + n + s;159 const arr = new Uint8Array(20);160 crypto.getRandomValues(arr);161 const res = [u[arr[0]! % u.length]!, l[arr[1]! % l.length]!, n[arr[2]! % n.length]!, s[arr[3]! % s.length]!,162 ...Array.from(arr.slice(4), b => all[b % all.length]!)];163 for (let i = res.length - 1; i > 0; i--) {164 const j = arr[i % arr.length]! % (i + 1);165 [res[i], res[j]] = [res[j]!, res[i]!];166 }167 return res.join("");168}169170export function observeAnchorPosition(171 anchorEl: HTMLElement,172 onUpdate: () => void173): () => void {174 let rafId = 0;175 let burstUntil = 0;176177 const runUpdate = () => {178 rafId = 0;179 onUpdate();180 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);181 };182 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };183 const scheduleBurst = (ms = 220) => {184 burstUntil = Math.max(burstUntil, performance.now() + ms);185 scheduleUpdate();186 };187188 const resizeObserver = new ResizeObserver(() => scheduleBurst());189 resizeObserver.observe(anchorEl);190 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);191192 const mutationObserver = new MutationObserver(() => scheduleBurst());193 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });194 if (anchorEl.parentElement) {195 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });196 }197198 const onScroll = () => scheduleUpdate();199 window.addEventListener("scroll", onScroll, true);200 window.addEventListener("resize", onScroll);201 window.visualViewport?.addEventListener("resize", onScroll);202 window.visualViewport?.addEventListener("scroll", onScroll);203 anchorEl.addEventListener("focus", () => scheduleBurst());204 anchorEl.addEventListener("input", () => scheduleBurst());205206 scheduleBurst(260);207208 return () => {209 if (rafId) cancelAnimationFrame(rafId);210 resizeObserver.disconnect();211 mutationObserver.disconnect();212 window.removeEventListener("scroll", onScroll, true);213 window.removeEventListener("resize", onScroll);214 window.visualViewport?.removeEventListener("resize", onScroll);215 window.visualViewport?.removeEventListener("scroll", onScroll);216 };217}218219export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {220 const button = document.createElement("button");221 button.type = "button";222 button.setAttribute("aria-label", "Peach autofill");223 button.title = "Fill with Peach";224 button.innerHTML = peachIconSvg();225 226 227 228 229 button.style.cssText = `230 position: fixed; z-index: 2147483646;231 width: 26px; height: 26px; cursor: pointer;232 pointer-events: auto; border: none; padding: 3px;233 display: inline-flex; align-items: center; justify-content: center;234 background: transparent; border-radius: 999px;235 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;236 opacity: 0;237 `;238 button.addEventListener("mouseenter", () => {239 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;240 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;241 button.style.transform = "scale(1.08)";242 });243 button.addEventListener("mouseleave", () => {244 button.style.background = "transparent";245 button.style.boxShadow = "none";246 button.style.transform = "none";247 });248 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });249 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });250251 document.body.appendChild(button);252253 function updatePosition() {254 const rect = field.getBoundingClientRect();255 if (rect.width === 0 || rect.height === 0) {256 button.style.opacity = "0"; button.style.pointerEvents = "none";257 return;258 }259 const sz = 26;260 const inset = Math.max(5, Math.min(10, rect.height * 0.22));261 const top = rect.top + rect.height / 2 - sz / 2;262 263 264 const minLeft = rect.left + 4;265 const left = Math.max(minLeft, rect.right - sz - inset);266 267 if (rect.width < 40) {268 button.style.opacity = "0"; button.style.pointerEvents = "none";269 return;270 }271 button.style.top = `${top}px`;272 button.style.left = `${left}px`;273 button.style.opacity = "1"; button.style.pointerEvents = "auto";274 }275 updatePosition();276 requestAnimationFrame(() => { button.style.opacity = "1"; });277 const stopObserving = observeAnchorPosition(field, updatePosition);278 return () => { stopObserving(); button.remove(); };279}280281282const SELECTOR_STYLES = `283 :host { all: initial; }284 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }285 .selector {286 background: #0f0e0b;287 border: 1px solid rgba(240,232,216,0.10);288 border-radius: 13px;289 overflow: hidden;290 box-shadow: 0 4px 16px rgba(0,0,0,0.45), 0 16px 48px rgba(0,0,0,0.7);291 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;292 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;293 }294 @keyframes sl-in {295 from { opacity:0; transform:translateY(-6px) scale(0.97) }296 to { opacity:1; transform:none }297 }298299 /* Domain header */300 .domain-header {301 padding: 9px 13px 7px;302 border-bottom: 1px solid rgba(240,232,216,0.055);303 display: flex; align-items: center; justify-content: space-between;304 }305 .domain-eyebrow {306 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;307 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;308 }309 .domain-name {310 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);311 font-family: "IBM Plex Mono", monospace;312 }313314 /* Search */315 .search-wrap {316 padding: 9px 12px 7px;317 border-bottom: 1px solid rgba(240,232,216,0.04);318 }319 .search {320 width: 100%; background: rgba(240,232,216,0.03);321 border: 1px solid rgba(240,232,216,0.08);322 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;323 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;324 transition: border-color 120ms, box-shadow 120ms;325 }326 .search:focus {327 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);328 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);329 }330 .search::placeholder { color: rgba(240,232,216,0.2); }331332 /* Results */333 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }334 .results::-webkit-scrollbar { display: none; }335336 /* Entry rows */337 .entry {338 display: flex; align-items: center; padding: 8px 12px;339 cursor: pointer; border-bottom: 1px solid rgba(240,232,216,0.04);340 transition: background 70ms; position: relative; outline: none;341 }342 .entry:last-child { border-bottom: none; }343 .entry:hover { background: rgba(240,232,216,0.04); }344 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }345 .entry.focused::before {346 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;347 width: 2px; border-radius: 2px; background: color-mix(in srgb, var(--peach-accent) 55%, transparent);348 }349350 .entry-icon {351 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;352 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.07);353 display: flex; align-items: center; justify-content: center;354 overflow: hidden; margin-right: 10px;355 }356 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }357 .entry-icon.is-new {358 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);359 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);360 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);361 }362363 .entry-info { flex: 1; min-width: 0; }364 .entry-title {365 font-size: 12.5px; color: #F0E8D8;366 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;367 line-height: 1.3;368 }369 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }370 .entry-sub {371 font-size: 10px; color: rgba(240,232,216,0.32);372 font-family: "IBM Plex Mono", monospace;373 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;374 margin-top: 1px;375 }376377 .totp-badge {378 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;379 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);380 color: color-mix(in srgb, var(--peach-accent) 75%, transparent); font-family: "IBM Plex Mono", monospace;381 flex-shrink: 0; margin-left: 8px;382 }383384 .divider { height: 1px; background: rgba(240,232,216,0.05); margin: 2px 0; }385386 .empty-hint {387 padding: 14px 13px; font-size: 11px; color: rgba(240,232,216,0.28);388 font-family: "IBM Plex Mono", monospace;389 }390391 /* ── Create form ─────────────────────────────────────── */392 .create-form-wrap {393 animation: cf-in 180ms cubic-bezier(0.16,1,0.3,1) both;394 }395 @keyframes cf-in {396 from { opacity:0; transform:translateX(8px) }397 to { opacity:1; transform:none }398 }399 .create-head {400 display: flex; align-items: center; gap: 8px;401 padding: 10px 12px; border-bottom: 1px solid rgba(240,232,216,0.055);402 }403 .back-btn {404 background: transparent; border: none; cursor: pointer;405 color: rgba(240,232,216,0.38); padding: 3px; border-radius: 5px;406 display: flex; align-items: center; line-height: 0;407 transition: color 100ms, background 100ms;408 }409 .back-btn:hover { color: rgba(240,232,216,0.75); background: rgba(240,232,216,0.05); }410 .create-label { font-size: 12px; color: rgba(240,232,216,0.65); font-weight: 500; flex: 1; }411 .create-body { padding: 12px; display: grid; gap: 9px; }412 .f-group { display: grid; gap: 3px; }413 .f-label {414 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;415 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;416 }417 .f-input {418 background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);419 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;420 font-size: 11px; font-family: "IBM Plex Mono", monospace;421 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;422 }423 .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); }424 .f-input::placeholder { color: rgba(240,232,216,0.18); }425 .f-input.url-f { font-size: 10px; color: rgba(240,232,216,0.5); letter-spacing: 0.01em; }426 .f-row { display: flex; gap: 6px; }427 .f-row .f-input { flex: 1; min-width: 0; }428 .gen-btn {429 padding: 0 10px; border-radius: 7px; border: none;430 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);431 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);432 font-size: 10px; font-family: "IBM Plex Mono", monospace;433 cursor: pointer; white-space: nowrap; flex-shrink: 0;434 display: flex; align-items: center; gap: 4px;435 transition: background 100ms;436 }437 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }438 .save-btn {439 width: 100%; padding: 8px; border-radius: 8px; border: none;440 background: var(--peach-accent); color: #fff; font-size: 12px;441 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;442 transition: background 120ms; letter-spacing: 0.01em;443 }444 .save-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }445 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }446 .url-hint {447 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);448 font-family: "IBM Plex Mono", monospace; font-style: italic;449 }450451 /* ── Success ─────────────────────────────────────────── */452 .success-wrap {453 display: flex; flex-direction: column; align-items: center;454 padding: 24px 16px 20px; gap: 8px;455 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;456 }457 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }458 .success-ring {459 width: 40px; height: 40px; border-radius: 50%;460 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);461 display: flex; align-items: center; justify-content: center;462 }463 .success-text { font-size: 12.5px; color: #F0E8D8; font-weight: 500; }464 .success-sub { font-size: 10px; color: rgba(240,232,216,0.3); font-family:"IBM Plex Mono",monospace; }465466 /* ── Locked state ────────────────────────────────────── */467 .locked-wrap {468 display: flex; flex-direction: column; align-items: stretch;469 padding: 14px 13px 13px; gap: 9px;470 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;471 }472 .locked-head {473 display: flex; align-items: center; gap: 8px; padding-bottom: 2px;474 }475 .locked-icon {476 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;477 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.08);478 display: flex; align-items: center; justify-content: center;479 }480 .locked-title {481 font-size: 11.5px; color: rgba(240,232,216,0.55); font-weight: 500;482 }483 .locked-input {484 width: 100%; background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);485 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;486 font-size: 11px; font-family: "IBM Plex Mono", monospace;487 outline: none; transition: border-color 120ms, box-shadow 120ms;488 }489 .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); }490 .locked-input::placeholder { color: rgba(240,232,216,0.2); }491 .locked-btn {492 width: 100%; padding: 8px; border-radius: 8px; border: none;493 background: var(--peach-accent); color: #fff; font-size: 12px;494 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;495 transition: background 120ms; letter-spacing: 0.01em;496 }497 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }498 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; }499 .locked-error {500 font-size: 10px; color: rgba(220,80,60,0.85);501 font-family: "IBM Plex Mono", monospace;502 }503`;504505export function showAutofillSelector(506 anchorEl: HTMLInputElement,507 entries: SelectorEntry[],508 onSelect: (id: string) => void,509 options: SelectorOptions = {}510): () => void {511 const host = document.createElement("div");512 host.style.cssText = "position: absolute; z-index: 2147483647;";513 host.style.setProperty("--peach-accent", accentHex);514 const shadow = host.attachShadow({ mode: "closed" });515516 const style = shadow.appendChild(document.createElement("style"));517 style.textContent = SELECTOR_STYLES;518519 const rect = anchorEl.getBoundingClientRect();520 host.style.top = `${rect.bottom + window.scrollY + 5}px`;521 host.style.left = `${rect.left + window.scrollX}px`;522 host.style.width = `${Math.max(rect.width, 260)}px`;523524 const container = shadow.appendChild(document.createElement("div"));525 container.className = "selector";526527 const searchableEntries = options.searchableEntries ?? [];528 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();529530 let query = "";531 let focusedIndex = -1;532 let currentMode: "list" | "create" | "success" = "list";533 let searchInput: HTMLInputElement | null = null;534535 536 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";537 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");538 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";539540 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {541 const seen = new Set<string>();542 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });543 }544545 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {546 const q = value.trim().toLowerCase();547 const deduped = uniqueEntries(source);548 if (!q) return deduped.slice(0, 8);549 return deduped.filter(e =>550 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)551 ).slice(0, 12);552 }553554 function activeEntries(): SelectorEntry[] {555 556 if (query.trim()) {557 const merged = uniqueEntries([...entries, ...searchableEntries]);558 return filterEntries(merged, query);559 }560 561 if (entries.length > 0) return entries;562 return filterEntries(searchableEntries, "").slice(0, 8);563 }564565 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {566 const icon = document.createElement("div");567 icon.className = `entry-icon${isNew ? " is-new" : ""}`;568 if (isNew) {569 icon.innerHTML = plusSvgStr();570 } else {571 572 573 574 void url;575 icon.innerHTML = keySvgStr();576 }577 return icon;578 }579580 function renderList() {581 container.innerHTML = "";582 focusedIndex = -1;583 searchInput = null;584585 const current = activeEntries();586587 588 const domHdr = document.createElement("div");589 domHdr.className = "domain-header";590 const eyebrow = document.createElement("div");591 eyebrow.className = "domain-eyebrow";592 eyebrow.textContent = options.isSignup593 ? "Sign up as"594 : entries.length > 0 ? "Saved for this site" : "No saved login";595 const domName = document.createElement("div");596 domName.className = "domain-name";597 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");598 domHdr.append(eyebrow, domName);599 container.appendChild(domHdr);600601 602 const sw = document.createElement("div"); sw.className = "search-wrap";603 searchInput = document.createElement("input");604 searchInput.className = "search";605 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";606 searchInput.value = query;607 searchInput.addEventListener("input", () => {608 query = searchInput?.value ?? "";609 renderList();610 searchInput?.focus();611 searchInput?.setSelectionRange(query.length, query.length);612 });613 sw.appendChild(searchInput);614 container.appendChild(sw);615616 617 const results = document.createElement("div");618 results.className = "results";619 container.appendChild(results);620621 if (current.length === 0 && entries.length > 0) {622 623 }624625 for (let i = 0; i < current.length; i++) {626 const e = current[i]!;627 const entryEl = document.createElement("div");628 entryEl.className = "entry";629 entryEl.dataset.id = e.id;630 entryEl.dataset.index = String(i);631 entryEl.setAttribute("tabindex", "-1");632633 entryEl.appendChild(makeEntryIcon(e.url));634635 const info = document.createElement("div"); info.className = "entry-info";636 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;637 const subEl = document.createElement("div"); subEl.className = "entry-sub";638 const domain = e.url ? getDomainFromUrl(e.url) : "";639 subEl.textContent = domain || e.username || "";640 info.append(titleEl, subEl);641 entryEl.appendChild(info);642643 if (e.hasTotp) {644 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";645 entryEl.appendChild(badge);646 }647648 entryEl.addEventListener("click", () => {649 if (options.isSignup && options.onFillFields) {650 651 options.onFillFields(e.username ?? "", "");652 dismiss();653 } else {654 onSelect(e.id);655 dismiss();656 }657 });658 results.appendChild(entryEl);659 }660661 662 if (current.length > 0) {663 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));664 }665666 667 const newEl = document.createElement("div");668 newEl.className = "entry";669 newEl.setAttribute("tabindex", "-1");670 newEl.appendChild(makeEntryIcon(undefined, true));671672 const newInfo = document.createElement("div"); newInfo.className = "entry-info";673 const newTitle = document.createElement("div");674 newTitle.className = "entry-title is-new";675 newTitle.textContent = `New entry for ${sTitle}`;676 const newSub = document.createElement("div");677 newSub.className = "entry-sub";678 newSub.textContent = sOrigin;679 newInfo.append(newTitle, newSub);680 newEl.appendChild(newInfo);681 newEl.addEventListener("click", showCreateForm);682 results.appendChild(newEl);683684 685 if (searchInput) {686 requestAnimationFrame(() => searchInput?.focus());687 }688 }689690 function setFocused(idx: number) {691 const all = container.querySelectorAll<HTMLElement>(".entry");692 all.forEach((el, i) => el.classList.toggle("focused", i === idx));693 focusedIndex = idx;694 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });695 }696697 async function showCreateForm() {698 currentMode = "create";699 container.innerHTML = "";700701 702 703 704 const statusRes = await chrome.runtime.sendMessage({ type: "GET_VAULT_STATUS" }).catch(() => null) as { unlocked?: boolean } | null;705 if (!statusRes?.unlocked) {706 renderLocked();707 return;708 }709710 const wrap = document.createElement("div");711 wrap.className = "create-form-wrap";712713 714 const head = document.createElement("div"); head.className = "create-head";715 const backBtn = document.createElement("button"); backBtn.className = "back-btn";716 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>`;717 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });718 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";719 head.append(backBtn, lbl);720721 722 const body = document.createElement("div"); body.className = "create-body";723724 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {725 const g = document.createElement("div"); g.className = "f-group";726 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;727 const inp = document.createElement("input"); inp.className = "f-input";728 g.append(l, inp); return { g, inp };729 }730731 732 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";733 body.appendChild(tg.g);734735 736 const ug = fGroup("website");737 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";738 if (loginSubdomain) {739 const h = document.createElement("div"); h.className = "url-hint";740 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);741 }742 body.appendChild(ug.g);743744 745 const xg = fGroup("username / email");746 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";747 xg.inp.setAttribute("autocomplete", "off");748 body.appendChild(xg.g);749750 751 const pg = fGroup("password");752 const pwInp = document.createElement("input"); pwInp.className = "f-input";753 pwInp.type = "text"; pwInp.value = prePassword || generatePassword();754 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';755 pwInp.style.letterSpacing = "0.04em";756 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");757 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";758 genBtn.innerHTML = genSvgStr() + " regen";759 genBtn.addEventListener("click", () => { pwInp.value = generatePassword(); });760 const pwRow = document.createElement("div"); pwRow.className = "f-row";761 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);762 body.appendChild(pg.g);763764 765 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";766 saveBtn.textContent = "Save entry";767 const errEl = document.createElement("div"); errEl.className = "locked-error"; errEl.style.display = "none";768 function showFormError(msg: string) { errEl.textContent = msg; errEl.style.display = ""; }769 saveBtn.addEventListener("click", async () => {770 const title = tg.inp.value.trim();771 const pw = pwInp.value.trim();772 const username = xg.inp.value.trim();773 if (!title) {774 tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus();775 showFormError("Entry needs a title");776 return;777 }778 if (!pw) {779 pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus();780 showFormError("Entry needs a password");781 return;782 }783 errEl.style.display = "none";784 saveBtn.disabled = true; saveBtn.textContent = "Saving…";785 try {786 const res = await chrome.runtime.sendMessage({787 type: "ADD_ENTRY",788 payload: { entry: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" } },789 });790 if ((res as { ok?: boolean }).ok) {791 792 if (options.isSignup && options.onFillFields) {793 options.onFillFields(username, pw);794 }795 showSuccess(title);796 } else {797 const errMsg = (res as { error?: string }).error === "vault locked"798 ? "Vault locked — open Peach to save"799 : "Could not save entry";800 showFormError(errMsg);801 saveBtn.disabled = false; saveBtn.textContent = "Save entry";802 }803 } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }804 });805 body.appendChild(saveBtn);806 body.appendChild(errEl);807808 wrap.append(head, body);809 container.appendChild(wrap);810 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });811 }812813 function showSuccess(title: string) {814 currentMode = "success";815 container.innerHTML = "";816 const wrap = document.createElement("div"); wrap.className = "success-wrap";817 const ring = document.createElement("div"); ring.className = "success-ring";818 ring.innerHTML = checkSvgStr();819 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;820 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;821 wrap.append(ring, t, s);822 container.appendChild(wrap);823 setTimeout(() => dismiss(), 1600);824 }825826 function renderLocked() {827 container.innerHTML = "";828829 830 const domHdr = document.createElement("div");831 domHdr.className = "domain-header";832 const eyebrow = document.createElement("div");833 eyebrow.className = "domain-eyebrow";834 eyebrow.textContent = "vault locked";835 const domName = document.createElement("div");836 domName.className = "domain-name";837 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");838 domHdr.append(eyebrow, domName);839 container.appendChild(domHdr);840841 const wrap = document.createElement("div");842 wrap.className = "locked-wrap";843844 845 const head = document.createElement("div"); head.className = "locked-head";846 const icon = document.createElement("div"); icon.className = "locked-icon";847 icon.innerHTML = keySvgStr();848 const title = document.createElement("div"); title.className = "locked-title";849 title.textContent = "unlock to autofill";850 head.append(icon, title);851852 853 const pwInp = document.createElement("input");854 pwInp.className = "locked-input";855 pwInp.type = "password";856 pwInp.placeholder = "master password";857 pwInp.setAttribute("autocomplete", "current-password");858859 860 const errEl = document.createElement("div");861 errEl.className = "locked-error";862 errEl.style.display = "none";863864 865 const unlockBtn = document.createElement("button");866 unlockBtn.className = "locked-btn";867 unlockBtn.type = "button";868 unlockBtn.textContent = "unlock";869870 async function tryUnlock() {871 const pw = pwInp.value.trim();872 if (!pw) { pwInp.focus(); return; }873 unlockBtn.disabled = true;874 unlockBtn.textContent = "unlocking…";875 errEl.style.display = "none";876 try {877 const res = await chrome.runtime.sendMessage({878 type: "UNLOCK",879 payload: { password: pw },880 });881 if ((res as { ok?: boolean }).ok) {882 dismiss();883 options.onUnlocked?.();884 } else {885 errEl.textContent = "incorrect password";886 errEl.style.display = "block";887 pwInp.value = "";888 pwInp.focus();889 unlockBtn.disabled = false;890 unlockBtn.textContent = "unlock";891 }892 } catch {893 errEl.textContent = "could not reach Peach";894 errEl.style.display = "block";895 unlockBtn.disabled = false;896 unlockBtn.textContent = "unlock";897 }898 }899900 pwInp.addEventListener("keydown", (e) => { if (e.key === "Enter") void tryUnlock(); });901 unlockBtn.addEventListener("click", () => void tryUnlock());902903 wrap.append(head, pwInp, errEl, unlockBtn);904 container.appendChild(wrap);905 requestAnimationFrame(() => pwInp.focus());906 }907908 function handleKeyDown(e: KeyboardEvent) {909 if (options.locked) {910 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }911 return;912 }913 if (currentMode === "create" || currentMode === "success") {914 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }915 return;916 }917 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");918 const count = allEntryEls.length;919 if (document.activeElement === searchInput) {920 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }921 if (count === 0) return;922 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;923 }924 if (count === 0) return;925 switch (e.key) {926 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;927 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;928 case "Enter": {929 if (focusedIndex >= 0) {930 const el = allEntryEls[focusedIndex] as HTMLElement;931 const id = el.dataset.id;932 if (id) { onSelect(id); dismiss(); }933 else if (el.classList.contains("is-new") || !id) {934 935 showCreateForm();936 }937 }938 break;939 }940 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;941 }942 }943944 document.addEventListener("keydown", handleKeyDown);945946 document.body.appendChild(host);947 if (options.locked) {948 renderLocked();949 } else {950 renderList();951 }952953 function updatePosition() {954 const r = anchorEl.getBoundingClientRect();955 host.style.top = `${r.bottom + window.scrollY + 5}px`;956 host.style.left = `${r.left + window.scrollX}px`;957 host.style.width = `${Math.max(r.width, 260)}px`;958 }959 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);960961 function dismiss(userAction = false) {962 stopObserving();963 host.remove();964 document.removeEventListener("click", outsideClick, { capture: true });965 document.removeEventListener("keydown", handleKeyDown);966 if (userAction) options.onDismiss?.();967 }968969 function outsideClick(ev: MouseEvent) {970 const path = ev.composedPath();971 972 973 974 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);975 }976 977 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);978979 return dismiss;980}