1import { createEffect, createMemo, createSignal, lazy, onCleanup, onMount, Show, Suspense } from "solid-js";2import type { PasskeyEntry, PendingPasskeyRequest, VaultEntry } from "../shared/types";3import { LockView } from "./views/Lock";4import { VaultView, type SortOrder } from "./views/Vault";5import { EntryView } from "./views/Entry";6import { AddView } from "./views/Add";7import { PeachWordmark } from "./components/PeachMark";8import { PasskeyApprovalView } from "./views/PasskeyApproval";9import { PasskeyEntryView } from "./views/PasskeyEntry";10import { extractDomain } from "../shared/domain";1112const ImportView = lazy(() => import("./views/Import").then(m => ({ default: m.ImportView })));13const ExportView = lazy(() => import("./views/Export").then(m => ({ default: m.ExportView })));14const CJKExport = lazy(() => import("./views/CJKExport").then(m => ({ default: m.CJKExportView })));15const CJKImport = lazy(() => import("./views/CJKImport").then(m => ({ default: m.CJKImportView })));16const SettingsView = lazy(() => import("./views/Settings").then(m => ({ default: m.SettingsView })));17const HealthView = lazy(() => import("./views/Health").then(m => ({ default: m.HealthView })));18const TrashView = lazy(() => import("./views/TrashView").then(m => ({ default: m.TrashView })));1920type View =21 | "lock"22 | "entry"23 | "list"24 | "add"25 | "settings"26 | "import"27 | "export"28 | "health"29 | "trash";30type BootState = "loading" | "ready";3132const canarySuppressMap = new Map<string, boolean>();33let shellRingAnimation: Animation | null = null;34let totpRingResetTimer: number | null = null;3536export function isCanarySuppressed(entryId: string): boolean {37 return canarySuppressMap.get(entryId) ?? false;38}3940export function setCanarySuppressed(entryId: string, suppressed: boolean): void {41 canarySuppressMap.set(entryId, suppressed);42}4344export function clearCanarySuppressions(): void {45 canarySuppressMap.clear();46}4748export function triggerRing() {49 const shell = document.getElementById("hd-shell");50 if (!shell) return;5152 shellRingAnimation?.cancel();53 shell.dataset.glowing = "true";54 const animation = shell.animate(55 [56 { offset: 0, boxShadow: "var(--copy-shadow)" },57 {58 offset: 0.3,59 boxShadow:60 "0 2px 8px rgba(0,0,0,0.5), 0 14px 48px rgba(0,0,0,0.6), 0 0 0 1.5px rgba(217,104,48,0.58), 0 0 22px rgba(217,104,48,0.18)",61 },62 { offset: 1, boxShadow: "var(--copy-shadow)" },63 ],64 {65 duration: 720,66 easing: "cubic-bezier(0.4, 0, 0.2, 1)",67 fill: "none",68 }69 );70 shellRingAnimation = animation;71 const clearGlow = () => {72 if (shellRingAnimation !== animation) return;73 shellRingAnimation = null;74 delete shell.dataset.glowing;75 };76 animation.onfinish = clearGlow;77 animation.oncancel = clearGlow;78}7980export function triggerTotpRing() {81 const shell = document.getElementById("hd-shell");82 if (!shell) return;8384 shell.classList.remove("peach-shell--totp-ring");85 void shell.offsetWidth;86 shell.classList.add("peach-shell--totp-ring");8788 if (totpRingResetTimer !== null) {89 window.clearTimeout(totpRingResetTimer);90 }9192 totpRingResetTimer = window.setTimeout(() => {93 shell.classList.remove("peach-shell--totp-ring");94 totpRingResetTimer = null;95 }, 920);96}9798export function triggerPipPulse() {99 const pip = document.querySelector(".peach-status-pip, .pip");100 if (pip) {101 pip.classList.remove("pulse");102 void (pip as HTMLElement).offsetWidth;103 pip.classList.add("pulse");104 }105}106107function humanizeError(error: string | undefined): string | undefined {108 if (!error) return error;109 if (error === "STORAGE_QUOTA_EXCEEDED") {110 return "Peach has run out of browser storage. Delete some entries or clear browser storage to continue.";111 }112 return error;113}114115async function sendMessage(msg: any): Promise<any> {116 const MESSAGE_TIMEOUT_MS = 5000;117118 return new Promise((resolve) => {119 let settled = false;120 const timeoutId = window.setTimeout(() => {121 if (settled) return;122 settled = true;123 resolve({ ok: false, error: "SERVICE_WORKER_UNAVAILABLE" });124 }, MESSAGE_TIMEOUT_MS);125126 try {127 chrome.runtime.sendMessage(msg, (response) => {128 if (settled) return;129 settled = true;130 window.clearTimeout(timeoutId);131132 if (chrome.runtime.lastError) {133 resolve({134 ok: false,135 error:136 chrome.runtime.lastError.message ?? "SERVICE_WORKER_UNAVAILABLE",137 });138 return;139 }140141 if (response === undefined) {142 resolve({ ok: false, error: "SERVICE_WORKER_UNAVAILABLE" });143 return;144 }145146 if (response && response.ok === false) {147 response.error = humanizeError(response.error);148 }149 resolve(response);150 });151 } catch (error) {152 if (settled) return;153 settled = true;154 window.clearTimeout(timeoutId);155 resolve({156 ok: false,157 error:158 error instanceof Error ? error.message : "SERVICE_WORKER_UNAVAILABLE",159 });160 }161 });162}163164export { sendMessage };165166const ENTRY_TRANSITION_EXIT_MS = 95;167const ENTRY_TRANSITION_SETTLE_MS = 135;168169export default function App() {170 const [bootState, setBootState] = createSignal<BootState>("loading");171 const [view, setView] = createSignal<View>("lock");172 const [contentPhase, setContentPhase] = createSignal<"enter" | "exit" | "idle">(173 "enter"174 );175 const [contentTransitioning, setContentTransitioning] = createSignal(false);176 const [hasVault, setHasVault] = createSignal(false);177 const [entries, setEntries] = createSignal<VaultEntry[]>([]);178 const [selectedEntry, setSelectedEntry] = createSignal<VaultEntry | null>(null);179 const [editEntry, setEditEntry] = createSignal<VaultEntry | undefined>(undefined);180 const [accentColor, setAccentColor] = createSignal("#D96830");181 const [theme, setTheme] = createSignal<"dark" | "light">("dark");182 const [lockOnClose, setLockOnClose] = createSignal(true);183 const [autoLockMinutes, setAutoLockMinutes] = createSignal(5);184 const [breachMonitoring, setBreachMonitoring] = createSignal(true);185 const [trashRetentionDays, setTrashRetentionDays] = createSignal(30);186 const [restoreView, setRestoreView] = createSignal<"cjk" | "file" | null>(null);187 const [pendingPasskey, setPendingPasskey] = createSignal<PendingPasskeyRequest | null>(null);188 const [currentTabDomain, setCurrentTabDomain] = createSignal<string>("");189 const [sortOrder, setSortOrder] = createSignal<SortOrder>("url-first");190191 let contentPhaseTimer: number | undefined;192193 createEffect(() => {194 document.documentElement.style.setProperty("--ac", accentColor());195 });196197 createEffect(() => {198 document.documentElement.classList.toggle("light", theme() === "light");199 document.body.classList.toggle("light", theme() === "light");200 });201202 const selectedIndex = createMemo(() => {203 const currentId = selectedEntry()?.id;204 if (!currentId) return entries().length > 0 ? 0 : -1;205 return entries().findIndex((entry) => entry.id === currentId);206 });207208 async function applyTheme(t: "dark" | "light") {209 setTheme(t);210 await sendMessage({ type: "UPDATE_SETTINGS", payload: { theme: t } });211 }212213 async function applyLockOnClose(enabled: boolean) {214 setLockOnClose(enabled);215 await sendMessage({216 type: "UPDATE_SETTINGS",217 payload: { lockOnClose: enabled },218 });219 }220221 async function applyAutoLockMinutes(minutes: number) {222 setAutoLockMinutes(minutes);223 await sendMessage({224 type: "UPDATE_SETTINGS",225 payload: { autoLockMinutes: minutes },226 });227 }228229 async function applyBreachMonitoring(enabled: boolean) {230 setBreachMonitoring(enabled);231 await sendMessage({232 type: "UPDATE_SETTINGS",233 payload: { breachMonitoring: enabled },234 });235 }236237 onMount(async () => {238 const [unlockResponse, vaultResponse] = await Promise.all([239 sendMessage({ type: "IS_UNLOCKED" }),240 sendMessage({ type: "HAS_VAULT" }),241 ]);242 const unlocked = Boolean(243 (unlockResponse as { ok: boolean; data?: boolean }).data244 );245 const vaultExists = Boolean(246 (vaultResponse as { ok: boolean; data?: boolean }).data247 );248 setHasVault(vaultExists);249 await restoreSettings();250251 if (unlocked && vaultExists) {252 await openVault();253 }254255 await refreshPendingPasskey();256 await refreshCurrentTab();257258 259 const handleTabActivated = () => { void refreshCurrentTab(); };260 const handleTabUpdated = (_tabId: number, changeInfo: { url?: string; status?: string }) => {261 if (changeInfo.url || changeInfo.status === "complete") void refreshCurrentTab();262 };263 try {264 chrome.tabs.onActivated.addListener(handleTabActivated);265 chrome.tabs.onUpdated.addListener(handleTabUpdated);266 onCleanup(() => {267 chrome.tabs.onActivated.removeListener(handleTabActivated);268 chrome.tabs.onUpdated.removeListener(handleTabUpdated);269 });270 } catch {271 272 }273274 setBootState("ready");275 document.getElementById("pre-lock")?.setAttribute("hidden", "true");276277 278 279 280 281 void Promise.all([282 import("./views/Settings").catch(() => {}),283 import("./views/Import").catch(() => {}),284 import("./views/Export").catch(() => {}),285 import("./views/Health").catch(() => {}),286 import("./views/TrashView").catch(() => {}),287 import("./views/CJKExport").catch(() => {}),288 import("./views/CJKImport").catch(() => {}),289 ]);290291 const handlePageHide = () => {292 if (!lockOnClose() || view() === "lock") return;293 chrome.runtime.sendMessage({ type: "LOCK" });294 };295296 window.addEventListener("pagehide", handlePageHide);297 onCleanup(() => window.removeEventListener("pagehide", handlePageHide));298299 300 301 const handleRuntimeMessage = (msg: { type?: string }) => {302 if (msg?.type === "VAULT_LOCKED" && view() !== "lock") handleLock();303 };304 chrome.runtime.onMessage.addListener(handleRuntimeMessage);305 onCleanup(() => chrome.runtime.onMessage.removeListener(handleRuntimeMessage));306307 const handleGlobalKeydown = (e: KeyboardEvent) => {308 const v = view();309 const inInput = (e.target as HTMLElement)?.tagName === "INPUT"310 || (e.target as HTMLElement)?.tagName === "TEXTAREA"311 || (e.target as HTMLElement)?.isContentEditable;312313 314 if (e.key === "Escape") {315 if (v === "list") { setView("entry"); return; }316 if (v === "settings" || v === "import" || v === "export" || v === "add" || v === "health") { handleBack(); return; }317 }318319 320 if ((e.key === "/" || (e.ctrlKey && e.key === "f")) && !inInput) {321 e.preventDefault();322 openList();323 return;324 }325326 327 if (e.ctrlKey && e.key === "l") {328 e.preventDefault();329 void handleHeaderLock();330 return;331 }332333 334 if (v === "entry" && !inInput) {335 336 if (e.key === "c") {337 e.preventDefault();338 const entry = selectedEntry();339 if (entry?.password) {340 void sendMessage({ type: "COPY", payload: { text: entry.password, label: "Password" } })341 .then(() => triggerRing());342 }343 return;344 }345 if (e.key === "u") {346 e.preventDefault();347 const entry = selectedEntry();348 if (entry?.username) {349 void sendMessage({ type: "COPY", payload: { text: entry.username, label: "Username" } })350 .then(() => triggerRing());351 }352 return;353 }354 355 if (e.key === "n") {356 e.preventDefault();357 handleAdd();358 return;359 }360 361 if (e.key === "ArrowLeft" || e.key === "ArrowUp") {362 e.preventDefault();363 navigate(-1);364 return;365 }366 if (e.key === "ArrowRight" || e.key === "ArrowDown") {367 e.preventDefault();368 navigate(1);369 return;370 }371 }372 };373 window.addEventListener("keydown", handleGlobalKeydown);374 onCleanup(() => window.removeEventListener("keydown", handleGlobalKeydown));375376 const handleStorageChanged = (377 changes: Record<string, chrome.storage.StorageChange>,378 areaName: string379 ) => {380 if (areaName !== "session") return;381 if (!Object.keys(changes).some((key) => key.startsWith("passkey_pending_"))) {382 return;383 }384 void refreshPendingPasskey();385 };386387 chrome.storage.onChanged.addListener(handleStorageChanged);388 onCleanup(() => chrome.storage.onChanged.removeListener(handleStorageChanged));389 });390391 async function refreshCurrentTab() {392 try {393 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });394 const url = tab?.url ?? tab?.pendingUrl ?? "";395 setCurrentTabDomain(url ? (extractDomain(url) ?? "") : "");396 } catch {397 setCurrentTabDomain("");398 }399 }400401 async function refreshPendingPasskey() {402 const res = await sendMessage({ type: "GET_PENDING_PASSKEY" });403 setPendingPasskey((res as { ok: boolean; data?: PendingPasskeyRequest | null }).data ?? null);404 }405406 async function restoreSettings() {407 const res = await sendMessage({ type: "GET_SETTINGS" });408 const s = (res as { ok: boolean; data?: any }).data ?? {};409 if (s.accentColor) setAccentColor(s.accentColor);410 setTheme(s.theme === "light" ? "light" : "dark");411 setLockOnClose(s.lockOnClose !== false);412 setAutoLockMinutes(typeof s.autoLockMinutes === "number" ? s.autoLockMinutes : 5);413 setBreachMonitoring(s.breachMonitoring !== false);414 setTrashRetentionDays(typeof s.trashRetentionDays === "number" ? s.trashRetentionDays : 30);415 }416417 async function applyTrashRetention(days: number) {418 setTrashRetentionDays(days);419 await sendMessage({ type: "UPDATE_SETTINGS", payload: { trashRetentionDays: days } });420 }421422 423 424 425 async function loadEntries(): Promise<VaultEntry[] | null> {426 const response = await sendMessage({ type: "GET_ENTRIES" });427 const res = response as { ok: boolean; data?: VaultEntry[]; error?: string };428429 430 431 432 433 if (!res.ok) {434 if (435 res.error === "vault locked" ||436 res.error === "SERVICE_WORKER_UNAVAILABLE" ||437 (typeof res.error === "string" && res.error.includes("locked"))438 ) {439 handleLock();440 return null; 441 }442 return [];443 }444445 const loadedEntries = res.data ?? [];446 setEntries(loadedEntries);447448 const currentId = selectedEntry()?.id;449 if (currentId) {450 setSelectedEntry(451 loadedEntries.find((entry) => entry.id === currentId) ?? loadedEntries[0] ?? null452 );453 }454455 return loadedEntries;456 }457458 async function openVault(preferredEntryId?: string) {459 460 const retDays = trashRetentionDays();461 if (retDays > 0) {462 void sendMessage({463 type: "PURGE_EXPIRED_TRASH",464 payload: { retentionMs: retDays * 86_400_000 },465 });466 }467468 const loadedEntries = await loadEntries();469470 471 472 if (loadedEntries === null) return;473474 475 let nextSelectedEntry: VaultEntry | null = null;476 if (preferredEntryId) {477 nextSelectedEntry = loadedEntries.find((e) => e.id === preferredEntryId) ?? null;478 }479 if (!nextSelectedEntry) {480 const tabDomain = currentTabDomain();481 if (tabDomain) {482 nextSelectedEntry = loadedEntries.find((e) => {483 if (e.type === "login" && e.url) return (extractDomain(e.url) ?? "") === tabDomain;484 if (e.type === "passkey" && e.rpId) return e.rpId === tabDomain;485 return false;486 }) ?? null;487 }488 }489 nextSelectedEntry ??= loadedEntries[0] ?? null;490491 setSelectedEntry(nextSelectedEntry);492 setEditEntry(undefined);493 setContentTransitioning(false);494 setContentPhase("enter");495 queueContentIdle();496 setView("entry");497 }498499 async function handleUnlock() {500 setHasVault(true);501 triggerPipPulse();502 await restoreSettings();503 await openVault();504 }505506 function handleSelectEntry(entry: VaultEntry) {507 setSelectedEntry(entry);508 setContentTransitioning(false);509 setContentPhase("enter");510 queueContentIdle();511 setView("entry");512 }513514 function handleAdd() {515 setEditEntry(undefined);516 setView("add");517 }518519 async function handleDelete() {520 const entry = selectedEntry();521 if (!entry) return;522 await sendMessage({ type: "DELETE_ENTRY", payload: { id: entry.id } });523 const loadedEntries = await loadEntries() ?? [];524 setSelectedEntry(loadedEntries[0] ?? null);525 setView("entry");526 }527528 async function handleDeleteMany(ids: string[]) {529 for (const id of ids) {530 await sendMessage({ type: "DELETE_ENTRY", payload: { id } });531 }532 const loadedEntries = await loadEntries() ?? [];533 534 const stillExists = loadedEntries.find(e => e.id === selectedEntry()?.id);535 if (!stillExists) setSelectedEntry(loadedEntries[0] ?? null);536 }537538 async function handleSave(savedEntry?: VaultEntry) {539 await openVault(savedEntry?.id);540 }541542 function handleBack() {543 setEditEntry(undefined);544 setView("entry");545 }546547 function handleLock() {548 clearCanarySuppressions();549 setEntries([]);550 setSelectedEntry(null);551 setEditEntry(undefined);552 setContentPhase("idle");553 setView("lock");554 }555556 function handleHeaderLock() {557 558 559 560 handleLock();561 void sendMessage({ type: "LOCK" });562 }563564 function navigate(dir: number) {565 const allEntries = entries();566 if (allEntries.length === 0) return;567 const currentIndex = selectedIndex();568 const baseIndex = currentIndex >= 0 ? currentIndex : 0;569 const nextIndex = (baseIndex + dir + allEntries.length) % allEntries.length;570 setSelectedEntry(allEntries[nextIndex]);571 setContentPhase("enter");572 queueContentIdle();573 }574575 function openList() {576 setView("list");577 }578579 function openHealth() {580 setEditEntry(undefined);581 setView("health");582 }583584 function handleOpenCJKImport() {585 setRestoreView("cjk");586 }587588 function handleOpenFileImport() {589 setRestoreView("file");590 }591592 const isShellOverlayOpen = () => view() !== "entry" || restoreView() !== null;593594 function handleFixEntry(entry: VaultEntry) {595 if (entry.type === "passkey") {596 setSelectedEntry(entry);597 setView("entry");598 return;599 }600 setEditEntry(entry);601 setView("add");602 }603604 const isLockedView = () => view() === "lock";605 const selectedPasskeyEntry = createMemo(() =>606 selectedEntry()?.type === "passkey" ? (selectedEntry() as PasskeyEntry) : null607 );608609 async function handleApprovePendingPasskey() {610 const pending = pendingPasskey();611 if (!pending) return;612 await sendMessage({ type: "APPROVE_PASSKEY", payload: { requestId: pending.requestId } });613 setPendingPasskey(null);614 }615616 async function handleDenyPendingPasskey() {617 const pending = pendingPasskey();618 if (!pending) return;619 await sendMessage({ type: "DENY_PASSKEY", payload: { requestId: pending.requestId } });620 setPendingPasskey(null);621 }622623 function queueContentIdle(after?: () => void) {624 if (contentPhaseTimer) window.clearTimeout(contentPhaseTimer);625 contentPhaseTimer = window.setTimeout(() => {626 setContentPhase("idle");627 after?.();628 }, ENTRY_TRANSITION_SETTLE_MS);629 }630631 return (632 <div class="popup-root peach-theme">633 <Show when={bootState() === "ready"}>634 <Show635 when={!pendingPasskey()}636 fallback={637 <PasskeyApprovalView638 type={pendingPasskey()!.type}639 rpName={pendingPasskey()!.rpName}640 rpId={pendingPasskey()!.rpId}641 userName={pendingPasskey()!.userName}642 userDisplayName={pendingPasskey()!.userDisplayName}643 onApprove={handleApprovePendingPasskey}644 onDeny={handleDenyPendingPasskey}645 />646 }647 >648 <Show649 when={!isLockedView()}650 fallback={651 <LockView652 hasVault={hasVault()}653 ready={bootState() === "ready"}654 onUnlock={handleUnlock}655 />656 }657 >658 <section659 id="hd-shell"660 class="peach-shell peach-theme"661 data-surface="extension"662 >663 <header class="peach-chrome" classList={{ "is-hidden-for-overlay": isShellOverlayOpen() }}>664 <PeachWordmark unlocked />665 <div class="peach-chrome-actions">666 <button667 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"668 onClick={() => setView("settings")}669 aria-label="Settings"670 >671 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">672 <path673 d="M5.91 1.11C6.38-.02 7.62-.02 8.09 1.11L8.31 1.64C8.48 2.05 8.91 2.27 9.34 2.17L9.9 2.04C11.1 1.76 11.98 2.64 11.7 3.84L11.57 4.4C11.47 4.83 11.69 5.26 12.1 5.43L12.63 5.65C13.76 6.12 13.76 7.36 12.63 7.83L12.1 8.05C11.69 8.22 11.47 8.65 11.57 9.08L11.7 9.64C11.98 10.84 11.1 11.72 9.9 11.44L9.34 11.31C8.91 11.21 8.48 11.43 8.31 11.84L8.09 12.37C7.62 13.5 6.38 13.5 5.91 12.37L5.69 11.84C5.52 11.43 5.09 11.21 4.66 11.31L4.1 11.44C2.9 11.72 2.02 10.84 2.3 9.64L2.43 9.08C2.53 8.65 2.31 8.22 1.9 8.05L1.37 7.83C.24 7.36.24 6.12 1.37 5.65L1.9 5.43C2.31 5.26 2.53 4.83 2.43 4.4L2.3 3.84C2.02 2.64 2.9 1.76 4.1 2.04L4.66 2.17C5.09 2.27 5.52 2.05 5.69 1.64L5.91 1.11Z"674 stroke="currentColor"675 stroke-width="0.9"676 />677 <circle cx="7" cy="7" r="2.1" stroke="currentColor" stroke-width="0.9" />678 </svg>679 </button>680 <button681 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"682 onClick={handleAdd}683 aria-label="New entry"684 >685 <svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round">686 <path d="M5.5 1v9M1 5.5h9" />687 </svg>688 </button>689 <button690 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-lock-btn"691 onClick={() => void handleHeaderLock()}692 aria-label="Lock vault"693 >694 <svg class="peach-lock-icon" width="14" height="16" viewBox="0 0 14 16" fill="none">695 <path696 d="M3.75 6V4.75C3.75 2.96 5.21 1.5 7 1.5C8.79 1.5 10.25 2.96 10.25 4.75V6"697 stroke="currentColor"698 stroke-width="1"699 stroke-linecap="round"700 />701 <rect x="1.5" y="6" width="11" height="8.5" rx="2.2" stroke="currentColor" stroke-width="1" />702 <circle cx="7" cy="10.25" r="1" fill="currentColor" />703 </svg>704 </button>705 </div>706 </header>707708 <div class="peach-shell-body">709 <div class="peach-shell-veil"></div>710 <div class="peach-content-frame" data-content-phase={contentPhase()} classList={{ navigating: contentTransitioning() }}>711 <Show712 when={selectedPasskeyEntry()}713 fallback={714 <EntryView715 entry={selectedEntry()}716 onDelete={handleDelete}717 onFieldSaved={() => void loadEntries()}718 onEscape={() => { setView("list"); }}719 />720 }721 >722 <PasskeyEntryView entry={selectedPasskeyEntry()} onDelete={handleDelete} />723 </Show>724 </div>725726 <section class="peach-search-wrap" classList={{ "is-hidden-for-overlay": isShellOverlayOpen() }}>727 <div class="peach-search-bar">728 <span class="peach-search-icon">729 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">730 <circle cx="5.5" cy="5.5" r="3.75" />731 <path d="M8.75 8.75L11.5 11.5" />732 </svg>733 </span>734 <input735 class="peach-search-input"736 placeholder="search vault..."737 autocomplete="off"738 aria-label="Search vault entries"739 onClick={openList}740 onFocus={openList}741 readOnly742 />743 <div class="peach-search-meta">744 <span class="peach-search-counter">745 {entries().length > 0746 ? `${selectedIndex() + 1} / ${entries().length}`747 : "0 entries"}748 </span>749 <span class="peach-divider"></span>750 <button751 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"752 onClick={() => navigate(-1)}753 aria-label="Previous entry"754 disabled={entries().length === 0}755 >756 <svg width="6" height="11" viewBox="0 0 6 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">757 <path d="M5 1L1 5.5L5 10" />758 </svg>759 </button>760 <button761 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"762 onClick={() => navigate(1)}763 aria-label="Next entry"764 disabled={entries().length === 0}765 >766 <svg width="6" height="11" viewBox="0 0 6 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">767 <path d="M1 1L5 5.5L1 10" />768 </svg>769 </button>770 </div>771 </div>772 </section>773 </div>774775 <Show when={view() === "list"}>776 <VaultView777 entries={entries()}778 selectedEntryId={selectedEntry()?.id}779 currentTabDomain={currentTabDomain()}780 sortOrder={sortOrder()}781 onSelectEntry={handleSelectEntry}782 onSortOrderChange={setSortOrder}783 onAdd={handleAdd}784 onClose={() => setView("entry")}785 onOpenTrash={() => setView("trash")}786 onDeleteMany={handleDeleteMany}787 />788 </Show>789 <Show when={view() === "settings"}>790 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>791 <SettingsView792 entriesCount={entries().length}793 breachMonitoring={breachMonitoring()}794 lockOnClose={lockOnClose()}795 autoLockMinutes={autoLockMinutes()}796 trashRetentionDays={trashRetentionDays()}797 theme={theme()}798 onBreachMonitoringChange={applyBreachMonitoring}799 onLockOnCloseChange={applyLockOnClose}800 onAutoLockMinutesChange={applyAutoLockMinutes}801 onTrashRetentionChange={applyTrashRetention}802 onThemeChange={applyTheme}803 onBack={handleBack}804 onImport={() => setView("import")}805 onExport={() => setView("export")}806 onBreachCheckDone={loadEntries}807 onOpenHealth={openHealth}808 onOpenCJKImport={handleOpenCJKImport}809 onOpenFileImport={handleOpenFileImport}810 />811 </Suspense>812 </Show>813 <Show when={view() === "add"}>814 <AddView815 entry={editEntry()}816 entries={entries()}817 onBack={handleBack}818 onSave={handleSave}819 />820 </Show>821 <Show when={view() === "import"}>822 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>823 <ImportView onBack={handleBack} onImport={handleSave} />824 </Suspense>825 </Show>826 <Show when={view() === "export"}>827 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>828 <ExportView onBack={handleBack} onRequiresUnlock={handleLock} />829 </Suspense>830 </Show>831 <Show when={view() === "health"}>832 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>833 <HealthView834 entries={entries()}835 onFixEntry={handleFixEntry}836 onBack={() => setView("settings")}837 onRefresh={loadEntries}838 />839 </Suspense>840 </Show>841 <Show when={view() === "trash"}>842 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>843 <TrashView844 onBack={() => setView("list")}845 onRestored={() => void loadEntries()}846 />847 </Suspense>848 </Show>849 <Show when={restoreView() === "cjk"}>850 <CJKImport851 mode="onboarding"852 onBack={() => setRestoreView(null)}853 onDone={async () => {854 setRestoreView(null);855 await handleUnlock();856 }}857 />858 </Show>859 <Show when={restoreView() === "file"}>860 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>861 <ImportView862 mode="onboarding"863 replacesExistingVault={true}864 onBack={() => setRestoreView(null)}865 onImport={async () => {866 setRestoreView(null);867 await handleUnlock();868 }}869 />870 </Suspense>871 </Show>872 </section>873 </Show>874 </Show>875 </Show>876 </div>877 );878}