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}4041function getDomainFromUrl(url: string): string {42 try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }43}4445function keySvgStr(): string {46 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>`;47}4849function plusSvgStr(): string {50 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>`;51}5253function peachIconSvg(): string {54 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>`;55}5657function genSvgStr(): string {58 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>`;59}6061function checkSvgStr(): string {62 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>`;63}6465function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {66 const { hostname, origin } = location;67 const parts = hostname.split(".");68 const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",69 "id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",70 "oidc", "federation", "saml", "cas", "ldap"];71 const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());72 const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;73 const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);74 return { origin, title, loginSubdomain };75}76777879808182export function showDomainConfirmToast(83 pageDomain: string,84 entryId: string,85): void {86 const host = document.createElement("div");87 host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";88 host.style.setProperty("--peach-accent", accentHex);89 const shadow = host.attachShadow({ mode: "closed" });9091 const style = document.createElement("style");92 style.textContent = `93 .bar {94 display:flex;align-items:center;gap:10px;padding:10px 14px;95 background:#131109;border:1px solid rgba(240,232,216,0.09);96 border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);97 font-family:Inter,sans-serif;white-space:nowrap;98 animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;99 }100 @keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }101 .txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }102 .yes {103 padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;104 background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;105 }106 .yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }107 .no {108 padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;109 background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;110 }111 `;112 shadow.appendChild(style);113114 const bar = document.createElement("div"); bar.className = "bar";115 const txt = document.createElement("span"); txt.className = "txt";116 txt.textContent = `Set ${pageDomain} as default for this entry?`;117 const yesBtn = document.createElement("button"); yesBtn.className = "yes";118 yesBtn.textContent = "Set default";119 yesBtn.addEventListener("click", async () => {120 await chrome.runtime.sendMessage({121 type: "UPDATE_ENTRY",122 payload: { id: entryId, patch: { url: location.origin } },123 });124 host.remove();125 });126 const noBtn = document.createElement("button"); noBtn.className = "no";127 noBtn.textContent = "Skip";128 noBtn.addEventListener("click", () => host.remove());129 bar.append(txt, yesBtn, noBtn);130 shadow.appendChild(bar);131 document.body.appendChild(host);132 setTimeout(() => host.remove(), 9000);133}134135function generatePassword(): string {136 const u = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", l = "abcdefghijklmnopqrstuvwxyz";137 const n = "0123456789", s = "!@#$%^&*";138 const all = u + l + n + s;139 const arr = new Uint8Array(20);140 crypto.getRandomValues(arr);141 const res = [u[arr[0]! % u.length]!, l[arr[1]! % l.length]!, n[arr[2]! % n.length]!, s[arr[3]! % s.length]!,142 ...Array.from(arr.slice(4), b => all[b % all.length]!)];143 for (let i = res.length - 1; i > 0; i--) {144 const j = arr[i % arr.length]! % (i + 1);145 [res[i], res[j]] = [res[j]!, res[i]!];146 }147 return res.join("");148}149150export function observeAnchorPosition(151 anchorEl: HTMLElement,152 onUpdate: () => void153): () => void {154 let rafId = 0;155 let burstUntil = 0;156157 const runUpdate = () => {158 rafId = 0;159 onUpdate();160 if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);161 };162 const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };163 const scheduleBurst = (ms = 220) => {164 burstUntil = Math.max(burstUntil, performance.now() + ms);165 scheduleUpdate();166 };167168 const resizeObserver = new ResizeObserver(() => scheduleBurst());169 resizeObserver.observe(anchorEl);170 if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);171172 const mutationObserver = new MutationObserver(() => scheduleBurst());173 mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });174 if (anchorEl.parentElement) {175 mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });176 }177178 const onScroll = () => scheduleUpdate();179 window.addEventListener("scroll", onScroll, true);180 window.addEventListener("resize", onScroll);181 window.visualViewport?.addEventListener("resize", onScroll);182 window.visualViewport?.addEventListener("scroll", onScroll);183 anchorEl.addEventListener("focus", () => scheduleBurst());184 anchorEl.addEventListener("input", () => scheduleBurst());185186 scheduleBurst(260);187188 return () => {189 if (rafId) cancelAnimationFrame(rafId);190 resizeObserver.disconnect();191 mutationObserver.disconnect();192 window.removeEventListener("scroll", onScroll, true);193 window.removeEventListener("resize", onScroll);194 window.visualViewport?.removeEventListener("resize", onScroll);195 window.visualViewport?.removeEventListener("scroll", onScroll);196 };197}198199export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {200 const button = document.createElement("button");201 button.type = "button";202 button.setAttribute("aria-label", "Peach autofill");203 button.title = "Fill with Peach";204 button.innerHTML = peachIconSvg();205 button.style.cssText = `206 position: fixed; z-index: 2147483646;207 width: 26px; height: 26px; cursor: pointer;208 pointer-events: auto; border: none; padding: 3px;209 display: inline-flex; align-items: center; justify-content: center;210 background: transparent; border-radius: 999px;211 transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;212 opacity: 0;213 `;214 button.addEventListener("mouseenter", () => {215 button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;216 button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;217 button.style.transform = "scale(1.08)";218 });219 button.addEventListener("mouseleave", () => {220 button.style.background = "transparent";221 button.style.boxShadow = "none";222 button.style.transform = "none";223 });224 button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });225 button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });226227 document.body.appendChild(button);228229 function updatePosition() {230 const rect = field.getBoundingClientRect();231 if (rect.width === 0 || rect.height === 0) {232 button.style.opacity = "0"; button.style.pointerEvents = "none";233 return;234 }235 const sz = 26;236 const inset = Math.max(5, Math.min(10, rect.height * 0.22));237 const top = rect.top + rect.height / 2 - sz / 2;238 239 240 const minLeft = rect.left + 4;241 const left = Math.max(minLeft, rect.right - sz - inset);242 243 if (rect.width < 40) {244 button.style.opacity = "0"; button.style.pointerEvents = "none";245 return;246 }247 button.style.top = `${top}px`;248 button.style.left = `${left}px`;249 button.style.opacity = "1"; button.style.pointerEvents = "auto";250 }251 updatePosition();252 requestAnimationFrame(() => { button.style.opacity = "1"; });253 const stopObserving = observeAnchorPosition(field, updatePosition);254 return () => { stopObserving(); button.remove(); };255}256257258const SELECTOR_STYLES = `259 :host { all: initial; }260 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }261 .selector {262 background: #0f0e0b;263 border: 1px solid rgba(240,232,216,0.10);264 border-radius: 13px;265 overflow: hidden;266 box-shadow: 0 4px 16px rgba(0,0,0,0.45), 0 16px 48px rgba(0,0,0,0.7);267 font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;268 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;269 }270 @keyframes sl-in {271 from { opacity:0; transform:translateY(-6px) scale(0.97) }272 to { opacity:1; transform:none }273 }274275 /* Domain header */276 .domain-header {277 padding: 9px 13px 7px;278 border-bottom: 1px solid rgba(240,232,216,0.055);279 display: flex; align-items: center; justify-content: space-between;280 }281 .domain-eyebrow {282 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;283 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;284 }285 .domain-name {286 font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);287 font-family: "IBM Plex Mono", monospace;288 }289290 /* Search */291 .search-wrap {292 padding: 9px 12px 7px;293 border-bottom: 1px solid rgba(240,232,216,0.04);294 }295 .search {296 width: 100%; background: rgba(240,232,216,0.03);297 border: 1px solid rgba(240,232,216,0.08);298 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;299 font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;300 transition: border-color 120ms, box-shadow 120ms;301 }302 .search:focus {303 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);304 box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);305 }306 .search::placeholder { color: rgba(240,232,216,0.2); }307308 /* Results */309 .results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }310 .results::-webkit-scrollbar { display: none; }311312 /* Entry rows */313 .entry {314 display: flex; align-items: center; padding: 8px 12px;315 cursor: pointer; border-bottom: 1px solid rgba(240,232,216,0.04);316 transition: background 70ms; position: relative; outline: none;317 }318 .entry:last-child { border-bottom: none; }319 .entry:hover { background: rgba(240,232,216,0.04); }320 .entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }321 .entry.focused::before {322 content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;323 width: 2px; border-radius: 2px; background: color-mix(in srgb, var(--peach-accent) 55%, transparent);324 }325326 .entry-icon {327 width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;328 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.07);329 display: flex; align-items: center; justify-content: center;330 overflow: hidden; margin-right: 10px;331 }332 .entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }333 .entry-icon.is-new {334 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);335 border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);336 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);337 }338339 .entry-info { flex: 1; min-width: 0; }340 .entry-title {341 font-size: 12.5px; color: #F0E8D8;342 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;343 line-height: 1.3;344 }345 .entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }346 .entry-sub {347 font-size: 10px; color: rgba(240,232,216,0.32);348 font-family: "IBM Plex Mono", monospace;349 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;350 margin-top: 1px;351 }352353 .totp-badge {354 font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;355 border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);356 color: color-mix(in srgb, var(--peach-accent) 75%, transparent); font-family: "IBM Plex Mono", monospace;357 flex-shrink: 0; margin-left: 8px;358 }359360 .divider { height: 1px; background: rgba(240,232,216,0.05); margin: 2px 0; }361362 .empty-hint {363 padding: 14px 13px; font-size: 11px; color: rgba(240,232,216,0.28);364 font-family: "IBM Plex Mono", monospace;365 }366367 /* ── Create form ─────────────────────────────────────── */368 .create-form-wrap {369 animation: cf-in 180ms cubic-bezier(0.16,1,0.3,1) both;370 }371 @keyframes cf-in {372 from { opacity:0; transform:translateX(8px) }373 to { opacity:1; transform:none }374 }375 .create-head {376 display: flex; align-items: center; gap: 8px;377 padding: 10px 12px; border-bottom: 1px solid rgba(240,232,216,0.055);378 }379 .back-btn {380 background: transparent; border: none; cursor: pointer;381 color: rgba(240,232,216,0.38); padding: 3px; border-radius: 5px;382 display: flex; align-items: center; line-height: 0;383 transition: color 100ms, background 100ms;384 }385 .back-btn:hover { color: rgba(240,232,216,0.75); background: rgba(240,232,216,0.05); }386 .create-label { font-size: 12px; color: rgba(240,232,216,0.65); font-weight: 500; flex: 1; }387 .create-body { padding: 12px; display: grid; gap: 9px; }388 .f-group { display: grid; gap: 3px; }389 .f-label {390 font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;391 color: rgba(240,232,216,0.28); font-family: "IBM Plex Mono", monospace;392 }393 .f-input {394 background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);395 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;396 font-size: 11px; font-family: "IBM Plex Mono", monospace;397 outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;398 }399 .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); }400 .f-input::placeholder { color: rgba(240,232,216,0.18); }401 .f-input.url-f { font-size: 10px; color: rgba(240,232,216,0.5); letter-spacing: 0.01em; }402 .f-row { display: flex; gap: 6px; }403 .f-row .f-input { flex: 1; min-width: 0; }404 .gen-btn {405 padding: 0 10px; border-radius: 7px; border: none;406 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);407 color: color-mix(in srgb, var(--peach-accent) 80%, transparent);408 font-size: 10px; font-family: "IBM Plex Mono", monospace;409 cursor: pointer; white-space: nowrap; flex-shrink: 0;410 display: flex; align-items: center; gap: 4px;411 transition: background 100ms;412 }413 .gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }414 .save-btn {415 width: 100%; padding: 8px; border-radius: 8px; border: none;416 background: var(--peach-accent); color: #fff; font-size: 12px;417 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;418 transition: background 120ms; letter-spacing: 0.01em;419 }420 .save-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }421 .save-btn:disabled { opacity: 0.4; cursor: not-allowed; }422 .url-hint {423 font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);424 font-family: "IBM Plex Mono", monospace; font-style: italic;425 }426427 /* ── Success ─────────────────────────────────────────── */428 .success-wrap {429 display: flex; flex-direction: column; align-items: center;430 padding: 24px 16px 20px; gap: 8px;431 animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;432 }433 @keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }434 .success-ring {435 width: 40px; height: 40px; border-radius: 50%;436 background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);437 display: flex; align-items: center; justify-content: center;438 }439 .success-text { font-size: 12.5px; color: #F0E8D8; font-weight: 500; }440 .success-sub { font-size: 10px; color: rgba(240,232,216,0.3); font-family:"IBM Plex Mono",monospace; }441442 /* ── Locked state ────────────────────────────────────── */443 .locked-wrap {444 display: flex; flex-direction: column; align-items: stretch;445 padding: 14px 13px 13px; gap: 9px;446 animation: sl-in 180ms cubic-bezier(0.16,1,0.3,1) both;447 }448 .locked-head {449 display: flex; align-items: center; gap: 8px; padding-bottom: 2px;450 }451 .locked-icon {452 width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;453 background: rgba(240,232,216,0.04); border: 1px solid rgba(240,232,216,0.08);454 display: flex; align-items: center; justify-content: center;455 }456 .locked-title {457 font-size: 11.5px; color: rgba(240,232,216,0.55); font-weight: 500;458 }459 .locked-input {460 width: 100%; background: rgba(240,232,216,0.035); border: 1px solid rgba(240,232,216,0.09);461 border-radius: 7px; color: #F0E8D8; padding: 7px 10px;462 font-size: 11px; font-family: "IBM Plex Mono", monospace;463 outline: none; transition: border-color 120ms, box-shadow 120ms;464 }465 .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); }466 .locked-input::placeholder { color: rgba(240,232,216,0.2); }467 .locked-btn {468 width: 100%; padding: 8px; border-radius: 8px; border: none;469 background: var(--peach-accent); color: #fff; font-size: 12px;470 font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;471 transition: background 120ms; letter-spacing: 0.01em;472 }473 .locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }474 .locked-btn:disabled { opacity: 0.4; cursor: not-allowed; }475 .locked-error {476 font-size: 10px; color: rgba(220,80,60,0.85);477 font-family: "IBM Plex Mono", monospace;478 }479`;480481export function showAutofillSelector(482 anchorEl: HTMLInputElement,483 entries: SelectorEntry[],484 onSelect: (id: string) => void,485 options: SelectorOptions = {}486): () => void {487 const host = document.createElement("div");488 host.style.cssText = "position: absolute; z-index: 2147483647;";489 host.style.setProperty("--peach-accent", accentHex);490 const shadow = host.attachShadow({ mode: "closed" });491492 const style = shadow.appendChild(document.createElement("style"));493 style.textContent = SELECTOR_STYLES;494495 const rect = anchorEl.getBoundingClientRect();496 host.style.top = `${rect.bottom + window.scrollY + 5}px`;497 host.style.left = `${rect.left + window.scrollX}px`;498 host.style.width = `${Math.max(rect.width, 260)}px`;499500 const container = shadow.appendChild(document.createElement("div"));501 container.className = "selector";502503 const searchableEntries = options.searchableEntries ?? [];504 const { origin: sOrigin, title: sTitle, loginSubdomain } = smartUrl();505506 let query = "";507 let focusedIndex = -1;508 let currentMode: "list" | "create" | "success" = "list";509 let searchInput: HTMLInputElement | null = null;510511 512 const preUsername = (anchorEl.type !== "password") ? (anchorEl.value?.trim() ?? "") : "";513 const pwInForm = anchorEl.closest("form")?.querySelector<HTMLInputElement>("input[type='password']");514 const prePassword = pwInForm?.value?.trim() ?? (anchorEl.type === "password" ? anchorEl.value?.trim() : "") ?? "";515516 function uniqueEntries(list: SelectorEntry[]): SelectorEntry[] {517 const seen = new Set<string>();518 return list.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });519 }520521 function filterEntries(source: SelectorEntry[], value: string): SelectorEntry[] {522 const q = value.trim().toLowerCase();523 const deduped = uniqueEntries(source);524 if (!q) return deduped.slice(0, 8);525 return deduped.filter(e =>526 [e.title, e.username ?? "", e.url ?? ""].join(" ").toLowerCase().includes(q)527 ).slice(0, 12);528 }529530 function activeEntries(): SelectorEntry[] {531 532 if (query.trim()) {533 const merged = uniqueEntries([...entries, ...searchableEntries]);534 return filterEntries(merged, query);535 }536 537 if (entries.length > 0) return entries;538 return filterEntries(searchableEntries, "").slice(0, 8);539 }540541 function makeEntryIcon(url?: string, isNew = false): HTMLDivElement {542 const icon = document.createElement("div");543 icon.className = `entry-icon${isNew ? " is-new" : ""}`;544 if (isNew) {545 icon.innerHTML = plusSvgStr();546 } else {547 548 549 550 void url;551 icon.innerHTML = keySvgStr();552 }553 return icon;554 }555556 function renderList() {557 container.innerHTML = "";558 focusedIndex = -1;559 searchInput = null;560561 const current = activeEntries();562563 564 const domHdr = document.createElement("div");565 domHdr.className = "domain-header";566 const eyebrow = document.createElement("div");567 eyebrow.className = "domain-eyebrow";568 eyebrow.textContent = options.isSignup569 ? "Sign up as"570 : entries.length > 0 ? "Saved for this site" : "No saved login";571 const domName = document.createElement("div");572 domName.className = "domain-name";573 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");574 domHdr.append(eyebrow, domName);575 container.appendChild(domHdr);576577 578 const sw = document.createElement("div"); sw.className = "search-wrap";579 searchInput = document.createElement("input");580 searchInput.className = "search";581 searchInput.placeholder = entries.length > 0 ? "search for another entry…" : "search vault…";582 searchInput.value = query;583 searchInput.addEventListener("input", () => {584 query = searchInput?.value ?? "";585 renderList();586 searchInput?.focus();587 searchInput?.setSelectionRange(query.length, query.length);588 });589 sw.appendChild(searchInput);590 container.appendChild(sw);591592 593 const results = document.createElement("div");594 results.className = "results";595 container.appendChild(results);596597 if (current.length === 0 && entries.length > 0) {598 599 }600601 for (let i = 0; i < current.length; i++) {602 const e = current[i]!;603 const entryEl = document.createElement("div");604 entryEl.className = "entry";605 entryEl.dataset.id = e.id;606 entryEl.dataset.index = String(i);607 entryEl.setAttribute("tabindex", "-1");608609 entryEl.appendChild(makeEntryIcon(e.url));610611 const info = document.createElement("div"); info.className = "entry-info";612 const titleEl = document.createElement("div"); titleEl.className = "entry-title"; titleEl.textContent = e.title;613 const subEl = document.createElement("div"); subEl.className = "entry-sub";614 const domain = e.url ? getDomainFromUrl(e.url) : "";615 subEl.textContent = domain || e.username || "";616 info.append(titleEl, subEl);617 entryEl.appendChild(info);618619 if (e.hasTotp) {620 const badge = document.createElement("span"); badge.className = "totp-badge"; badge.textContent = "TOTP";621 entryEl.appendChild(badge);622 }623624 entryEl.addEventListener("click", () => {625 if (options.isSignup && options.onFillFields) {626 627 options.onFillFields(e.username ?? "", "");628 dismiss();629 } else {630 onSelect(e.id);631 dismiss();632 }633 });634 results.appendChild(entryEl);635 }636637 638 if (current.length > 0) {639 results.appendChild(Object.assign(document.createElement("div"), { className: "divider" }));640 }641642 643 const newEl = document.createElement("div");644 newEl.className = "entry";645 newEl.setAttribute("tabindex", "-1");646 newEl.appendChild(makeEntryIcon(undefined, true));647648 const newInfo = document.createElement("div"); newInfo.className = "entry-info";649 const newTitle = document.createElement("div");650 newTitle.className = "entry-title is-new";651 newTitle.textContent = `New entry for ${sTitle}`;652 const newSub = document.createElement("div");653 newSub.className = "entry-sub";654 newSub.textContent = sOrigin;655 newInfo.append(newTitle, newSub);656 newEl.appendChild(newInfo);657 newEl.addEventListener("click", showCreateForm);658 results.appendChild(newEl);659660 661 if (searchInput) {662 requestAnimationFrame(() => searchInput?.focus());663 }664 }665666 function setFocused(idx: number) {667 const all = container.querySelectorAll<HTMLElement>(".entry");668 all.forEach((el, i) => el.classList.toggle("focused", i === idx));669 focusedIndex = idx;670 if (idx >= 0) (all[idx] as HTMLElement).scrollIntoView({ block: "nearest" });671 }672673 function showCreateForm() {674 currentMode = "create";675 container.innerHTML = "";676677 const wrap = document.createElement("div");678 wrap.className = "create-form-wrap";679680 681 const head = document.createElement("div"); head.className = "create-head";682 const backBtn = document.createElement("button"); backBtn.className = "back-btn";683 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>`;684 backBtn.addEventListener("click", () => { currentMode = "list"; renderList(); });685 const lbl = document.createElement("div"); lbl.className = "create-label"; lbl.textContent = "New entry";686 head.append(backBtn, lbl);687688 689 const body = document.createElement("div"); body.className = "create-body";690691 function fGroup(label: string): { g: HTMLDivElement; inp: HTMLInputElement } {692 const g = document.createElement("div"); g.className = "f-group";693 const l = document.createElement("div"); l.className = "f-label"; l.textContent = label;694 const inp = document.createElement("input"); inp.className = "f-input";695 g.append(l, inp); return { g, inp };696 }697698 699 const tg = fGroup("title"); tg.inp.value = sTitle; tg.inp.placeholder = "e.g. Google";700 body.appendChild(tg.g);701702 703 const ug = fGroup("website");704 ug.inp.className = "f-input url-f"; ug.inp.value = sOrigin; ug.inp.placeholder = "https://";705 if (loginSubdomain) {706 const h = document.createElement("div"); h.className = "url-hint";707 h.textContent = "⊘ Recognized sign-in subdomain"; ug.g.appendChild(h);708 }709 body.appendChild(ug.g);710711 712 const xg = fGroup("username / email");713 xg.inp.value = preUsername; xg.inp.placeholder = "[email protected]";714 xg.inp.setAttribute("autocomplete", "off");715 body.appendChild(xg.g);716717 718 const pg = fGroup("password");719 const pwInp = document.createElement("input"); pwInp.className = "f-input";720 pwInp.type = "text"; pwInp.value = prePassword || generatePassword();721 pwInp.style.fontFamily = '"IBM Plex Mono", monospace';722 pwInp.style.letterSpacing = "0.04em";723 pwInp.setAttribute("autocomplete", "off"); pwInp.setAttribute("spellcheck", "false");724 const genBtn = document.createElement("button"); genBtn.className = "gen-btn"; genBtn.type = "button";725 genBtn.innerHTML = genSvgStr() + " regen";726 genBtn.addEventListener("click", () => { pwInp.value = generatePassword(); });727 const pwRow = document.createElement("div"); pwRow.className = "f-row";728 pwRow.append(pwInp, genBtn); pg.g.appendChild(pwRow);729 body.appendChild(pg.g);730731 732 const saveBtn = document.createElement("button"); saveBtn.className = "save-btn"; saveBtn.type = "button";733 saveBtn.textContent = "Save entry";734 saveBtn.addEventListener("click", async () => {735 const title = tg.inp.value.trim();736 const pw = pwInp.value.trim();737 const username = xg.inp.value.trim();738 if (!title || !pw) {739 if (!title) { tg.inp.style.borderColor = "rgba(220,70,60,0.5)"; tg.inp.focus(); }740 else { pwInp.style.borderColor = "rgba(220,70,60,0.5)"; pwInp.focus(); }741 return;742 }743 saveBtn.disabled = true; saveBtn.textContent = "Saving…";744 try {745 const res = await chrome.runtime.sendMessage({746 type: "ADD_ENTRY",747 payload: { entry: { title, url: ug.inp.value.trim(), username, password: pw, type: "login" } },748 });749 if ((res as { ok?: boolean }).ok) {750 751 if (options.isSignup && options.onFillFields) {752 options.onFillFields(username, pw);753 }754 showSuccess(title);755 } else {756 saveBtn.disabled = false; saveBtn.textContent = "Save entry";757 }758 } catch { saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }759 });760 body.appendChild(saveBtn);761762 wrap.append(head, body);763 container.appendChild(wrap);764 requestAnimationFrame(() => { tg.inp.focus(); tg.inp.select(); });765 }766767 function showSuccess(title: string) {768 currentMode = "success";769 container.innerHTML = "";770 const wrap = document.createElement("div"); wrap.className = "success-wrap";771 const ring = document.createElement("div"); ring.className = "success-ring";772 ring.innerHTML = checkSvgStr();773 const t = document.createElement("div"); t.className = "success-text"; t.textContent = `${title} saved`;774 const s = document.createElement("div"); s.className = "success-sub"; s.textContent = sOrigin;775 wrap.append(ring, t, s);776 container.appendChild(wrap);777 setTimeout(() => dismiss(), 1600);778 }779780 function renderLocked() {781 container.innerHTML = "";782783 784 const domHdr = document.createElement("div");785 domHdr.className = "domain-header";786 const eyebrow = document.createElement("div");787 eyebrow.className = "domain-eyebrow";788 eyebrow.textContent = "vault locked";789 const domName = document.createElement("div");790 domName.className = "domain-name";791 domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");792 domHdr.append(eyebrow, domName);793 container.appendChild(domHdr);794795 const wrap = document.createElement("div");796 wrap.className = "locked-wrap";797798 799 const head = document.createElement("div"); head.className = "locked-head";800 const icon = document.createElement("div"); icon.className = "locked-icon";801 icon.innerHTML = keySvgStr();802 const title = document.createElement("div"); title.className = "locked-title";803 title.textContent = "unlock to autofill";804 head.append(icon, title);805806 807 const pwInp = document.createElement("input");808 pwInp.className = "locked-input";809 pwInp.type = "password";810 pwInp.placeholder = "master password";811 pwInp.setAttribute("autocomplete", "current-password");812813 814 const errEl = document.createElement("div");815 errEl.className = "locked-error";816 errEl.style.display = "none";817818 819 const unlockBtn = document.createElement("button");820 unlockBtn.className = "locked-btn";821 unlockBtn.type = "button";822 unlockBtn.textContent = "unlock";823824 async function tryUnlock() {825 const pw = pwInp.value.trim();826 if (!pw) { pwInp.focus(); return; }827 unlockBtn.disabled = true;828 unlockBtn.textContent = "unlocking…";829 errEl.style.display = "none";830 try {831 const res = await chrome.runtime.sendMessage({832 type: "UNLOCK",833 payload: { password: pw },834 });835 if ((res as { ok?: boolean }).ok) {836 dismiss();837 options.onUnlocked?.();838 } else {839 errEl.textContent = "incorrect password";840 errEl.style.display = "block";841 pwInp.value = "";842 pwInp.focus();843 unlockBtn.disabled = false;844 unlockBtn.textContent = "unlock";845 }846 } catch {847 errEl.textContent = "could not reach Peach";848 errEl.style.display = "block";849 unlockBtn.disabled = false;850 unlockBtn.textContent = "unlock";851 }852 }853854 pwInp.addEventListener("keydown", (e) => { if (e.key === "Enter") void tryUnlock(); });855 unlockBtn.addEventListener("click", () => void tryUnlock());856857 wrap.append(head, pwInp, errEl, unlockBtn);858 container.appendChild(wrap);859 requestAnimationFrame(() => pwInp.focus());860 }861862 function handleKeyDown(e: KeyboardEvent) {863 if (options.locked) {864 if (e.key === "Escape") { e.preventDefault(); dismiss(); }865 return;866 }867 if (currentMode === "create" || currentMode === "success") {868 if (e.key === "Escape") { e.preventDefault(); dismiss(); }869 return;870 }871 const allEntryEls = container.querySelectorAll(".entry:not(.empty-state)");872 const count = allEntryEls.length;873 if (document.activeElement === searchInput) {874 if (e.key === "Escape" || e.key === "Tab") { e.preventDefault(); dismiss(); return; }875 if (count === 0) return;876 if (!["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return;877 }878 if (count === 0) return;879 switch (e.key) {880 case "ArrowDown": e.preventDefault(); setFocused(focusedIndex < count - 1 ? focusedIndex + 1 : 0); break;881 case "ArrowUp": e.preventDefault(); setFocused(focusedIndex > 0 ? focusedIndex - 1 : count - 1); break;882 case "Enter": {883 if (focusedIndex >= 0) {884 const el = allEntryEls[focusedIndex] as HTMLElement;885 const id = el.dataset.id;886 if (id) { onSelect(id); dismiss(); }887 else if (el.classList.contains("is-new") || !id) {888 889 showCreateForm();890 }891 }892 break;893 }894 case "Escape": case "Tab": e.preventDefault(); dismiss(); break;895 }896 }897898 document.addEventListener("keydown", handleKeyDown);899900 document.body.appendChild(host);901 if (options.locked) {902 renderLocked();903 } else {904 renderList();905 }906907 function updatePosition() {908 const r = anchorEl.getBoundingClientRect();909 host.style.top = `${r.bottom + window.scrollY + 5}px`;910 host.style.left = `${r.left + window.scrollX}px`;911 host.style.width = `${Math.max(r.width, 260)}px`;912 }913 const stopObserving = observeAnchorPosition(anchorEl, updatePosition);914915 function dismiss() {916 stopObserving();917 host.remove();918 document.removeEventListener("click", outsideClick);919 document.removeEventListener("keydown", handleKeyDown);920 }921922 function outsideClick(ev: MouseEvent) {923 const path = ev.composedPath();924 925 926 927 if (!path.includes(host) && !path.includes(anchorEl)) dismiss();928 }929 setTimeout(() => document.addEventListener("click", outsideClick), 0);930931 return dismiss;932}