1import { ACCENT_COLORS } from "../shared/types";23let accentHex = "#E8956D";4chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string } } | undefined) => {5 const name = resp?.data?.accentColor ?? "peach";6 accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";7});89export interface SelectorEntry {10 id: string;11 title: string;12 username?: string;13 url?: string;14 hasTotp?: boolean;15}1617interface SelectorOptions {18 domain?: string;19 searchableEntries?: SelectorEntry[];20 21 isSignup?: boolean;22 2324252627 onFillFields?: (username: string, password: string) => void;28 2930313233 locked?: boolean;34 35363738 onUnlocked?: () => void;39 4041424344 onDismiss?: () => void;45}4647function getDomainFromUrl(url: string): string {48 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }49}5051function keySvgStr(): string {52 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>`;53}5455function plusSvgStr(): string {56 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>`;57}5859function peachIconSvg(): string {60 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>`;61}6263function genSvgStr(): string {64 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>`;65}6667function checkSvgStr(): string {68 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>`;69}7071function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {72 const { hostname, origin } = location;73 const parts = hostname.split(".");74 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",75 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",76 "oidc", "federation", "saml", "cas", "ldap"];77 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());78 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;79 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);80 return { origin, title, loginSubdomain };81}82838485868788export function showDomainConfirmToast(89 pageDomain: string,90 entryId: string,91): void {92 const host = document.createElement("div");93 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";94 host.style.setProperty("--peach-accent", accentHex);95 const shadow = host.attachShadow({ mode: "closed" });9697 const style = document.createElement("style");98 style.textContent = `99 .bar {100 display:flex;align-items:center;gap:10px;padding:10px 14px;101 background:#131109;border:1px solid rgba(240,232,216,0.09);102 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);103 font-family:Inter,sans-serif;white-space:nowrap;104 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;105 }106 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }107 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }108 .yes {109 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;110 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;111 }112 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }113 .no {114 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;115 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;116 }117 `;118 shadow.appendChild(style);119120 const bar = document.createElement("div"); bar.className = "bar";121 const txt = document.createElement("span"); txt.className = "txt";122 txt.textContent = `Set ${pageDomain} as default for this entry?`;123 const yesBtn = document.createElement("button"); yesBtn.className = "yes";124 yesBtn.textContent = "Set default";125 yesBtn.addEventListener("click", async () => {126 await chrome.runtime.sendMessage({127 type: "UPDATE_ENTRY",128 payload: { id: entryId, patch: { url: location.origin } },129 });130 host.remove();131 });132 const noBtn = document.createElement("button"); noBtn.className = "no";133 noBtn.textContent = "Skip";134 noBtn.addEventListener("click", () => host.remove());135 bar.append(txt, yesBtn, noBtn);136 shadow.appendChild(bar);137 document.body.appendChild(host);138 setTimeout(() => host.remove(), 9000);139}140141function generatePassword(): string {142 const u = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", l = "abcdefghijklmnopqrstuvwxyz";143 const n = "0123456789", s = "!@#$%^&*";144 const all = u + l + n + s;145 const arr = new Uint8Array(20);146 crypto.getRandomValues(arr);147 const res = [u[arr[0]! % u.length]!, l[arr[1]! % l.length]!, n[arr[2]! % n.length]!, s[arr[3]! % s.length]!,148 ...Array.from(arr.slice(4), b => all[b % all.length]!)];149 for (let i = res.length - 1; i > 0; i--) {150 const j = arr[i % arr.length]! % (i + 1);151 [res[i], res[j]] = [res[j]!, res[i]!];152 }153 return res.join("");154}155156export function observeAnchorPosition(157 anchorEl: HTMLElement,158 onUpdate: () => void159): () => void {160 let rafId = 0;161 let burstUntil = 0;162163 const runUpdate = () => {164 rafId = 0;165 onUpdate();166 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);167 };168 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };169 const scheduleBurst = (ms = 220) => {170 burstUntil = Math.max(burstUntil, performance.now() + ms);171 scheduleUpdate();172 };173174 const resizeObserver = new ResizeObserver(() => scheduleBurst());175 resizeObserver.observe(anchorEl);176 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);177178 const mutationObserver = new MutationObserver(() => scheduleBurst());179 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });180 if (anchorEl.parentElement) {181 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });182 }183184 const onScroll = () => scheduleUpdate();185 window.addEventListener("scroll", onScroll, true);186 window.addEventListener("resize", onScroll);187 window.visualViewport?.addEventListener("resize", onScroll);188 window.visualViewport?.addEventListener("scroll", onScroll);189 anchorEl.addEventListener("focus", () => scheduleBurst());190 anchorEl.addEventListener("input", () => scheduleBurst());191192 scheduleBurst(260);193194 return () => {195 if (rafId) cancelAnimationFrame(rafId);196 resizeObserver.disconnect();197 mutationObserver.disconnect();198 window.removeEventListener("scroll", onScroll, true);199 window.removeEventListener("resize", onScroll);200 window.visualViewport?.removeEventListener("resize", onScroll);201 window.visualViewport?.removeEventListener("scroll", onScroll);202 };203}204205export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {206 const button = document.createElement("button");207 button.type = "button";208 button.setAttribute("aria-label", "Peach autofill");209 button.title = "Fill with Peach";210 button.innerHTML = peachIconSvg();211 button.style.cssText = `212 position: fixed; z-index: 2147483646;213 width: 26px; height: 26px; cursor: pointer;214 pointer-events: auto; border: none; padding: 3px;215 display: inline-flex; align-items: center; justify-content: center;216 background: transparent; border-radius: 999px;217 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;218 opacity: 0;219 `;220 button.addEventListener("mouseenter", () => {221 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;222 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;223 button.style.transform = "scale(1.08)";224 });225 button.addEventListener("mouseleave", () => {226 button.style.background = "transparent";227 button.style.boxShadow = "none";228 button.style.transform = "none";229 });230 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });231 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });232233 document.body.appendChild(button);234235 function updatePosition() {236 const rect = field.getBoundingClientRect();237 if (rect.width === 0 || rect.height === 0) {238 button.style.opacity = "0"; button.style.pointerEvents = "none";239 return;240 }241 const sz = 26;242 const inset = Math.max(5, Math.min(10, rect.height * 0.22));243 const top = rect.top + rect.height / 2 - sz / 2;244 245 246 const minLeft = rect.left + 4;247 const left = Math.max(minLeft, rect.right - sz - inset);248 249 if (rect.width < 40) {250 button.style.opacity = "0"; button.style.pointerEvents = "none";251 return;252 }253 button.style.top = `${top}px`;254 button.style.left = `${left}px`;255 button.style.opacity = "1"; button.style.pointerEvents = "auto";256 }257 updatePosition();258 requestAnimationFrame(() => { button.style.opacity = "1"; });259 const stopObserving = observeAnchorPosition(field, updatePosition);260 return () => { stopObserving(); button.remove(); };261}262263264const SELECTOR_STYLES = `265 :host { all: initial; }266 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }267 .selector {268 background: #0f0e0b;269 border: 1px solid rgba(240,232,216,0.10);270 border-radius: 13px;271 overflow: hidden;272 box-shadow: 0 4px 16px rgba(0,0,0,0.45), 0 16px 48px rgba(0,0,0,0.7);273 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;274 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;275 }276 @keyframes sl-in {277 from { opacity:0; transform:translateY(-6px) scale(0.97) }278 to { opacity:1; transform:none }279 }280281 /* Domain header */282 .domain-header {283 padding: 9px 13px 7px;284 border-bottom: 1px solid rgba(240,232,216,0.055);285 display: flex; align-items: center; justify-content: space-between;286 }287 .domain-eyebrow {288 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;289 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;290 }291 .domain-name {292 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);293 font-family: "IBM Plex Mono", monospace;294 }295296 /* Search */297 .search-wrap {298 padding: 9px 12px 7px;299 border-bottom: 1px solid rgba(240,232,216,0.04);300 }301 .search {302 width: 100%; background: rgba(240,232,216,0.03);303 border: 1px solid rgba(240,232,216,0.08);304 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;305 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;306 transition: border-color 120ms, box-shadow 120ms;307 }308 .search:focus {309 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);310 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);311 }312 .search::placeholder { color: rgba(240,232,216,0.2); }313314 /* Results */315 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }316 .results::-webkit-scrollbar { display: none; }317318 /* Entry rows */319 .entry {320 display: flex; align-items: center; padding: 8px 12px;321 cursor: pointer; border-bottom: 1px solid rgba(240,232,216,0.04);322 transition: background 70ms; position: relative; outline: none;323 }324 .entry:last-child { border-bottom: none; }325 .entry:hover { background: rgba(240,232,216,0.04); }326 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }327 .entry.focused::before {328 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;329 width: 2px; border-radius: 2px; background: color-mix(in srgb, var(--peach-accent) 55%, transparent);330 }331332 .entry-icon {333 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;334 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.07);335 display: flex; align-items: center; justify-content: center;336 overflow: hidden; margin-right: 10px;337 }338 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }339 .entry-icon.is-new {340 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);341 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);342 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);343 }344345 .entry-info { flex: 1; min-width: 0; }346 .entry-title {347 font-size: 12.5px; color: #F0E8D8;348 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;349 line-height: 1.3;350 }351 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }352 .entry-sub {353 font-size: 10px; color: rgba(240,232,216,0.32);354 font-family: "IBM Plex Mono", monospace;355 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;356 margin-top: 1px;357 }358359 .totp-badge {360 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;361 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);362 color: color-mix(in srgb, var(--peach-accent) 75%, transparent); font-family: "IBM Plex Mono", monospace;363 flex-shrink: 0; margin-left: 8px;364 }365366 .divider { height: 1px; background: rgba(240,232,216,0.05); margin: 2px 0; }367368 .empty-hint {369 padding: 14px 13px; font-size: 11px; color: rgba(240,232,216,0.28);370 font-family: "IBM Plex Mono", monospace;371 }372373 /* ── Create form ─────────────────────────────────────── */374 .create-form-wrap {375 animation: cf-in 180ms cubic-bezier(0.16,1,0.3,1) both;376 }377 @keyframes cf-in {378 from { opacity:0; transform:translateX(8px) }379 to { opacity:1; transform:none }380 }381 .create-head {382 display: flex; align-items: center; gap: 8px;383 padding: 10px 12px; border-bottom: 1px solid rgba(240,232,216,0.055);384 }385 .back-btn {386 background: transparent; border: none; cursor: pointer;387 color: rgba(240,232,216,0.38); padding: 3px; border-radius: 5px;388 display: flex; align-items: center; line-height: 0;389 transition: color 100ms, background 100ms;390 }391 .back-btn:hover { color: rgba(240,232,216,0.75); background: rgba(240,232,216,0.05); }392 .create-label { font-size: 12px; color: rgba(240,232,216,0.65); font-weight: 500; flex: 1; }393 .create-body { padding: 12px; display: grid; gap: 9px; }394 .f-group { display: grid; gap: 3px; }395 .f-label {396 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;397 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;398 }399 .f-input {400 background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);401 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;402 font-size: 11px; font-family: "IBM Plex Mono", monospace;403 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;404 }405 .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); }406 .f-input::placeholder { color: rgba(240,232,216,0.18); }407 .f-input.url-f { font-size: 10px; color: rgba(240,232,216,0.5); letter-spacing: 0.01em; }408 .f-row { display: flex; gap: 6px; }409 .f-row .f-input { flex: 1; min-width: 0; }410 .gen-btn {411 padding: 0 10px; border-radius: 7px; border: none;412 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);413 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);414 font-size: 10px; font-family: "IBM Plex Mono", monospace;415 cursor: pointer; white-space: nowrap; flex-shrink: 0;416 display: flex; align-items: center; gap: 4px;417 transition: background 100ms;418 }419 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }420 .save-btn {421 width: 100%; padding: 8px; border-radius: 8px; border: none;422 background: var(--peach-accent); color: #fff; font-size: 12px;423 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;424 transition: background 120ms; letter-spacing: 0.01em;425 }426 .save-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }427 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }428 .url-hint {429 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);430 font-family: "IBM Plex Mono", monospace; font-style: italic;431 }432433 /* ── Success ─────────────────────────────────────────── */434 .success-wrap {435 display: flex; flex-direction: column; align-items: center;436 padding: 24px 16px 20px; gap: 8px;437 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;438 }439 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }440 .success-ring {441 width: 40px; height: 40px; border-radius: 50%;442 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);443 display: flex; align-items: center; justify-content: center;444 }445 .success-text { font-size: 12.5px; color: #F0E8D8; font-weight: 500; }446 .success-sub { font-size: 10px; color: rgba(240,232,216,0.3); font-family:"IBM Plex Mono",monospace; }447448 /* ── Locked state ────────────────────────────────────── */449 .locked-wrap {450 display: flex; flex-direction: column; align-items: stretch;451 padding: 14px 13px 13px; gap: 9px;452 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;453 }454 .locked-head {455 display: flex; align-items: center; gap: 8px; padding-bottom: 2px;456 }457 .locked-icon {458 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;459 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.08);460 display: flex; align-items: center; justify-content: center;461 }462 .locked-title {463 font-size: 11.5px; color: rgba(240,232,216,0.55); font-weight: 500;464 }465 .locked-input {466 width: 100%; background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);467 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;468 font-size: 11px; font-family: "IBM Plex Mono", monospace;469 outline: none; transition: border-color 120ms, box-shadow 120ms;470 }471 .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); }472 .locked-input::placeholder { color: rgba(240,232,216,0.2); }473 .locked-btn {474 width: 100%; padding: 8px; border-radius: 8px; border: none;475 background: var(--peach-accent); color: #fff; font-size: 12px;476 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;477 transition: background 120ms; letter-spacing: 0.01em;478 }479 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }480 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; }481 .locked-error {482 font-size: 10px; color: rgba(220,80,60,0.85);483 font-family: "IBM Plex Mono", monospace;484 }485`;486487export function showAutofillSelector(488 anchorEl: HTMLInputElement,489 entries: SelectorEntry[],490 onSelect: (id: string) => void,491 options: SelectorOptions = {}492): () => void {493 const host = document.createElement("div");494 host.style.cssText = "position: absolute; z-index: 2147483647;";495 host.style.setProperty("--peach-accent", accentHex);496 const shadow = host.attachShadow({ mode: "closed" });497498 const style = shadow.appendChild(document.createElement("style"));499 style.textContent = SELECTOR_STYLES;500501 const rect = anchorEl.getBoundingClientRect();502 host.style.top = `${rect.bottom + window.scrollY + 5}px`;503 host.style.left = `${rect.left + window.scrollX}px`;504 host.style.width = `${Math.max(rect.width, 260)}px`;505506 const container = shadow.appendChild(document.createElement("div"));507 container.className = "selector";508509 const searchableEntries = options.searchableEntries ?? [];510 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();511512 let query = "";513 let focusedIndex = -1;514 let currentMode: "list" | "create" | "success" = "list";515 let searchInput: HTMLInputElement | null = null;516517 518 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";519 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");520 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";521522 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {523 const seen = new Set<string>();524 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });525 }526527 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {528 const q = value.trim().toLowerCase();529 const deduped = uniqueEntries(source);530 if (!q) return deduped.slice(0, 8);531 return deduped.filter(e =>532 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)533 ).slice(0, 12);534 }535536 function activeEntries(): SelectorEntry[] {537 538 if (query.trim()) {539 const merged = uniqueEntries([...entries, ...searchableEntries]);540 return filterEntries(merged, query);541 }542 543 if (entries.length > 0) return entries;544 return filterEntries(searchableEntries, "").slice(0, 8);545 }546547 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {548 const icon = document.createElement("div");549 icon.className = `entry-icon${isNew ? " is-new" : ""}`;550 if (isNew) {551 icon.innerHTML = plusSvgStr();552 } else {553 554 555 556 void url;557 icon.innerHTML = keySvgStr();558 }559 return icon;560 }561562 function renderList() {563 container.innerHTML = "";564 focusedIndex = -1;565 searchInput = null;566567 const current = activeEntries();568569 570 const domHdr = document.createElement("div");571 domHdr.className = "domain-header";572 const eyebrow = document.createElement("div");573 eyebrow.className = "domain-eyebrow";574 eyebrow.textContent = options.isSignup575 ? "Sign up as"576 : entries.length > 0 ? "Saved for this site" : "No saved login";577 const domName = document.createElement("div");578 domName.className = "domain-name";579 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");580 domHdr.append(eyebrow, domName);581 container.appendChild(domHdr);582583 584 const sw = document.createElement("div"); sw.className = "search-wrap";585 searchInput = document.createElement("input");586 searchInput.className = "search";587 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";588 searchInput.value = query;589 searchInput.addEventListener("input", () => {590 query = searchInput?.value ?? "";591 renderList();592 searchInput?.focus();593 searchInput?.setSelectionRange(query.length, query.length);594 });595 sw.appendChild(searchInput);596 container.appendChild(sw);597598 599 const results = document.createElement("div");600 results.className = "results";601 container.appendChild(results);602603 if (current.length === 0 && entries.length > 0) {604 605 }606607 for (let i = 0; i < current.length; i++) {608 const e = current[i]!;609 const entryEl = document.createElement("div");610 entryEl.className = "entry";611 entryEl.dataset.id = e.id;612 entryEl.dataset.index = String(i);613 entryEl.setAttribute("tabindex", "-1");614615 entryEl.appendChild(makeEntryIcon(e.url));616617 const info = document.createElement("div"); info.className = "entry-info";618 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;619 const subEl = document.createElement("div"); subEl.className = "entry-sub";620 const domain = e.url ? getDomainFromUrl(e.url) : "";621 subEl.textContent = domain || e.username || "";622 info.append(titleEl, subEl);623 entryEl.appendChild(info);624625 if (e.hasTotp) {626 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";627 entryEl.appendChild(badge);628 }629630 entryEl.addEventListener("click", () => {631 if (options.isSignup && options.onFillFields) {632 633 options.onFillFields(e.username ?? "", "");634 dismiss();635 } else {636 onSelect(e.id);637 dismiss();638 }639 });640 results.appendChild(entryEl);641 }642643 644 if (current.length > 0) {645 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));646 }647648 649 const newEl = document.createElement("div");650 newEl.className = "entry";651 newEl.setAttribute("tabindex", "-1");652 newEl.appendChild(makeEntryIcon(undefined, true));653654 const newInfo = document.createElement("div"); newInfo.className = "entry-info";655 const newTitle = document.createElement("div");656 newTitle.className = "entry-title is-new";657 newTitle.textContent = `New entry for ${sTitle}`;658 const newSub = document.createElement("div");659 newSub.className = "entry-sub";660 newSub.textContent = sOrigin;661 newInfo.append(newTitle, newSub);662 newEl.appendChild(newInfo);663 newEl.addEventListener("click", showCreateForm);664 results.appendChild(newEl);665666 667 if (searchInput) {668 requestAnimationFrame(() => searchInput?.focus());669 }670 }671672 function setFocused(idx: number) {673 const all = container.querySelectorAll<HTMLElement>(".entry");674 all.forEach((el, i) => el.classList.toggle("focused", i === idx));675 focusedIndex = idx;676 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });677 }678679 function showCreateForm() {680 currentMode = "create";681 container.innerHTML = "";682683 const wrap = document.createElement("div");684 wrap.className = "create-form-wrap";685686 687 const head = document.createElement("div"); head.className = "create-head";688 const backBtn = document.createElement("button"); backBtn.className = "back-btn";689 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>`;690 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });691 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";692 head.append(backBtn, lbl);693694 695 const body = document.createElement("div"); body.className = "create-body";696697 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {698 const g = document.createElement("div"); g.className = "f-group";699 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;700 const inp = document.createElement("input"); inp.className = "f-input";701 g.append(l, inp); return { g, inp };702 }703704 705 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";706 body.appendChild(tg.g);707708 709 const ug = fGroup("website");710 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";711 if (loginSubdomain) {712 const h = document.createElement("div"); h.className = "url-hint";713 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);714 }715 body.appendChild(ug.g);716717 718 const xg = fGroup("username / email");719 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";720 xg.inp.setAttribute("autocomplete", "off");721 body.appendChild(xg.g);722723 724 const pg = fGroup("password");725 const pwInp = document.createElement("input"); pwInp.className = "f-input";726 pwInp.type = "text"; pwInp.value = prePassword || generatePassword();727 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';728 pwInp.style.letterSpacing = "0.04em";729 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");730 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";731 genBtn.innerHTML = genSvgStr() + " regen";732 genBtn.addEventListener("click", () => { pwInp.value = generatePassword(); });733 const pwRow = document.createElement("div"); pwRow.className = "f-row";734 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);735 body.appendChild(pg.g);736737 738 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";739 saveBtn.textContent = "Save entry";740 saveBtn.addEventListener("click", async () => {741 const title = tg.inp.value.trim();742 const pw = pwInp.value.trim();743 const username = xg.inp.value.trim();744 if (!title || !pw) {745 if (!title) { tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus(); }746 else { pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus(); }747 return;748 }749 saveBtn.disabled = true; saveBtn.textContent = "Saving…";750 try {751 const res = await chrome.runtime.sendMessage({752 type: "ADD_ENTRY",753 payload: { entry: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" } },754 });755 if ((res as { ok?: boolean }).ok) {756 757 if (options.isSignup && options.onFillFields) {758 options.onFillFields(username, pw);759 }760 showSuccess(title);761 } else {762 saveBtn.disabled = false; saveBtn.textContent = "Save entry";763 }764 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }765 });766 body.appendChild(saveBtn);767768 wrap.append(head, body);769 container.appendChild(wrap);770 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });771 }772773 function showSuccess(title: string) {774 currentMode = "success";775 container.innerHTML = "";776 const wrap = document.createElement("div"); wrap.className = "success-wrap";777 const ring = document.createElement("div"); ring.className = "success-ring";778 ring.innerHTML = checkSvgStr();779 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;780 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;781 wrap.append(ring, t, s);782 container.appendChild(wrap);783 setTimeout(() => dismiss(), 1600);784 }785786 function renderLocked() {787 container.innerHTML = "";788789 790 const domHdr = document.createElement("div");791 domHdr.className = "domain-header";792 const eyebrow = document.createElement("div");793 eyebrow.className = "domain-eyebrow";794 eyebrow.textContent = "vault locked";795 const domName = document.createElement("div");796 domName.className = "domain-name";797 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");798 domHdr.append(eyebrow, domName);799 container.appendChild(domHdr);800801 const wrap = document.createElement("div");802 wrap.className = "locked-wrap";803804 805 const head = document.createElement("div"); head.className = "locked-head";806 const icon = document.createElement("div"); icon.className = "locked-icon";807 icon.innerHTML = keySvgStr();808 const title = document.createElement("div"); title.className = "locked-title";809 title.textContent = "unlock to autofill";810 head.append(icon, title);811812 813 const pwInp = document.createElement("input");814 pwInp.className = "locked-input";815 pwInp.type = "password";816 pwInp.placeholder = "master password";817 pwInp.setAttribute("autocomplete", "current-password");818819 820 const errEl = document.createElement("div");821 errEl.className = "locked-error";822 errEl.style.display = "none";823824 825 const unlockBtn = document.createElement("button");826 unlockBtn.className = "locked-btn";827 unlockBtn.type = "button";828 unlockBtn.textContent = "unlock";829830 async function tryUnlock() {831 const pw = pwInp.value.trim();832 if (!pw) { pwInp.focus(); return; }833 unlockBtn.disabled = true;834 unlockBtn.textContent = "unlocking…";835 errEl.style.display = "none";836 try {837 const res = await chrome.runtime.sendMessage({838 type: "UNLOCK",839 payload: { password: pw },840 });841 if ((res as { ok?: boolean }).ok) {842 dismiss();843 options.onUnlocked?.();844 } else {845 errEl.textContent = "incorrect password";846 errEl.style.display = "block";847 pwInp.value = "";848 pwInp.focus();849 unlockBtn.disabled = false;850 unlockBtn.textContent = "unlock";851 }852 } catch {853 errEl.textContent = "could not reach Peach";854 errEl.style.display = "block";855 unlockBtn.disabled = false;856 unlockBtn.textContent = "unlock";857 }858 }859860 pwInp.addEventListener("keydown", (e) => { if (e.key === "Enter") void tryUnlock(); });861 unlockBtn.addEventListener("click", () => void tryUnlock());862863 wrap.append(head, pwInp, errEl, unlockBtn);864 container.appendChild(wrap);865 requestAnimationFrame(() => pwInp.focus());866 }867868 function handleKeyDown(e: KeyboardEvent) {869 if (options.locked) {870 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }871 return;872 }873 if (currentMode === "create" || currentMode === "success") {874 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }875 return;876 }877 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");878 const count = allEntryEls.length;879 if (document.activeElement === searchInput) {880 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(true); return; }881 if (count === 0) return;882 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;883 }884 if (count === 0) return;885 switch (e.key) {886 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;887 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;888 case "Enter": {889 if (focusedIndex >= 0) {890 const el = allEntryEls[focusedIndex] as HTMLElement;891 const id = el.dataset.id;892 if (id) { onSelect(id); dismiss(); }893 else if (el.classList.contains("is-new") || !id) {894 895 showCreateForm();896 }897 }898 break;899 }900 case "Escape": case "Tab": e.preventDefault(); dismiss(true); break;901 }902 }903904 document.addEventListener("keydown", handleKeyDown);905906 document.body.appendChild(host);907 if (options.locked) {908 renderLocked();909 } else {910 renderList();911 }912913 function updatePosition() {914 const r = anchorEl.getBoundingClientRect();915 host.style.top = `${r.bottom + window.scrollY + 5}px`;916 host.style.left = `${r.left + window.scrollX}px`;917 host.style.width = `${Math.max(r.width, 260)}px`;918 }919 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);920921 function dismiss(userAction = false) {922 stopObserving();923 host.remove();924 document.removeEventListener("click", outsideClick, { capture: true });925 document.removeEventListener("keydown", handleKeyDown);926 if (userAction) options.onDismiss?.();927 }928929 function outsideClick(ev: MouseEvent) {930 const path = ev.composedPath();931 932 933 934 if (!path.includes(host) && !path.includes(anchorEl)) dismiss(true);935 }936 937 setTimeout(() => document.addEventListener("click", outsideClick, { capture: true }), 0);938939 return dismiss;940}