1import { createEffect, createMemo, createSignal, lazy, onCleanup, onMount, Show, Suspense } from "solid-js";2import { Portal } from "solid-js/web";3import { storageGet, storageSet } from "../shared/storage";4import { sendMessage } from "../shared/bridge";5import { ACCENT_COLORS } from "../shared/types";6import type { AccentColorName, PendingPasskeyRequest, ProFeature, VaultEntry } from "../shared/types";7import { isPasskeyOnly } from "../shared/types";8import { LockView } from "./views/Lock";9import { Onboarding } from "./views/Onboarding";10import { NotesMode } from "./views/NotesMode";11import { ProCelebration } from "./views/ProCelebration";12import { VaultView, type SortOrder } from "./views/Vault";13import { EntryView } from "./views/Entry";14import { AddView } from "./views/Add";15import { PeachWordmark } from "./components/PeachMark";16import { PasskeyApprovalView } from "./views/PasskeyApproval";17import { extractDomain } from "../shared/domain";18import { popupCopy } from "./copy";19import {20 DEFAULT_BINDINGS,21 formatKeybinding,22 getActiveBindings,23 matchKeybinding,24 SHORTCUT_DESCRIPTORS,25 type Keybinding,26 type ShortcutAction,27} from "../shared/shortcuts";28import { generateTOTP } from "../crypto/totp";29import { isNativePlatform, nativePlugin } from "../shared/native";3031export { sendMessage } from "../shared/bridge";3233const ImportView = lazy(() => import("./views/Import").then(m => ({ default: m.ImportView })));34const ExportView = lazy(() => import("./views/Export").then(m => ({ default: m.ExportView })));35const SettingsView = lazy(() => import("./views/Settings").then(m => ({ default: m.SettingsView })));36const HealthView = lazy(() => import("./views/Health").then(m => ({ default: m.HealthView })));37const TrashView = lazy(() => import("./views/TrashView").then(m => ({ default: m.TrashView })));38const PanicView = lazy(() => import("./views/Panic").then(m => ({ default: m.PanicView })));39const ChirpSyncView = lazy(() => import("./views/ChirpSync").then(m => ({ default: m.ChirpSyncView })));40const CodexGenerateView = lazy(() => import("./views/CodexGenerateView").then(m => ({ default: m.CodexGenerateView })));41const CodexRestoreView = lazy(() => import("./views/CodexRestoreView").then(m => ({ default: m.CodexRestoreView })));4243type View =44 | "lock"45 | "pro-celebration"46 | "entry"47 | "list"48 | "add"49 | "settings"50 | "chirp-sync"51 | "import"52 | "export"53 | "health"54 | "trash"55 | "panic"56 | "codex-generate"57 | "codex-restore";58type BootState = "loading" | "ready";5960const canarySuppressMap = new Map<string, boolean>();61let shellRingAnimation: Animation | null = null;6263export function isCanarySuppressed(entryId: string): boolean {64 return canarySuppressMap.get(entryId) ?? false;65}6667export function setCanarySuppressed(entryId: string, suppressed: boolean): void {68 canarySuppressMap.set(entryId, suppressed);69}7071export function clearCanarySuppressions(): void {72 canarySuppressMap.clear();73}7475export function triggerRing() {76 const shell = document.getElementById("hd-shell");77 if (!shell) return;7879 shellRingAnimation?.cancel();80 shell.dataset.glowing = "true";81 const animation = shell.animate(82 [83 { offset: 0, boxShadow: "var(--copy-shadow)" },84 {85 offset: 0.3,86 boxShadow:87 "0 2px 8px rgba(0,0,0,0.5), 0 14px 48px rgba(0,0,0,0.6), 0 0 0 1.5px color-mix(in srgb, var(--peach-accent) 58%, transparent), 0 0 22px color-mix(in srgb, var(--peach-accent) 18%, transparent)",88 },89 { offset: 1, boxShadow: "var(--copy-shadow)" },90 ],91 {92 duration: 720,93 easing: "cubic-bezier(0.4, 0, 0.2, 1)",94 fill: "none",95 }96 );97 shellRingAnimation = animation;98 const clearGlow = () => {99 if (shellRingAnimation !== animation) return;100 shellRingAnimation = null;101 delete shell.dataset.glowing;102 };103 animation.onfinish = clearGlow;104 animation.oncancel = clearGlow;105}106107108export function triggerPipPulse() {109 const pip = document.querySelector(".peach-status-pip, .pip");110 if (pip) {111 pip.classList.remove("pulse");112 void (pip as HTMLElement).offsetWidth;113 pip.classList.add("pulse");114 }115}116117const ENTRY_TRANSITION_SETTLE_MS = 135;118119export default function App() {120 const [bootState, setBootState] = createSignal<BootState>("loading");121 const [view, setView] = createSignal<View>("lock");122 123 124 const [sheetTranslate, setSheetTranslate] = createSignal<number | null>(null);125 const [contentPhase, setContentPhase] = createSignal<"enter" | "exit" | "idle">(126 "enter"127 );128 const [contentTransitioning, setContentTransitioning] = createSignal(false);129 const [hasVault, setHasVault] = createSignal(false);130 const [onboarded, setOnboarded] = createSignal(true);131 const [entries, setEntries] = createSignal<VaultEntry[]>([]);132 const [selectedEntry, setSelectedEntry] = createSignal<VaultEntry | null>(null);133 const [editEntry, setEditEntry] = createSignal<VaultEntry | undefined>(undefined);134 const [accentColor, setAccentColor] = createSignal<AccentColorName>("peach");135 const [theme, setTheme] = createSignal<"dark" | "light">("dark");136 const [textSize, setTextSize] = createSignal<"small" | "medium" | "large">("medium");137 const [reduceMotion, setReduceMotion] = createSignal<boolean>(false);138 const [lockOnClose, setLockOnClose] = createSignal(true);139 const [autoLockMinutes, setAutoLockMinutes] = createSignal(5);140 const [breachMonitoring, setBreachMonitoring] = createSignal(true);141 const [shortcutBindings, setShortcutBindings] = createSignal<Record<ShortcutAction, Keybinding>>(DEFAULT_BINDINGS);142 const [showShortcutsHelp, setShowShortcutsHelp] = createSignal(false);143 const [trashRetentionDays, setTrashRetentionDays] = createSignal(30);144 const [restoreView, setRestoreView] = createSignal<"file" | null>(null);145 const [pendingPasskey, setPendingPasskey] = createSignal<PendingPasskeyRequest | null>(null);146 const [currentTabDomain, setCurrentTabDomain] = createSignal<string>("");147 const [currentTabUrl, setCurrentTabUrl] = createSignal<string>("");148 149 150 151 152 const [proStatus, setProStatus] = createSignal(false);153 const [settingsInitialSection, setSettingsInitialSection] = createSignal<string | null>(null);154 const [settingsActiveCard, setSettingsActiveCard] = createSignal<string>("protection");155 const [upgradeHighlight, setUpgradeHighlight] = createSignal<ProFeature | null>(null);156 const [sortOrder, setSortOrder] = createSignal<SortOrder>("url-first");157 const [listSearchSeed, setListSearchSeed] = createSignal("");158 const [listSearchSeedNonce, setListSearchSeedNonce] = createSignal(0);159 const [pendingCelebration, setPendingCelebration] = createSignal(false);160 const [pendingProNotice, setPendingProNotice] = createSignal<{ feature: string } | null>(null);161 162 163 164 const [returnToPanic, setReturnToPanic] = createSignal(false);165 166 167 const [editBackView, setEditBackView] = createSignal<View>("entry");168 const [settingsExiting, setSettingsExiting] = createSignal(false);169 const [showNotes, setShowNotes] = createSignal(false);170 const [openNoteId, setOpenNoteId] = createSignal<string | undefined>(undefined);171 172 173 174 175 const [forceRecoveryWords, setForceRecoveryWords] = createSignal(false);176177 let contentPhaseTimer: number | undefined;178179 function applyAccent(name: AccentColorName) {180 const color = ACCENT_COLORS.find(c => c.name === name) ?? ACCENT_COLORS[0]!;181 const hex = color.hex;182 document.documentElement.style.setProperty("--peach-accent", hex);183 184 185 document.documentElement.style.setProperty("--peach-glow", hex + "2A");186 document.documentElement.style.setProperty("--peach-surface-tint", hex + "09");187 document.documentElement.style.setProperty("--peach-border-accent", hex + "24");188 }189190 createEffect(() => {191 applyAccent(accentColor());192 });193194 createEffect(() => {195 document.documentElement.classList.toggle("light", theme() === "light");196 document.body.classList.toggle("light", theme() === "light");197 });198199 createEffect(() => {200 document.documentElement.setAttribute("data-text-size", textSize());201 });202203 createEffect(() => {204 if (reduceMotion()) {205 document.documentElement.setAttribute("data-motion", "reduce");206 } else {207 document.documentElement.removeAttribute("data-motion");208 }209 });210211 const selectedIndex = createMemo(() => {212 const currentId = selectedEntry()?.id;213 if (!currentId) return entries().length > 0 ? 0 : -1;214 return entries().findIndex((entry) => entry.id === currentId);215 });216217 async function applyAccentColor(name: AccentColorName) {218 applyAccent(name);219 setAccentColor(name);220 await sendMessage({ type: "UPDATE_SETTINGS", payload: { accentColor: name } });221 }222223 async function applyTheme(t: "dark" | "light") {224 setTheme(t);225 await sendMessage({ type: "UPDATE_SETTINGS", payload: { theme: t } });226 }227228 async function applyTextSize(size: "small" | "medium" | "large") {229 setTextSize(size);230 await sendMessage({ type: "UPDATE_SETTINGS", payload: { textSize: size } });231 }232233 async function applyReduceMotion(enabled: boolean) {234 setReduceMotion(enabled);235 await sendMessage({ type: "UPDATE_SETTINGS", payload: { reduceMotion: enabled } });236 }237238 239 240 241 async function applyShortcuts(active: Record<ShortcutAction, Keybinding>) {242 setShortcutBindings(active);243 const overrides: Record<string, Keybinding> = {};244 for (const action of Object.keys(active) as ShortcutAction[]) {245 const a = active[action];246 const d = DEFAULT_BINDINGS[action];247 const same = a.key === d.key248 && Boolean(a.ctrl) === Boolean(d.ctrl)249 && Boolean(a.meta) === Boolean(d.meta)250 && Boolean(a.shift) === Boolean(d.shift)251 && Boolean(a.alt) === Boolean(d.alt);252 if (!same) overrides[action] = a;253 }254 await sendMessage({ type: "UPDATE_SETTINGS", payload: { shortcuts: overrides } });255 }256257 async function applyLockOnClose(enabled: boolean) {258 setLockOnClose(enabled);259 await sendMessage({260 type: "UPDATE_SETTINGS",261 payload: { lockOnClose: enabled },262 });263 }264265 async function applyAutoLockMinutes(minutes: number) {266 setAutoLockMinutes(minutes);267 await sendMessage({268 type: "UPDATE_SETTINGS",269 payload: { autoLockMinutes: minutes },270 });271 }272273 async function applyBreachMonitoring(enabled: boolean) {274 setBreachMonitoring(enabled);275 await sendMessage({276 type: "UPDATE_SETTINGS",277 payload: { breachMonitoring: enabled },278 });279 }280281 onMount(async () => {282 283 284 285 286 287 const [unlockResponse, vaultResponse, , celebrationResponse] = await Promise.all([288 sendMessage({ type: "IS_UNLOCKED" }),289 sendMessage({ type: "HAS_VAULT" }),290 refreshCurrentTab(),291 sendMessage({ type: "SHOULD_PLAY_CELEBRATION" }),292 ]);293 const unlocked = Boolean(294 (unlockResponse as { ok: boolean; data?: boolean }).data295 );296 const vaultExists = Boolean(297 (vaultResponse as { ok: boolean; data?: boolean }).data298 );299 const shouldCelebrate = Boolean(300 (celebrationResponse as { ok: boolean; data?: boolean }).data301 );302 setHasVault(vaultExists);303 304 305 306 try {307 const onboarded = await storageGet("peach_onboarded");308 const seen = onboarded === "true";309 if (vaultExists && !seen) {310 await storageSet("peach_onboarded", "true");311 setOnboarded(true);312 } else {313 setOnboarded(seen);314 }315 } catch {316 317 setOnboarded(true);318 }319 await Promise.all([restoreSettings(), refreshProStatus()]);320321 322 if (shouldCelebrate) {323 await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });324 }325326 327 328 329 330 331 332 let recoveryPending = false;333 try {334 const out = await chrome.storage.local?.get("peach_recovery_pending");335 recoveryPending = (out as { peach_recovery_pending?: string })?.peach_recovery_pending === "1";336 } catch { }337338 339 340 341 342 343 if (recoveryPending && !unlocked && vaultExists && isNativePlatform()) {344 try {345 await chrome.storage.local?.remove([346 "peach_vault_a",347 "peach_vault_b",348 "peach_recovery_pending",349 "peach_onboarded",350 "peach_vault_history",351 ]);352 } catch { }353 setHasVault(false);354 setOnboarded(false);355 setView("lock");356 setBootState("ready");357 document.getElementById("pre-lock")?.setAttribute("hidden", "true");358 return;359 }360361 if (recoveryPending && unlocked) {362 363 364 setForceRecoveryWords(true);365 setView("lock");366 } else if (unlocked && vaultExists) {367 await openVault(undefined, shouldCelebrate);368 if (shouldCelebrate) {369 setView("pro-celebration");370 window.setTimeout(() => setView("entry"), 2500);371 }372 } else if (shouldCelebrate) {373 setPendingCelebration(true);374 }375376 await refreshPendingPasskey();377378 379 const handleTabActivated = () => { void refreshCurrentTab(); };380 const handleTabUpdated = (_tabId: number, changeInfo: { url?: string; status?: string }) => {381 if (changeInfo.url || changeInfo.status === "complete") void refreshCurrentTab();382 };383 try {384 chrome.tabs.onActivated.addListener(handleTabActivated);385 chrome.tabs.onUpdated.addListener(handleTabUpdated);386 onCleanup(() => {387 chrome.tabs.onActivated.removeListener(handleTabActivated);388 chrome.tabs.onUpdated.removeListener(handleTabUpdated);389 });390 } catch {391 392 }393394 setBootState("ready");395 document.getElementById("pre-lock")?.setAttribute("hidden", "true");396397 398 399 400 401 try {402 const route = new URLSearchParams(window.location.search).get("codex");403 if (route === "pdf" || route === "restore") {404 setView("codex-restore");405 }406 } catch {407 408 }409410 411 if (chrome.storage.session) {412 const session = await chrome.storage.session.get(["pendingProGate", "pendingProNotice"]) as413 Record<string, { feature?: string } | undefined>;414 if (session.pendingProGate?.feature) {415 await chrome.storage.session.remove("pendingProGate");416 setUpgradeHighlight((session.pendingProGate.feature as ProFeature) ?? null);417 setSettingsInitialSection("license");418 setView("settings");419 } else if (session.pendingProNotice?.feature) {420 await chrome.storage.session.remove("pendingProNotice");421 setPendingProNotice({ feature: session.pendingProNotice.feature ?? "" });422 }423 }424425 426 427 428 429 void Promise.all([430 import("./views/Settings").catch(() => {}),431 import("./views/Import").catch(() => {}),432 import("./views/Export").catch(() => {}),433 import("./views/Health").catch(() => {}),434 import("./views/TrashView").catch(() => {}),435 ]);436437 438 439 440 let popupPort: chrome.runtime.Port | null = null;441 try {442 popupPort = chrome.runtime.connect({ name: "popup" });443 } catch { }444 onCleanup(() => { try { popupPort?.disconnect(); } catch {} });445446 447 448 449 if (isNativePlatform()) {450 let backgroundedAt: number | null = null;451452 const CapApp = nativePlugin<{453 addListener: (454 ev: string,455 cb: (s: { isActive: boolean }) => void,456 ) => Promise<{ remove: () => Promise<void> }> | { remove: () => Promise<void> };457 }>("App");458 if (CapApp?.addListener) {459 const sub = CapApp.addListener(460 "appStateChange",461 ({ isActive }: { isActive: boolean }) => {462 if (!isActive) {463 backgroundedAt = Date.now();464 return;465 }466 if (backgroundedAt !== null && autoLockMinutes() > 0) {467 const elapsed = Date.now() - backgroundedAt;468 if (elapsed >= autoLockMinutes() * 60_000) handleHeaderLock();469 }470 backgroundedAt = null;471 }472 );473 474 onCleanup(() => {475 Promise.resolve(sub).then((s) => s?.remove?.()).catch(() => {});476 });477 }478479 480 481 482 483 484 let lastUserActivityAt = Date.now();485 const bumpActivity = () => { lastUserActivityAt = Date.now(); };486 const ACTIVITY_EVENTS = ["touchstart", "click", "keydown", "input"] as const;487 for (const ev of ACTIVITY_EVENTS) {488 document.addEventListener(ev, bumpActivity, { passive: true, capture: true });489 }490 const idleLockInterval = setInterval(() => {491 const minutes = autoLockMinutes();492 if (!minutes || minutes <= 0) return;493 const elapsed = Date.now() - lastUserActivityAt;494 if (elapsed >= minutes * 60_000) {495 lastUserActivityAt = Date.now();496 handleHeaderLock();497 }498 }, 30_000);499 onCleanup(() => {500 clearInterval(idleLockInterval);501 for (const ev of ACTIVITY_EVENTS) {502 document.removeEventListener(ev, bumpActivity, { capture: true });503 }504 });505 }506507 508 509 const handleRuntimeMessage = (msg: { type?: string; payload?: { feature?: ProFeature } }) => {510 if (msg?.type === "VAULT_LOCKED" && view() !== "lock") handleLock();511 if (msg?.type === "PRO_GATE_TRIGGERED") {512 setUpgradeHighlight(msg.payload?.feature ?? null);513 setSettingsInitialSection("license");514 setView("settings");515 }516 };517 chrome.runtime.onMessage.addListener(handleRuntimeMessage);518 onCleanup(() => chrome.runtime.onMessage.removeListener(handleRuntimeMessage));519520 521 522 523 const handlePeachBack = () => {524 525 if (showNotes()) {526 setShowNotes(false);527 setOpenNoteId(undefined);528 return;529 }530 const v = view();531 if (v === "lock" && !hasVault() && onboarded()) {532 setOnboarded(false);533 return;534 }535 if (v === "list") { setView("entry"); return; }536 if (v === "settings") {537 window.dispatchEvent(new Event("peach:settings-back"));538 return;539 }540 if (v === "import" || v === "health") { setView("settings"); return; }541 if (v === "export") { handleExportBack(); return; }542 if (v === "add") { handleBack(); return; }543 if (v === "chirp-sync") { setView("settings"); return; }544 };545 window.addEventListener("peach:back", handlePeachBack);546 onCleanup(() => window.removeEventListener("peach:back", handlePeachBack));547548 549 550 551 552 553 if (isNativePlatform()) {554 let touchStartY = 0;555 let touchStartX = 0;556 let lastY = 0;557 let lastT = 0;558 let dragging = false;559 let captured = false;560 const HORIZONTAL_TOLERANCE = 16;561 const ACTIVATION_DY = 8;562 const PEEK_PX = 60;563564 const onTouchStart = (e: TouchEvent) => {565 const t = e.touches[0];566 if (!t) return;567 const v = view();568 if (v !== "entry" && v !== "list") return;569 570 571 const inBand = v === "entry"572 ? t.clientY > window.innerHeight * 0.6573 : t.clientY < window.innerHeight * 0.35;574 if (!inBand) return;575 touchStartY = lastY = t.clientY;576 touchStartX = t.clientX;577 lastT = e.timeStamp;578 dragging = true;579 captured = false;580 };581582 const onTouchMove = (e: TouchEvent) => {583 if (!dragging) return;584 const t = e.touches[0];585 if (!t) return;586 const dy = t.clientY - touchStartY;587 const dx = Math.abs(t.clientX - touchStartX);588589 if (!captured) {590 if (dx > HORIZONTAL_TOLERANCE && Math.abs(dy) < dx) {591 592 dragging = false;593 return;594 }595 if (Math.abs(dy) < ACTIVATION_DY) return;596 captured = true;597 }598599 600 601 const v = view();602 const restClosed = window.innerHeight - PEEK_PX;603 const baseTranslate = v === "list" ? 0 : restClosed;604 const next = Math.max(0, Math.min(restClosed, baseTranslate + dy));605 setSheetTranslate(next);606 lastY = t.clientY;607 lastT = e.timeStamp;608 };609610 const onTouchEnd = (e: TouchEvent) => {611 if (!dragging) { return; }612 const t = e.changedTouches[0];613 const elapsed = Math.max(1, e.timeStamp - lastT);614 const velocity = t ? (t.clientY - lastY) / elapsed : 0; 615 const wasCaptured = captured;616 dragging = false;617 captured = false;618 const v = view();619620 if (!wasCaptured) {621 622 if (v === "entry" && touchStartY > window.innerHeight - PEEK_PX) setView("list");623 setSheetTranslate(null);624 return;625 }626627 const restClosed = window.innerHeight - PEEK_PX;628 const current = sheetTranslate() ?? restClosed;629630 631 632 633 634 let nextOpen: boolean;635 if (velocity < -0.05) nextOpen = true;636 else if (velocity > 0.05) nextOpen = false;637 else nextOpen = current < restClosed * 0.5;638639 if (nextOpen && v !== "list") setView("list");640 if (!nextOpen && v !== "entry") setView("entry");641 642 setSheetTranslate(null);643 };644645 document.addEventListener("touchstart", onTouchStart, { passive: true });646 document.addEventListener("touchmove", onTouchMove, { passive: true });647 document.addEventListener("touchend", onTouchEnd, { passive: true });648 document.addEventListener("touchcancel", () => { dragging = false; captured = false; setSheetTranslate(null); }, { passive: true });649 onCleanup(() => {650 document.removeEventListener("touchstart", onTouchStart);651 document.removeEventListener("touchmove", onTouchMove);652 document.removeEventListener("touchend", onTouchEnd);653 });654 }655656 657 658 659 const SHORTCUT_HANDLERS: Partial<Record<ShortcutAction, () => void>> = {660 copy_password: () => {661 const entry = selectedEntry();662 if (entry?.password) void popupCopy(entry.password, "Password").then((ok) => { if (ok) triggerRing(); });663 },664 copy_username: () => {665 const entry = selectedEntry();666 if (entry?.username) void popupCopy(entry.username, "Username").then((ok) => { if (ok) triggerRing(); });667 },668 copy_totp: () => {669 const entry = selectedEntry();670 if (!entry?.totp) return;671 try {672 const { code } = generateTOTP(entry.totp);673 void popupCopy(code, "2FA").then((ok) => { if (ok) triggerRing(); });674 } catch { }675 },676 new_entry: () => handleAdd(),677 edit_entry: () => {678 const entry = selectedEntry();679 680 681 if (!entry || isPasskeyOnly(entry)) return;682 setEditEntry(entry);683 setView("add");684 },685 delete_entry: () => {686 if (selectedEntry()) void handleDelete();687 },688 lock_vault: () => { void handleHeaderLock(); },689 search: () => openList(),690 nav_prev: () => navigate(-1),691 nav_next: () => navigate(1),692 jump_vault: () => setView("entry"),693 jump_settings: () => setView("settings"),694 show_help: () => setShowShortcutsHelp(true),695 };696697 698 699 700 const ENTRY_SCOPED: ReadonlySet<ShortcutAction> = new Set([701 "copy_password", "copy_username", "copy_totp",702 "edit_entry", "delete_entry", "nav_prev", "nav_next",703 ]);704705 const handleGlobalKeydown = (e: KeyboardEvent) => {706 const v = view();707 const inInput = (e.target as HTMLElement)?.tagName === "INPUT"708 || (e.target as HTMLElement)?.tagName === "TEXTAREA"709 || (e.target as HTMLElement)?.isContentEditable;710 const isPrintableSearchKey =711 e.key.length === 1 &&712 !e.ctrlKey &&713 !e.metaKey &&714 !e.altKey;715716 717 if (v === "pro-celebration") return;718719 720 721 if (showShortcutsHelp()) {722 if (e.key === "Escape") { e.preventDefault(); setShowShortcutsHelp(false); }723 return;724 }725726 727 if (e.key === "Escape") {728 if (showNotes()) { setShowNotes(false); setOpenNoteId(undefined); return; }729 if (v === "list") { setView("entry"); return; }730 if (v === "settings" || v === "add") { handleBack(); return; }731 if (v === "import" || v === "health") { setView("settings"); return; }732 if (v === "export") { handleExportBack(); return; }733 if (v === "chirp-sync") { setView("settings"); return; }734 }735736 737 738 739 const bindings = shortcutBindings();740 for (const action of Object.keys(bindings) as ShortcutAction[]) {741 const binding = bindings[action];742 if (!matchKeybinding(e, binding)) continue;743 744 745 const hasMod = binding.ctrl || binding.meta || binding.alt || binding.shift;746 if (!hasMod && binding.key.length === 1 && inInput) return;747 if (ENTRY_SCOPED.has(action) && v !== "entry") return;748 e.preventDefault();749 SHORTCUT_HANDLERS[action]?.();750 return;751 }752753 754 755 756 757 if (v === "entry" && !inInput && isPrintableSearchKey) {758 e.preventDefault();759 primeListSearch(e.key);760 return;761 }762 };763 window.addEventListener("keydown", handleGlobalKeydown);764 onCleanup(() => window.removeEventListener("keydown", handleGlobalKeydown));765766 const handleStorageChanged = (767 changes: Record<string, chrome.storage.StorageChange>,768 areaName: string769 ) => {770 if (areaName !== "session") return;771 if (!Object.keys(changes).some((key) => key.startsWith("passkey_pending_"))) {772 return;773 }774 void refreshPendingPasskey();775 };776 chrome.storage.onChanged.addListener(handleStorageChanged);777 onCleanup(() => chrome.storage.onChanged.removeListener(handleStorageChanged));778 });779780 async function refreshCurrentTab() {781 try {782 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });783 const url = tab?.url ?? tab?.pendingUrl ?? "";784 setCurrentTabDomain(url ? (extractDomain(url) ?? "") : "");785 786 787 const isWebUrl = /^https?:\/\//i.test(url);788 setCurrentTabUrl(isWebUrl ? url : "");789 } catch {790 setCurrentTabDomain("");791 setCurrentTabUrl("");792 }793 }794795 async function refreshPendingPasskey() {796 const res = await sendMessage({ type: "GET_PENDING_PASSKEY" });797 setPendingPasskey((res as { ok: boolean; data?: PendingPasskeyRequest | null }).data ?? null);798 }799800 async function refreshProStatus() {801 const res = await sendMessage({ type: "GET_LICENSE" });802 const data = (res as { ok: boolean; data?: { valid?: boolean; plan?: string } }).data;803 setProStatus(Boolean(data?.valid && data?.plan === "pro"));804 }805806 async function restoreSettings() {807 const res = await sendMessage({ type: "GET_SETTINGS" });808 const s = (res as { ok: boolean; data?: any }).data ?? {};809 if (s.accentColor && ACCENT_COLORS.some(c => c.name === s.accentColor)) {810 setAccentColor(s.accentColor as AccentColorName);811 }812 setTheme(s.theme === "light" ? "light" : "dark");813 if (s.textSize === "small" || s.textSize === "large") {814 setTextSize(s.textSize);815 } else {816 setTextSize("medium");817 }818 setReduceMotion(s.reduceMotion === true);819 setLockOnClose(s.lockOnClose !== false);820 setAutoLockMinutes(typeof s.autoLockMinutes === "number" ? s.autoLockMinutes : 5);821 setBreachMonitoring(s.breachMonitoring !== false);822 setTrashRetentionDays(typeof s.trashRetentionDays === "number" ? s.trashRetentionDays : 30);823 setShortcutBindings(getActiveBindings(s.shortcuts));824 }825826 async function applyTrashRetention(days: number) {827 setTrashRetentionDays(days);828 await sendMessage({ type: "UPDATE_SETTINGS", payload: { trashRetentionDays: days } });829 }830831 function primeListSearch(seed: string) {832 setListSearchSeed(seed);833 setListSearchSeedNonce((value) => value + 1);834 openList();835 }836837 838 839 840 async function loadEntries(): Promise<VaultEntry[] | null> {841 const response = await sendMessage({ type: "GET_ENTRIES" });842 const res = response as { ok: boolean; data?: VaultEntry[]; error?: string };843844 845 846 847 848 if (!res.ok) {849 if (850 res.error === "vault_locked" ||851 res.error === "SERVICE_WORKER_UNAVAILABLE" ||852 (typeof res.error === "string" && res.error.includes("locked"))853 ) {854 handleLock();855 return null; 856 }857 return [];858 }859860 const loadedEntries = res.data ?? [];861 setEntries(loadedEntries);862863 const currentId = selectedEntry()?.id;864 if (currentId) {865 setSelectedEntry(866 loadedEntries.find((entry) => entry.id === currentId) ?? loadedEntries[0] ?? null867 );868 }869870 return loadedEntries;871 }872873 async function openVault(preferredEntryId?: string, skipViewSet = false) {874 875 const retDays = trashRetentionDays();876 if (retDays > 0) {877 void sendMessage({878 type: "PURGE_EXPIRED_TRASH",879 payload: { retentionMs: retDays * 86_400_000 },880 });881 }882883 const loadedEntries = await loadEntries();884885 886 887 if (loadedEntries === null) return;888889 890 let nextSelectedEntry: VaultEntry | null = null;891 if (preferredEntryId) {892 nextSelectedEntry = loadedEntries.find((e) => e.id === preferredEntryId) ?? null;893 }894 if (!nextSelectedEntry) {895 const tabDomain = currentTabDomain();896 if (tabDomain) {897 nextSelectedEntry = loadedEntries.find((e) => {898 if (e.type === "login") {899 if (e.url && (extractDomain(e.url) ?? "") === tabDomain) return true;900 for (const extra of e.additionalUrls ?? []) {901 if ((extractDomain(extra) ?? "") === tabDomain) return true;902 }903 return false;904 }905 if (e.passkey?.rpId) return e.passkey.rpId === tabDomain;906 return false;907 }) ?? null;908 }909 }910 nextSelectedEntry ??= loadedEntries[0] ?? null;911912 setSelectedEntry(nextSelectedEntry);913 setEditEntry(undefined);914 setContentTransitioning(false);915 setContentPhase("enter");916 queueContentIdle();917 if (!skipViewSet) setView("entry");918 }919920 async function handleUnlock() {921 922 923 924 925 926 927 let recoveryPending = false;928 try {929 const out = await chrome.storage.local?.get("peach_recovery_pending");930 recoveryPending = (out as { peach_recovery_pending?: string })?.peach_recovery_pending === "1";931 } catch { }932 if (recoveryPending) {933 setForceRecoveryWords(true);934 setView("lock");935 triggerPipPulse();936 await restoreSettings();937 return;938 }939 setHasVault(true);940 triggerPipPulse();941 await restoreSettings();942 const celebrating = pendingCelebration();943 if (celebrating) {944 await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });945 setPendingCelebration(false);946 }947 await openVault(undefined, celebrating);948 if (celebrating) {949 setView("pro-celebration");950 window.setTimeout(() => setView("entry"), 2500);951 }952 }953954 function handleSelectEntry(entry: VaultEntry) {955 if (entry.type === "note") {956 setOpenNoteId(entry.id);957 setShowNotes(true);958 return;959 }960 setSelectedEntry(entry);961 setContentTransitioning(false);962 setContentPhase("enter");963 queueContentIdle();964 setView("entry");965 }966967 function handleAdd() {968 setEditEntry(undefined);969 setView("add");970 }971972 async function handleDelete() {973 const entry = selectedEntry();974 if (!entry) return;975 await sendMessage({ type: "DELETE_ENTRY", payload: { id: entry.id } });976 const loadedEntries = await loadEntries() ?? [];977 setSelectedEntry(loadedEntries[0] ?? null);978 setView("entry");979 }980981 async function handleDeleteMany(ids: string[]) {982 for (const id of ids) {983 await sendMessage({ type: "DELETE_ENTRY", payload: { id } });984 }985 const loadedEntries = await loadEntries() ?? [];986 987 const stillExists = loadedEntries.find(e => e.id === selectedEntry()?.id);988 if (!stillExists) setSelectedEntry(loadedEntries[0] ?? null);989 }990991 async function handleSave(savedEntry?: VaultEntry) {992 await openVault(savedEntry?.id);993 }994995 function handleBack() {996 const dest = editBackView();997 setEditBackView("entry");998 setEditEntry(undefined);999 setView(dest);1000 }10011002 function handleExportBack() {1003 if (returnToPanic()) {1004 setReturnToPanic(false);1005 setView("panic");1006 return;1007 }1008 setView("settings");1009 }10101011 function handleLock() {1012 clearCanarySuppressions();1013 setEntries([]);1014 setSelectedEntry(null);1015 setEditEntry(undefined);1016 setContentPhase("idle");1017 setView("lock");1018 }10191020 function handleHeaderLock() {1021 1022 1023 1024 handleLock();1025 void sendMessage({ type: "LOCK" });1026 }10271028 function navigate(dir: number) {1029 const allEntries = entries();1030 if (allEntries.length === 0) return;1031 const currentIndex = selectedIndex();1032 const baseIndex = currentIndex >= 0 ? currentIndex : 0;1033 const nextIndex = (baseIndex + dir + allEntries.length) % allEntries.length;1034 setSelectedEntry(allEntries[nextIndex]);1035 setContentPhase("enter");1036 queueContentIdle();1037 }10381039 function openList() {1040 setView("list");1041 }10421043 function openHealth() {1044 setEditEntry(undefined);1045 setView("health");1046 }10471048 function handleOpenFileImport() {1049 setRestoreView("file");1050 }10511052 const isShellOverlayOpen = () => view() !== "entry" || restoreView() !== null;10531054 function handleFixEntry(entry: VaultEntry) {1055 setSelectedEntry(entry);1056 1057 1058 if (isPasskeyOnly(entry)) {1059 setView("entry");1060 return;1061 }1062 setEditBackView(view()); 1063 setEditEntry(entry);1064 setView("add");1065 }10661067 const isLockedView = () => view() === "lock";10681069 async function handleApprovePendingPasskey() {1070 const pending = pendingPasskey();1071 if (!pending) return;1072 if (pending.browser === "firefox" && pending.type === "create") {1073 1074 1075 1076 1077 const { createFirefoxPasskey } = await import("./firefox-passkey");1078 const result = await createFirefoxPasskey(pending);1079 if (!result.ok) {1080 console.warn("Peach Firefox passkey create failed:", result.error);1081 }1082 await sendMessage({ type: "DENY_PASSKEY", payload: { requestId: pending.requestId } });1083 setPendingPasskey(null);1084 void loadEntries();1085 window.close();1086 return;1087 }1088 await sendMessage({ type: "APPROVE_PASSKEY", payload: { requestId: pending.requestId } });1089 window.close();1090 }10911092 async function handleDenyPendingPasskey() {1093 const pending = pendingPasskey();1094 if (!pending) return;1095 await sendMessage({ type: "DENY_PASSKEY", payload: { requestId: pending.requestId } });1096 window.close();1097 }10981099 function queueContentIdle(after?: () => void) {1100 if (contentPhaseTimer) window.clearTimeout(contentPhaseTimer);1101 contentPhaseTimer = window.setTimeout(() => {1102 setContentPhase("idle");1103 after?.();1104 }, ENTRY_TRANSITION_SETTLE_MS);1105 }11061107 return (1108 <div class="popup-root peach-theme">1109 <Show when={bootState() === "ready"}>1110 <Show1111 when={!pendingPasskey()}1112 fallback={1113 <PasskeyApprovalView1114 type={pendingPasskey()!.type}1115 rpName={pendingPasskey()!.rpName}1116 rpId={pendingPasskey()!.rpId}1117 userName={pendingPasskey()!.userName}1118 userDisplayName={pendingPasskey()!.userDisplayName}1119 onApprove={handleApprovePendingPasskey}1120 onDeny={handleDenyPendingPasskey}1121 />1122 }1123 >1124 <Show1125 when={!isLockedView()}1126 fallback={1127 <Show1128 when={onboarded() || hasVault()}1129 fallback={1130 <Onboarding1131 onComplete={() => { setOnboarded(true); }}1132 />1133 }1134 >1135 <LockView1136 hasVault={hasVault()}1137 ready={bootState() === "ready"}1138 isPro={proStatus()}1139 forceRecoveryWords={forceRecoveryWords()}1140 onUnlock={() => {1141 1142 1143 1144 if (forceRecoveryWords()) setForceRecoveryWords(false);1145 return handleUnlock();1146 }}1147 onBackToOnboarding={1148 !hasVault()1149 ? () => setOnboarded(false)1150 : undefined1151 }1152 onOpenCodexRestore={!hasVault() ? () => setView("codex-restore") : undefined}1153 />1154 </Show>1155 }1156 >1157 <Show1158 when={view() !== "pro-celebration"}1159 fallback={<ProCelebration />}1160 >1161 <section1162 id="hd-shell"1163 class="peach-shell peach-theme"1164 data-surface="extension"1165 >1166 <header class="peach-chrome" classList={{ "is-hidden-for-overlay": isShellOverlayOpen(), "is-collapsed": view() === "chirp-sync" }}>1167 <PeachWordmark unlocked />1168 <div class="peach-chrome-actions">1169 <button1170 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1171 onClick={() => { setOpenNoteId(undefined); setShowNotes(true); }}1172 aria-label="Notes"1173 title="Notes"1174 >1175 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">1176 <rect x="1.5" y="1.5" width="11" height="11" rx="2" />1177 <line x1="4" y1="5" x2="10" y2="5" />1178 <line x1="4" y1="7.5" x2="10" y2="7.5" />1179 <line x1="4" y1="10" x2="7" y2="10" />1180 </svg>1181 </button>1182 <button1183 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1184 onClick={() => setView("settings")}1185 aria-label="Settings"1186 >1187 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">1188 <path1189 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"1190 stroke="currentColor"1191 stroke-width="0.9"1192 />1193 <circle cx="7" cy="7" r="2.1" stroke="currentColor" stroke-width="0.9" />1194 </svg>1195 </button>1196 <button1197 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1198 onClick={handleAdd}1199 aria-label="New entry"1200 >1201 <svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round">1202 <path d="M5.5 1v9M1 5.5h9" />1203 </svg>1204 </button>1205 <button1206 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-lock-btn"1207 onClick={() => void handleHeaderLock()}1208 aria-label="Lock vault"1209 >1210 <svg class="peach-lock-icon" width="14" height="16" viewBox="0 0 14 16" fill="none">1211 <path1212 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"1213 stroke="currentColor"1214 stroke-width="1"1215 stroke-linecap="round"1216 />1217 <rect x="1.5" y="6" width="11" height="8.5" rx="2.2" stroke="currentColor" stroke-width="1" />1218 <circle cx="7" cy="10.25" r="1" fill="currentColor" />1219 </svg>1220 </button>1221 </div>1222 </header>12231224 <Show when={pendingProNotice() !== null}>1225 <div class="peach-pro-notice-banner">1226 <span>✦ {pendingProNotice()?.feature === "sync" ? "Sync" : "A Pro feature"} requires Peach Pro</span>1227 <button1228 class="peach-pro-notice-link"1229 onClick={() => {1230 setUpgradeHighlight((pendingProNotice()?.feature as ProFeature) ?? null);1231 setSettingsInitialSection("license");1232 setView("settings");1233 setPendingProNotice(null);1234 }}1235 >1236 Learn more →1237 </button>1238 <button class="peach-pro-notice-close" onClick={() => setPendingProNotice(null)} aria-label="Dismiss">✕</button>1239 </div>1240 </Show>12411242 <div class="peach-shell-body">1243 <div class="peach-shell-veil"></div>1244 <div class="peach-content-frame" data-content-phase={contentPhase()} classList={{ navigating: contentTransitioning() }}>1245 <EntryView1246 entry={selectedEntry()}1247 currentTabUrl={currentTabUrl()}1248 entries={entries()}1249 isPro={proStatus()}1250 onDelete={handleDelete}1251 onFieldSaved={() => void loadEntries()}1252 onEscape={() => { setView("list"); }}1253 onProGate={(feature) => {1254 setUpgradeHighlight(feature);1255 setSettingsInitialSection("license");1256 setView("settings");1257 }}1258 />1259 </div>12601261 <section class="peach-search-wrap" classList={{ "is-hidden-for-overlay": isShellOverlayOpen(), "is-collapsed": view() === "chirp-sync" }}>1262 <div class="peach-search-bar">1263 <span class="peach-search-icon">1264 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">1265 <circle cx="5.5" cy="5.5" r="3.75" />1266 <path d="M8.75 8.75L11.5 11.5" />1267 </svg>1268 </span>1269 <input1270 class="peach-search-input"1271 placeholder="search vault..."1272 autocomplete="off"1273 aria-label="Search vault entries"1274 onClick={openList}1275 onFocus={openList}1276 readOnly1277 />1278 <div class="peach-search-meta">1279 <span class="peach-search-counter">1280 {entries().length > 01281 ? `${selectedIndex() + 1} / ${entries().length}`1282 : "0 entries"}1283 </span>1284 <span class="peach-divider"></span>1285 <button1286 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1287 onClick={() => navigate(-1)}1288 aria-label="Previous entry"1289 disabled={entries().length === 0}1290 >1291 <svg width="6" height="11" viewBox="0 0 6 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1292 <path d="M5 1L1 5.5L5 10" />1293 </svg>1294 </button>1295 <button1296 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1297 onClick={() => navigate(1)}1298 aria-label="Next entry"1299 disabled={entries().length === 0}1300 >1301 <svg width="6" height="11" viewBox="0 0 6 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1302 <path d="M1 1L5 5.5L1 10" />1303 </svg>1304 </button>1305 </div>1306 </div>1307 </section>1308 </div>13091310 {13111312}1313 <Show when={!isNativePlatform() && view() === "list"}>1314 <VaultView1315 entries={entries()}1316 selectedEntryId={selectedEntry()?.id}1317 currentTabDomain={currentTabDomain()}1318 sortOrder={sortOrder()}1319 searchSeed={listSearchSeed()}1320 searchSeedNonce={listSearchSeedNonce()}1321 onSelectEntry={handleSelectEntry}1322 onSortOrderChange={setSortOrder}1323 onAdd={handleAdd}1324 onClose={() => setView("entry")}1325 onOpenTrash={() => setView("trash")}1326 onDeleteMany={handleDeleteMany}1327 />1328 </Show>1329 <Show when={isNativePlatform() && hasVault()}>1330 <Portal>1331 <div1332 class="peach-vault-sheet"1333 classList={{ "is-peeking": view() === "entry", "is-open": view() === "list", "is-dragging": sheetTranslate() !== null }}1334 style={(() => {1335 const live = sheetTranslate();1336 if (live === null) return undefined;1337 return { transform: `translateY(${live}px)` };1338 })()}1339 >1340 <VaultView1341 entries={entries()}1342 selectedEntryId={selectedEntry()?.id}1343 currentTabDomain={currentTabDomain()}1344 sortOrder={sortOrder()}1345 searchSeed={listSearchSeed()}1346 searchSeedNonce={listSearchSeedNonce()}1347 isNative={true}1348 onSelectEntry={handleSelectEntry}1349 onSortOrderChange={setSortOrder}1350 onAdd={handleAdd}1351 onClose={() => setView("entry")}1352 onOpenTrash={() => setView("trash")}1353 onDeleteMany={handleDeleteMany}1354 />1355 </div>1356 </Portal>1357 </Show>1358 <Show when={view() === "settings" || settingsExiting()}>1359 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1360 <SettingsView1361 entriesCount={entries().length}1362 breachMonitoring={breachMonitoring()}1363 lockOnClose={lockOnClose()}1364 autoLockMinutes={autoLockMinutes()}1365 trashRetentionDays={trashRetentionDays()}1366 theme={theme()}1367 accentColor={accentColor()}1368 textSize={textSize()}1369 reduceMotion={reduceMotion()}1370 isPro={proStatus()}1371 initialSection={settingsInitialSection()}1372 upgradeHighlight={upgradeHighlight()}1373 isExiting={settingsExiting()}1374 onBreachMonitoringChange={applyBreachMonitoring}1375 onLockOnCloseChange={applyLockOnClose}1376 onAutoLockMinutesChange={applyAutoLockMinutes}1377 onTrashRetentionChange={applyTrashRetention}1378 onThemeChange={applyTheme}1379 onAccentChange={applyAccentColor}1380 onTextSizeChange={applyTextSize}1381 onReduceMotionChange={applyReduceMotion}1382 shortcuts={shortcutBindings()}1383 onShortcutsChange={applyShortcuts}1384 onLicenseChange={async () => {1385 1386 1387 1388 1389 1390 await refreshProStatus();1391 void loadEntries();1392 const should = await sendMessage({ type: "SHOULD_PLAY_CELEBRATION" });1393 if ((should as { ok: boolean; data?: boolean }).data === true) {1394 await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });1395 setView("pro-celebration");1396 window.setTimeout(() => setView("settings"), 2500);1397 }1398 }}1399 onBack={() => {1400 setSettingsExiting(true);1401 window.setTimeout(() => {1402 setSettingsExiting(false);1403 setSettingsInitialSection(null);1404 setUpgradeHighlight(null);1405 handleBack();1406 }, 200);1407 }}1408 onImport={() => setView("import")}1409 onExport={() => setView("export")}1410 onBreachCheckDone={loadEntries}1411 onOpenHealth={openHealth}1412 onOpenSyncPairing={() => { setSettingsInitialSection(settingsActiveCard()); setView("chirp-sync"); }}1413 onOpenFileImport={handleOpenFileImport}1414 onOpenPanic={() => setView("panic")}1415 onOpenCodexGenerate={() => { setSettingsInitialSection(settingsActiveCard()); setView("codex-generate"); }}1416 onOpenCodexRestore={() => setView("codex-restore")}1417 onCardChange={setSettingsActiveCard}1418 onProGate={(feature) => {1419 setUpgradeHighlight(feature);1420 setSettingsInitialSection("license");1421 }}1422 />1423 </Suspense>1424 </Show>1425 <Show when={view() === "codex-generate"}>1426 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1427 <CodexGenerateView1428 isPro={proStatus()}1429 onBack={() => setView("settings")}1430 onUpgradePro={() => { setSettingsInitialSection("license"); setView("settings"); }}1431 onOpenSecurity={() => { setSettingsInitialSection("security"); setView("settings"); }}1432 onVaultLocked={() => handleLock()}1433 />1434 </Suspense>1435 </Show>1436 <Show when={view() === "codex-restore"}>1437 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1438 <CodexRestoreView1439 onBack={() => setView("lock")}1440 onRestoreSuccess={async () => {1441 1442 1443 1444 await handleUnlock();1445 }}1446 />1447 </Suspense>1448 </Show>1449 <Show when={view() === "chirp-sync"}>1450 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1451 <ChirpSyncView1452 isPro={proStatus()}1453 onBack={() => setView("settings")}1454 onLearnPro={() => { setSettingsInitialSection("license"); setView("settings"); }}1455 />1456 </Suspense>1457 </Show>1458 <Show when={view() === "add"}>1459 <AddView1460 entry={editEntry()}1461 entries={entries()}1462 onBack={handleBack}1463 onSave={handleSave}1464 />1465 </Show>1466 <Show when={view() === "import"}>1467 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1468 <ImportView onBack={() => setView("settings")} onImport={handleSave} />1469 </Suspense>1470 </Show>1471 <Show when={view() === "export"}>1472 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1473 <ExportView onBack={handleExportBack} />1474 </Suspense>1475 </Show>1476 <Show when={view() === "panic"}>1477 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1478 <PanicView1479 onCancel={() => setView("settings")}1480 onOpenExport={() => {1481 setReturnToPanic(true);1482 setView("export");1483 }}1484 onDestroyed={async () => {1485 clearCanarySuppressions();1486 setEntries([]);1487 setSelectedEntry(null);1488 setEditEntry(undefined);1489 setHasVault(false);1490 setOnboarded(false);1491 setProStatus(false);1492 setPendingPasskey(null);1493 setUpgradeHighlight(null);1494 setSettingsInitialSection(null);1495 setPendingProNotice(null);1496 setPendingCelebration(false);1497 setReturnToPanic(false);1498 setContentPhase("idle");1499 setView("lock");1500 }}1501 />1502 </Suspense>1503 </Show>1504 {1505}1506 <Show when={view() === "health" || (view() === "add" && editBackView() === "health")}>1507 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1508 <HealthView1509 entries={entries()}1510 isPro={proStatus()}1511 onFixEntry={handleFixEntry}1512 onBack={() => setView("settings")}1513 onRefresh={loadEntries}1514 hidden={view() !== "health"}1515 />1516 </Suspense>1517 </Show>1518 <Show when={view() === "trash"}>1519 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1520 <TrashView1521 onBack={() => setView("list")}1522 onRestored={() => void loadEntries()}1523 />1524 </Suspense>1525 </Show>1526 <Show when={showNotes()}>1527 <div class={`peach-notes-overlay${showNotes() ? " is-visible" : ""}`}>1528 <NotesMode1529 notes={entries().filter((e) => e.type === "note")}1530 initialNoteId={openNoteId()}1531 onBack={() => { setShowNotes(false); setOpenNoteId(undefined); }}1532 onNoteListChanged={() => void loadEntries()}1533 />1534 </div>1535 </Show>1536 <Show when={restoreView() === "file"}>1537 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1538 <ImportView1539 mode="onboarding"1540 replacesExistingVault={true}1541 onBack={() => setRestoreView(null)}1542 onImport={async () => {1543 setRestoreView(null);1544 await handleUnlock();1545 }}1546 />1547 </Suspense>1548 </Show>1549 </section>1550 </Show>1551 </Show>1552 </Show>1553 </Show>1554 <Show when={showShortcutsHelp()}>1555 <ShortcutsHelpOverlay1556 bindings={shortcutBindings()}1557 onClose={() => setShowShortcutsHelp(false)}1558 />1559 </Show>1560 </div>1561 );1562}15631564function ShortcutsHelpOverlay(props: {1565 bindings: Record<ShortcutAction, Keybinding>;1566 onClose: () => void;1567}) {1568 return (1569 <div class="peach-shortcuts-help-backdrop" onClick={props.onClose}>1570 <div class="peach-shortcuts-help-card" onClick={(e) => e.stopPropagation()}>1571 <div class="peach-shortcuts-help-head">1572 <h3>Keyboard shortcuts</h3>1573 <button class="peach-shortcuts-help-close" onClick={props.onClose} aria-label="Close">✕</button>1574 </div>1575 <div class="peach-shortcuts-help-body">1576 <p class="peach-settings-sub-label">ENTRY VIEW</p>1577 <ul>1578 {SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry").map((d) => (1579 <li>1580 <span>{d.label}</span>1581 <kbd>{formatKeybinding(props.bindings[d.action])}</kbd>1582 </li>1583 ))}1584 </ul>1585 <p class="peach-settings-sub-label">ANYWHERE</p>1586 <ul>1587 {SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global").map((d) => (1588 <li>1589 <span>{d.label}</span>1590 <kbd>{formatKeybinding(props.bindings[d.action])}</kbd>1591 </li>1592 ))}1593 </ul>1594 </div>1595 </div>1596 </div>1597 );1598}