Settings.tsx

77.1 KB · raw

1import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2import { sendMessage } from "../App";3import { PeachMark } from "../components/PeachMark";4import { LockRuneCanvas } from "../components/LockRuneCanvas";5import { RecoveryWordDisplayView } from "./RecoveryWordDisplay";6import type { Canary } from "../../shared/types";7import { ACCENT_COLORS } from "../../shared/types";8import type {9  AccentColorName,10  PasskeyProviderState,11  ProFeature,12  VaultSnapshot,13} from "../../shared/types";14import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types";15import {16  DEFAULT_BINDINGS,17  findConflict,18  formatKeybinding,19  keybindingFromEvent,20  SHORTCUT_DESCRIPTORS,21  type Keybinding,22  type ShortcutAction,23} from "../../shared/shortcuts";2425interface LicenseInfo {26  valid: boolean;27  plan: "free" | "pro";28  issued?: string;29}3031interface SettingsProps {32  entriesCount: number;33  breachMonitoring: boolean;34  lockOnClose: boolean;35  autoLockMinutes: number;36  trashRetentionDays: number;37  theme: "dark" | "light";38  accentColor: AccentColorName;39  displayFont: "cormorant-italic" | "cormorant" | "inter" | "system";40  textSize: "small" | "medium" | "large";41  reduceMotion: boolean;42  isPro?: boolean;43  initialSection?: string | null;44  upgradeHighlight?: ProFeature | null;45  onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;46  onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;47  onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;48  onTrashRetentionChange: (days: number) => void | Promise<void>;49  onThemeChange: (theme: "dark" | "light") => void;50  onAccentChange: (name: AccentColorName) => void | Promise<void>;51  onDisplayFontChange: (font: "cormorant-italic" | "cormorant" | "inter" | "system") => void | Promise<void>;52  onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise<void>;53  onReduceMotionChange: (enabled: boolean) => void | Promise<void>;54  shortcuts: Record<ShortcutAction, Keybinding>;55  onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;56  onLicenseChange?: () => void | Promise<void>;57  onBack: () => void;58  onImport: () => void;59  onExport: () => void;60  onBreachCheckDone: () => void;61  onOpenHealth?: () => void;62  onOpenSyncPairing?: () => void;63  onOpenFileImport?: () => void;64  onOpenPanic?: () => void;65}6667type SectionName =68  | "security"69  | "vault"70  | "sync"71  | "appearance"72  | "shortcuts"73  | "license"74  | "feedback";7576const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7778function formatActivatedSince(issued: string | undefined): string {79  if (!issued) return "Active";80  try {81    const d = new Date(issued);82    if (Number.isNaN(d.getTime())) return "Active";83    return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;84  } catch {85    return "Active";86  }87}8889function isSectionName(value: string): value is SectionName {90  return (91    value === "security" ||92    value === "vault" ||93    value === "sync" ||94    value === "appearance" ||95    value === "shortcuts" ||96    value === "license" ||97    value === "feedback"98  );99}100101export function SettingsView(props: SettingsProps) {102  const version = chrome.runtime.getManifest().version;103  const initial = props.initialSection;104  const [activeSection, setActiveSection] = createSignal<SectionName | null>(105    initial && isSectionName(initial) ? (initial as SectionName) : null106  );107108  const [checkingBreaches, setCheckingBreaches] = createSignal(false);109  const [breachResult, setBreachResult] = createSignal<string | null>(null);110  const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);111  const [licenseKeyInput, setLicenseKeyInput] = createSignal("");112  const [licenseError, setLicenseError] = createSignal<string | null>(null);113  const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);114  const [activatingLicense, setActivatingLicense] = createSignal(false);115116  const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);117  const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);118  const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");119  const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);120  // Prefer the prop (cached at App boot), fall back to the sync-state probe.121  // The prop is authoritative once present so license activation reflects122  // immediately without waiting for a sync round-trip.123  const [syncProStatus, setSyncProStatus] = createSignal(false);124  const isPro = () => props.isPro ?? syncProStatus();125  const [syncingNow, setSyncingNow] = createSignal(false);126  const [syncFeedback, setSyncFeedback] = createSignal<127    { kind: "ok" | "err"; text: string } | null128  >(null);129  const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);130  const [passkeySaving, setPasskeySaving] = createSignal(false);131132  const [dedupCount, setDedupCount] = createSignal<number | null>(null);133  const [dedupRunning, setDedupRunning] = createSignal(false);134  const [dedupDone, setDedupDone] = createSignal<number | null>(null);135136  const [declinedCardCount, setDeclinedCardCount] = createSignal(0);137138  // Vault history (peach_vault_history) — last 10 snapshots, snapshotted139  // BEFORE every real-mode vault write so the user can roll back if a sync140  // or import overwrites the wrong state.141  const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);142  const [historyLoading, setHistoryLoading] = createSignal(false);143  const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);144145  async function loadVaultHistory() {146    setHistoryLoading(true);147    try {148      const res = await sendMessage({ type: "GET_VAULT_HISTORY" });149      const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];150      setHistorySnapshots(list);151    } finally {152      setHistoryLoading(false);153    }154  }155156  async function handleRestoreVaultSnapshot(id: string) {157    if (historyRestoring()) return;158    setHistoryRestoring(id);159    const res = await sendMessage({160      type: "RESTORE_VAULT_SNAPSHOT",161      payload: { snapshotId: id },162    });163    setHistoryRestoring(null);164    if ((res as { ok: boolean }).ok) {165      // Vault is now locked — let the parent close Settings; App.tsx's166      // VAULT_LOCKED listener will route back to the lock screen.167      props.onBack();168    }169  }170171  function formatSnapshotTime(savedAt: number): string {172    const diff = Date.now() - savedAt;173    if (diff < 60_000) return "Just now";174    if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;175    const d = new Date(savedAt);176    const isToday = new Date().toDateString() === d.toDateString();177    const isYest =178      new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();179    const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });180    if (isToday) return `Today ${t}`;181    if (isYest) return `Yesterday ${t}`;182    return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;183  }184185  function reasonLabel(reason: VaultSnapshot["reason"]): string {186    return (187      {188        local_save: "saved",189        sync_received: "synced",190        import: "imported",191        restore: "restored",192      } as const193    )[reason] ?? reason;194  }195196  const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);197198  const [showChangePw, setShowChangePw] = createSignal(false);199  const [changePwCurrent, setChangePwCurrent] = createSignal("");200  const [changePwNew, setChangePwNew] = createSignal("");201  const [changePwConfirm, setChangePwConfirm] = createSignal("");202  const [changePwSaving, setChangePwSaving] = createSignal(false);203  const [changePwError, setChangePwError] = createSignal<string | null>(null);204  const [changePwSuccess, setChangePwSuccess] = createSignal(false);205  const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);206207  const [duressEnabled, setDuressEnabled] = createSignal(false);208  const [showDuressSetup, setShowDuressSetup] = createSignal(false);209  const [duressPassword, setDuressPassword] = createSignal("");210  const [duressConfirm, setDuressConfirm] = createSignal("");211  const [duressMainPassword, setDuressMainPassword] = createSignal("");212  const [duressSaving, setDuressSaving] = createSignal(false);213  const [duressError, setDuressError] = createSignal<string | null>(null);214  const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);215216  const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);217218  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);219  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);220221  const [fbCategory, setFbCategory] = createSignal("Bug report");222  const [fbMessage, setFbMessage] = createSignal("");223  const [fbSubmitting, setFbSubmitting] = createSignal(false);224  const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);225226  onMount(async () => {227    const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([228      sendMessage({ type: "GET_LICENSE" }),229      sendMessage({ type: "GET_SYNC_STATE" }),230      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),231      sendMessage({ type: "IS_DURESS_ENABLED" }),232      sendMessage({ type: "GET_SETTINGS" }),233      sendMessage({ type: "GET_LICENSE_KEY" }),234    ]);235236    // Vault history loads in the background — the Vault sub-view picks it up237    // when the user navigates in. Loading it lazily keeps Settings boot fast.238    void loadVaultHistory();239240    if ((licenseRes as { ok: boolean }).ok) {241      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);242    }243244    if ((syncRes as { ok: boolean }).ok) {245      const data = syncRes as { ok: boolean; data?: {246        pairedDevices?: PairedDevice[];247        trustedNetworks?: TrustedNetwork[];248        status?: SyncStatus;249        lastSyncAt?: number;250        isPro?: boolean;251      }};252      if (data.data) {253        setPairedDevices(data.data.pairedDevices ?? []);254        setTrustedNetworks(data.data.trustedNetworks ?? []);255        setSyncStatus(data.data.status ?? "offline");256        setLastSyncAt(data.data.lastSyncAt);257        setSyncProStatus(data.data.isPro ?? false);258      }259    }260261    if ((passkeyRes as { ok: boolean }).ok) {262      setPasskeyState(263        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null264      );265    }266267    if ((duressRes as { ok: boolean }).ok) {268      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);269    }270271    if ((settingsRes as { ok: boolean }).ok) {272      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;273      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);274    }275276    if ((keyRes as { ok: boolean }).ok) {277      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);278    }279280    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });281    if ((entriesRes as { ok: boolean }).ok) {282      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];283      setCanaryEntries(284        entries285          .filter((e) => e.canary?.webhookUrl)286          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))287      );288    }289290    chrome.runtime.onMessage.addListener((msg) => {291      if (msg.type === "SYNC_STATE_UPDATE") {292        setPairedDevices(msg.pairedDevices ?? []);293        setTrustedNetworks(msg.trustedNetworks ?? []);294        setSyncStatus(msg.status ?? "offline");295        setLastSyncAt(msg.lastSyncAt);296        setSyncProStatus(msg.isPro ?? false);297      }298    });299  });300301  async function handleChangePassword() {302    setChangePwError(null);303    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }304    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }305    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }306    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }307    setChangePwSaving(true);308    const res = await sendMessage({309      type: "CHANGE_PASSWORD",310      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },311    }) as { ok: boolean; error?: string };312    if (res.ok) {313      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");314      setShowChangePw(false);315      setShowNewRecoveryWords(true);316    } else {317      setChangePwError(res.error ?? "failed to change password");318    }319    setChangePwSaving(false);320  }321322  const [copied, setCopied] = createSignal(false);323324  async function copyLicenseKey(key: string) {325    await navigator.clipboard.writeText(key);326    setCopied(true);327    setTimeout(() => setCopied(false), 2000);328  }329330  async function handleActivateLicense() {331    const key = licenseKeyInput().trim();332    if (!key) return;333    setActivatingLicense(true);334    setLicenseError(null);335    setLicenseSuccess(null);336    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });337    setActivatingLicense(false);338    if ((res as { ok: boolean }).ok) {339      const data = (res as { ok: boolean; data?: LicenseInfo }).data;340      if (data?.valid) {341        setLicenseInfo(data);342        setLicenseKey(key);343        setLicenseSuccess("License activated!");344        setLicenseKeyInput("");345      } else {346        setLicenseError("Invalid license key");347      }348    } else {349      setLicenseError("Activation failed");350    }351  }352353  async function handleTogglePasskeys() {354    const current = passkeyState();355    if (!current || !current.supported) return;356    setPasskeySaving(true);357    const res = await sendMessage({358      type: "SET_PASSKEY_PROVIDER_ENABLED",359      payload: { enabled: !current.enabled },360    });361    setPasskeySaving(false);362    if ((res as { ok: boolean }).ok) {363      setPasskeyState(364        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null365      );366    }367  }368369  async function handleCheckAllBreaches() {370    setCheckingBreaches(true);371    setBreachResult(null);372    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });373    setCheckingBreaches(false);374    if ((res as { ok: boolean }).ok) {375      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};376      const breached = Object.values(data).filter((count) => count > 0).length;377      setBreachResult(378        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"379      );380      props.onBreachCheckDone();381    }382  }383384  async function handleSyncNow() {385    if (syncStatus() === "not_trusted") return;386    setSyncingNow(true);387    setSyncFeedback(null);388    const res = await sendMessage({ type: "TRIGGER_SYNC" });389    const ok = (res as { ok: boolean }).ok;390    if (ok) {391      const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;392      const count = data?.syncedCount ?? 0;393      setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });394      setLastSyncAt(Date.now());395      void refreshSyncState();396    } else {397      const error = (res as { error?: string }).error;398      const map: Record<string, string> = {399        no_paired_devices: "No paired devices",400        no_devices_reachable: "No devices found",401        vault_locked: "Vault is locked",402        pro_required: "Sync requires Pro",403      };404      setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });405    }406    setTimeout(() => {407      setSyncingNow(false);408      setSyncFeedback(null);409    }, 3500);410  }411412  async function refreshSyncState() {413    const res = await sendMessage({ type: "GET_SYNC_STATE" });414    const data = (res as { ok: boolean; data?: {415      pairedDevices?: PairedDevice[];416      trustedNetworks?: TrustedNetwork[];417      status?: SyncStatus;418      lastSyncAt?: number;419    }}).data;420    if (!data) return;421    setPairedDevices(data.pairedDevices ?? []);422    setTrustedNetworks(data.trustedNetworks ?? []);423    setSyncStatus(data.status ?? "offline");424    setLastSyncAt(data.lastSyncAt);425  }426427  async function handleRemoveDevice(deviceId: string) {428    setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));429    await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });430    void refreshSyncState();431  }432433  async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {434    await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });435    void refreshSyncState();436  }437438  const [markingNetwork, setMarkingNetwork] = createSignal(false);439440  async function handleMarkNetworkTrusted() {441    if (markingNetwork()) return;442    setMarkingNetwork(true);443    await sendMessage({ type: "MARK_NETWORK_TRUSTED" });444    await refreshSyncState();445    setMarkingNetwork(false);446  }447448  const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);449  const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);450451  async function handleResetSyncIdentity() {452    setResetIdentityRunning(true);453    const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });454    setResetIdentityRunning(false);455    if ((res as { ok: boolean }).ok) {456      setPairedDevices([]);457      setResetIdentityConfirm(false);458    }459  }460461  function formatRelativeTime(timestamp?: number): string {462    if (!timestamp) return "Never";463    const diff = Date.now() - timestamp;464    const minutes = Math.floor(diff / 60000);465    const hours = Math.floor(diff / 3600000);466    const days = Math.floor(diff / 86400000);467    if (minutes < 1) return "Just now";468    if (minutes < 60) return `${minutes}m ago`;469    if (hours < 24) return `${hours}h ago`;470    return `${days}d ago`;471  }472473  function getStatusColor(status: SyncStatus): string {474    switch (status) {475      case "synced": return "var(--peach-sync-ok)";476      case "syncing": return "var(--peach-sync-busy)";477      case "no_peers": return "var(--peach-sync-off)";478      case "not_trusted": return "var(--peach-sync-err)";479      case "offline": return "var(--peach-sync-off)";480      default: return "var(--peach-sync-off)";481    }482  }483484  function getStatusLabel(status: SyncStatus): string {485    switch (status) {486      case "synced": return "Synced";487      case "syncing": return "Syncing";488      case "no_peers": return "No peers visible";489      case "not_trusted": return "Not on a trusted network";490      case "offline": return "Offline";491      default: return "Unknown";492    }493  }494495  function isCurrentNetworkTrusted(): boolean {496    return trustedNetworks().length > 0;497  }498499  async function handleSetupDuress() {500    const mainPw = duressMainPassword().trim();501    const duressPw = duressPassword().trim();502    const confirmPw = duressConfirm().trim();503504    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }505    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }506    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }507    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }508509    setDuressSaving(true);510    setDuressError(null);511512    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };513    if (!verifyRes.ok || !verifyRes.data) {514      setDuressError("incorrect main password");515      setDuressSaving(false);516      return;517    }518519    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };520    setDuressSaving(false);521    if (res.ok) {522      setDuressEnabled(true);523      setShowDuressSetup(false);524      setDuressPassword("");525      setDuressConfirm("");526      setDuressMainPassword("");527      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");528      setTimeout(() => setDuressSuccess(null), 5000);529    } else {530      setDuressError(res.error ?? "failed to set duress password");531    }532  }533534  async function handleClearDeclinedCards() {535    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });536    setDeclinedCardCount(0);537  }538539  async function handleCountDuplicates() {540    setDedupRunning(true);541    setDedupDone(null);542    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };543    setDedupRunning(false);544    if (res.ok) setDedupCount(res.data ?? 0);545  }546547  async function handleDeduplicate() {548    setDedupRunning(true);549    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };550    setDedupRunning(false);551    if (res.ok) {552      setDedupDone(res.data ?? 0);553      setDedupCount(0);554    }555  }556557  async function handleDisableDuress() {558    const mainPw = duressMainPassword().trim();559    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }560561    setDuressSaving(true);562    setDuressError(null);563564    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };565    if (!verifyRes.ok || !verifyRes.data) {566      setDuressError("incorrect main password");567      setDuressSaving(false);568      return;569    }570571    const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };572    setDuressSaving(false);573    if (res.ok) {574      setDuressEnabled(false);575      setShowDuressSetup(false);576      setDuressMainPassword("");577      setDuressSuccess("Duress password disabled.");578      setTimeout(() => setDuressSuccess(null), 4000);579    } else {580      setDuressError(res.error ?? "failed to disable duress password");581    }582  }583584  async function handleSubmitFeedback() {585    if (!fbMessage().trim() || fbSubmitting()) return;586    setFbSubmitting(true);587    setFbResult(null);588    const res = await sendMessage({589      type: "SUBMIT_FEEDBACK",590      payload: {591        category: fbCategory(),592        message: fbMessage().trim(),593        version,594        browser: navigator.userAgent,595      },596    }) as { ok: boolean; error?: string };597    setFbSubmitting(false);598    if (res.ok) {599      setFbResult("success");600      setFbMessage("");601      setTimeout(() => setFbResult(null), 4000);602    } else {603      setFbResult("error");604    }605  }606607  function handleHeaderBack() {608    if (activeSection() !== null) {609      setActiveSection(null);610    } else {611      props.onBack();612    }613  }614615  function currentTitle(): string {616    const s = activeSection();617    if (s === null) return "settings";618    if (s === "license") return "account";619    return s;620  }621622  function renderCatRow(opts: {623    section: SectionName;624    name: string;625    desc: string;626    icon: () => any;627    pro?: boolean;628  }) {629    return (630      <button631        class="peach-settings-cat-row"632        onClick={() => setActiveSection(opts.section)}633      >634        <div class="peach-settings-cat-icon">{opts.icon()}</div>635        <div class="peach-settings-cat-content">636          <span class="peach-settings-cat-name">637            {opts.name}638            <Show when={opts.pro && !isPro()}>639              <span class="peach-pro-mark" aria-label="Pro feature">✦</span>640            </Show>641          </span>642          <span class="peach-settings-cat-desc">{opts.desc}</span>643        </div>644        <svg645          class="peach-settings-cat-chevron"646          width="6" height="10" viewBox="0 0 6 10" fill="none"647          stroke="currentColor" stroke-width="1.1"648          stroke-linecap="round" stroke-linejoin="round"649        >650          <path d="M1 1l4 4-4 4" />651        </svg>652      </button>653    );654  }655656  const iconShield = () => (657    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">658      <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>659    </svg>660  );661  const iconKey = () => (662    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">663      <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>664    </svg>665  );666  const iconLock = () => (667    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">668      <rect x="3" y="7" width="10" height="7" rx="1.5"/>669      <path d="M6 7V5a3 3 0 016 0v2"/>670    </svg>671  );672  const iconSync = () => (673    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">674      <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>675      <path d="M14 6l-2 2-2-2"/>676      <path d="M2 10l2-2 2 2"/>677    </svg>678  );679  const iconSun = () => (680    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">681      <circle cx="8" cy="8" r="3"/>682      <path d="M8 2v1m0 10v1M2 8h1m10 0h1m-2.1-4.1-.7.7M4.8 11.3l-.7.7m0-7.7.7.7m6.5 6.3-.7.7"/>683    </svg>684  );685  const iconKeys = () => (686    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">687      <rect x="2" y="5" width="5" height="4" rx="1"/>688      <rect x="9" y="7" width="5" height="4" rx="1"/>689      <path d="M7 7h2m-2 2h2"/>690    </svg>691  );692  const iconBubble = () => (693    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">694      <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>695    </svg>696  );697698  function renderHome() {699    return (700      <div class="peach-settings-body">701        <p class="peach-settings-nav-label">PROTECTION</p>702        {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}703        {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}704705        <p class="peach-settings-nav-label">VAULT</p>706        {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}707        {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}708709        <p class="peach-settings-nav-label">PREFERENCES</p>710        {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}711        {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}712713        <p class="peach-settings-nav-label">SUPPORT</p>714        {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}715716        <div class="peach-settings-footer">717          <span>Peach {version}</span>718          <span class="peach-divider"></span>719          <span>Browser extension</span>720        </div>721      </div>722    );723  }724725  function renderSecurity() {726    return (727      <div class="peach-settings-body">728        <div class="peach-settings-group">729          <p class="peach-settings-sub-label">LOCKING</p>730          <div class="peach-settings-row">731            <span>Lock on close</span>732            <button733              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}734              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}735              aria-label="Toggle lock on close"736            >737              <span class="peach-toggle-knob"></span>738            </button>739          </div>740          <div class="peach-settings-row">741            <span>Auto-lock after</span>742            <select743              class="peach-settings-select"744              value={String(props.autoLockMinutes)}745              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}746            >747              <option value="1">1 minute</option>748              <option value="5">5 minutes</option>749              <option value="15">15 minutes</option>750              <option value="30">30 minutes</option>751              <option value="0">Never</option>752            </select>753          </div>754755          <p class="peach-settings-sub-label">BREACH PROTECTION</p>756          <div class="peach-settings-row">757            <span>Breach monitoring</span>758            <button759              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}760              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}761              aria-label="Toggle breach monitoring"762            >763              <span class="peach-toggle-knob"></span>764            </button>765          </div>766          <div class="peach-settings-row peach-settings-row--meta">767            <span>Check all passwords</span>768            <button769              class="peach-btn peach-btn--subtle peach-btn--sm"770              onClick={() => void handleCheckAllBreaches()}771              disabled={checkingBreaches()}772            >773              {checkingBreaches() ? "checking..." : "run"}774            </button>775          </div>776          <Show when={breachResult()}>777            <div class="peach-settings-row peach-settings-row--meta">778              <span>Last check</span>779              <span class="peach-settings-value">{breachResult()}</span>780            </div>781          </Show>782783          <p class="peach-settings-sub-label">MASTER PASSWORD</p>784          <div class="peach-settings-row peach-settings-row--meta">785            <span>Master password</span>786            <button787              class="peach-btn peach-btn--subtle peach-btn--sm"788              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}789            >790              {showChangePw() ? "cancel" : "change"}791            </button>792          </div>793          <Show when={showChangePw()}>794            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">795              <input796                class="peach-settings-input"797                type="password"798                placeholder="current password"799                value={changePwCurrent()}800                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}801                autocomplete="current-password"802              />803              <input804                class="peach-settings-input"805                type="password"806                placeholder="new password (min 12 chars)"807                value={changePwNew()}808                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}809                autocomplete="new-password"810              />811              <input812                class="peach-settings-input"813                type="password"814                placeholder="confirm new password"815                value={changePwConfirm()}816                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}817                autocomplete="new-password"818                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}819              />820              <Show when={changePwError()}>821                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>822              </Show>823              <Show when={changePwSuccess()}>824                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>825              </Show>826              <button827                class="peach-btn peach-btn--accent peach-btn--sm"828                style="align-self:flex-start"829                onClick={() => void handleChangePassword()}830                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}831              >832                {changePwSaving() ? "saving…" : "save new password"}833              </button>834            </div>835          </Show>836837          <p class="peach-settings-sub-label">DURESS PASSWORD</p>838          <div class="peach-settings-row peach-settings-row--top">839            <div class="peach-settings-row-main">840              <span>Duress password</span>841              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">842                If entered under coercion, shows a decoy empty vault instead of your real one.843              </span>844            </div>845            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>846              {duressEnabled() ? "on" : "off"}847            </div>848          </div>849850          <Show when={duressEnabled() && !showDuressSetup()}>851            <div class="peach-settings-row peach-settings-row--meta">852              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">853                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.854              </span>855            </div>856          </Show>857858          <Show when={duressSuccess()}>859            <div class="peach-settings-row peach-settings-row--meta">860              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>861            </div>862          </Show>863864          <Show when={!showDuressSetup()}>865            <div class="peach-settings-row peach-settings-row--meta">866              <button867                class="peach-btn peach-btn--subtle peach-btn--sm"868                onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}869              >870                {duressEnabled() ? "Change / disable" : "Set up"}871              </button>872            </div>873          </Show>874875          <Show when={showDuressSetup()}>876            <div class="peach-duress-form">877              <label class="peach-label">main password (to confirm)</label>878              <input879                class="peach-settings-input"880                type="password"881                placeholder="your current master password"882                autocomplete="current-password"883                value={duressMainPassword()}884                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}885              />886              <Show when={!duressEnabled()}>887                <label class="peach-label">new duress password</label>888                <input889                  class="peach-settings-input"890                  type="password"891                  placeholder="duress password (min 8 chars)"892                  autocomplete="new-password"893                  value={duressPassword()}894                  onInput={(e) => setDuressPassword(e.currentTarget.value)}895                />896                <label class="peach-label">confirm duress password</label>897                <input898                  class="peach-settings-input"899                  type="password"900                  placeholder="confirm duress password"901                  autocomplete="new-password"902                  value={duressConfirm()}903                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}904                />905              </Show>906907              <Show when={duressError()}>908                <p class="peach-hint peach-hint-error">{duressError()}</p>909              </Show>910911              <div class="peach-duress-actions">912                <button913                  class="peach-btn peach-btn--subtle peach-btn--sm"914                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}915                >916                  cancel917                </button>918                <Show when={duressEnabled()}>919                  <button920                    class="peach-btn peach-btn--ghost peach-btn--sm"921                    style="color:var(--peach-breach)"922                    onClick={() => void handleDisableDuress()}923                    disabled={duressSaving()}924                  >925                    {duressSaving() ? "disabling..." : "disable duress"}926                  </button>927                </Show>928                <Show when={!duressEnabled()}>929                  <button930                    class="peach-btn peach-btn--accent peach-btn--sm"931                    onClick={() => void handleSetupDuress()}932                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}933                  >934                    {duressSaving() ? "saving..." : "save"}935                  </button>936                </Show>937              </div>938            </div>939          </Show>940941          <p class="peach-settings-sub-label">CANARY ENTRIES</p>942          <div class="peach-settings-row peach-settings-row--top">943            <div class="peach-settings-row-main">944              <span>Canary entries</span>945              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">946                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.947              </span>948            </div>949            <span class="peach-settings-value" style="flex-shrink:0">950              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}951            </span>952          </div>953          <Show when={canaryEntries().length > 0}>954            <For each={canaryEntries()}>955              {(entry) => (956                <div class="peach-settings-row peach-settings-row--meta">957                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>958                  <button959                    class="peach-btn peach-btn--subtle peach-btn--sm"960                    onClick={() => void handleTestCanary(entry)}961                    disabled={canaryTestingId() === entry.id}962                    style="flex-shrink:0"963                  >964                    {canaryTestingId() === entry.id965                      ? "sending…"966                      : canaryTestResult()?.id === entry.id967                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")968                        : "test"}969                  </button>970                </div>971              )}972            </For>973          </Show>974975          <p class="peach-settings-sub-label">PASSKEYS</p>976          <div class="peach-settings-row peach-settings-row--top">977            <div class="peach-settings-row-main">978              <span>Intercept passkey requests</span>979              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">980                Peach can handle passkey sign-in and creation on supported Chromium browsers.981              </span>982            </div>983            <button984              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}985              onClick={() => void handleTogglePasskeys()}986              aria-label="Toggle passkey interception"987              disabled={!passkeyState()?.supported || passkeySaving()}988            >989              <span class="peach-toggle-knob"></span>990            </button>991          </div>992          <div class="peach-settings-row peach-settings-row--meta">993            <span>Status</span>994            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>995          </div>996          <div class="peach-settings-row peach-settings-row--meta">997            <span>Browser</span>998            <span class="peach-settings-value">999              {passkeyState()?.browserLabel ?? "Unknown"}1000              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1001            </span>1002          </div>1003          <div class="peach-info-notice" style="margin-top:8px">1004            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1005          </div>1006          <Show when={passkeyState()?.conflict}>1007            <div class="peach-settings-row peach-settings-row--meta">1008              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1009            </div>1010          </Show>1011        </div>1012      </div>1013    );1014  }10151016  function renderVault() {1017    return (1018      <div class="peach-settings-body">1019        <div class="peach-settings-group">1020          <p class="peach-settings-sub-label">ENTRIES</p>1021          <div class="peach-settings-row peach-settings-row--meta">1022            <span>Entries</span>1023            <span class="peach-settings-value">{props.entriesCount}</span>1024          </div>1025          <div class="peach-settings-row peach-settings-row--meta">1026            <span>Duplicates</span>1027            <Show when={dedupDone() !== null} fallback={1028              <button1029                class="peach-btn peach-btn--subtle peach-btn--sm"1030                onClick={() => void handleCountDuplicates()}1031                disabled={dedupRunning()}1032              >1033                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1034              </button>1035            }>1036              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1037                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1038              </span>1039            </Show>1040          </div>1041          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1042            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1043              <div class="peach-restore-warning">1044                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1045              </div>1046              <div style="display:flex;gap:6px">1047                <button1048                  class="peach-btn peach-btn--subtle peach-btn--sm"1049                  onClick={() => setDedupCount(null)}1050                >1051                  cancel1052                </button>1053                <button1054                  class="peach-btn peach-btn--accent peach-btn--sm"1055                  onClick={() => void handleDeduplicate()}1056                  disabled={dedupRunning()}1057                >1058                  {dedupRunning() ? "removing..." : "remove duplicates"}1059                </button>1060              </div>1061            </div>1062          </Show>10631064          <p class="peach-settings-sub-label">MAINTENANCE</p>1065          <div class="peach-settings-row peach-settings-row--meta">1066            <span>Trash auto-delete</span>1067            <select1068              class="peach-settings-select"1069              value={String(props.trashRetentionDays)}1070              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1071            >1072              <option value="7">7 days</option>1073              <option value="30">30 days</option>1074              <option value="90">90 days</option>1075              <option value="0">Never</option>1076            </select>1077          </div>1078          <div class="peach-settings-row peach-settings-row--meta">1079            <span>Vault Health</span>1080            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1081              open1082            </button>1083          </div>1084          <Show when={declinedCardCount() > 0}>1085            <div class="peach-settings-row peach-settings-row--meta">1086              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1087              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1088                Clear1089              </button>1090            </div>1091          </Show>10921093          <p class="peach-settings-sub-label">DATA</p>1094          <div class="peach-settings-row peach-settings-row--meta">1095            <span>Import passwords</span>1096            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1097              open1098            </button>1099          </div>1100          <div class="peach-settings-row peach-settings-row--meta">1101            <span>Export backup</span>1102            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1103              open1104            </button>1105          </div>11061107          <p class="peach-settings-sub-label">VAULT HISTORY</p>1108          <Show when={historyLoading()}>1109            <p class="peach-settings-hint">Loading…</p>1110          </Show>1111          <Show when={!historyLoading() && historySnapshots().length === 0}>1112            <p class="peach-settings-hint">1113              No snapshots yet. Peach saves automatically before every change —1114              saves, syncs, and imports.1115            </p>1116          </Show>1117          <Show when={!historyLoading() && historySnapshots().length > 0}>1118            <p class="peach-settings-hint">1119              Restoring locks your vault. Unlock with the password that1120              encrypted that snapshot to confirm.1121            </p>1122            <div class="peach-history-list">1123              <For each={historySnapshots()}>1124                {(snap) => (1125                  <div class="peach-history-row">1126                    <div class="peach-history-meta">1127                      <span class="peach-history-time">1128                        {formatSnapshotTime(snap.savedAt)}1129                      </span>1130                      <span class="peach-history-sub">1131                        {[1132                          snap.entryCount !== null1133                            ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1134                            : null,1135                          reasonLabel(snap.reason),1136                        ]1137                          .filter(Boolean)1138                          .join(" · ")}1139                      </span>1140                    </div>1141                    <button1142                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1143                        historyRestoring() === snap.id ? " is-busy" : ""1144                      }`}1145                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1146                      disabled={historyRestoring() !== null}1147                    >1148                      <Show when={historyRestoring() === snap.id} fallback="Restore">1149                        …1150                      </Show>1151                    </button>1152                  </div>1153                )}1154              </For>1155            </div>1156            <p class="peach-history-footer">1157              {historySnapshots().length} of 10 snapshots1158            </p>1159          </Show>11601161          <button1162            class="peach-settings-advanced-toggle"1163            onClick={() => setShowVaultAdvanced((v) => !v)}1164            aria-expanded={showVaultAdvanced()}1165          >1166            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1167            <span class="peach-settings-advanced-chevron">1168              {showVaultAdvanced() ? "▲" : "▼"}1169            </span>1170          </button>11711172          <Show when={showVaultAdvanced()}>1173            <div class="peach-danger-card">1174              <div class="peach-danger-card-head">1175                <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1176                <h3 class="peach-danger-card-title">Replace vault from backup</h3>1177              </div>1178              <p class="peach-danger-card-copy">1179                Wipes the current vault and restores from a backup file.1180                Recoverable only if your backup is good.1181              </p>1182              <button1183                class="peach-danger-card-button"1184                onClick={() => props.onOpenFileImport?.()}1185              >1186                Choose backup file1187              </button>1188            </div>11891190            <Show when={props.onOpenPanic}>1191              <div class="peach-danger-card peach-danger-card--hazard">1192                <div class="peach-danger-card-head">1193                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1194                  <h3 class="peach-danger-card-title">Destroy vault</h3>1195                </div>1196                <p class="peach-danger-card-copy">1197                  Permanently wipes every entry, setting, and license on this1198                  device. There is no recovery.1199                </p>1200                <button1201                  class="peach-danger-card-button"1202                  onClick={() => props.onOpenPanic?.()}1203                >1204                  Begin destruction1205                </button>1206              </div>1207            </Show>1208          </Show>1209        </div>1210      </div>1211    );1212  }12131214  function renderSync() {1215    if (!isPro()) {1216      return (1217        <div class="peach-settings-body">1218          <div class="peach-pro-upgrade">1219            <div class="peach-pro-upgrade-hero">1220              <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1221              <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1222            </div>12231224            <div class="peach-pro-features">1225              <div class="peach-pro-feature">1226                <span class="peach-pro-feature-mark">✦</span>1227                <div class="peach-pro-feature-text">1228                  <span class="peach-pro-feature-name">Always in sync</span>1229                  <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1230                </div>1231              </div>1232              <div class="peach-pro-feature">1233                <span class="peach-pro-feature-mark">✦</span>1234                <div class="peach-pro-feature-text">1235                  <span class="peach-pro-feature-name">No cloud. No relay.</span>1236                  <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1237                </div>1238              </div>1239              <div class="peach-pro-feature">1240                <span class="peach-pro-feature-mark">✦</span>1241                <div class="peach-pro-feature-text">1242                  <span class="peach-pro-feature-name">Tailscale supported</span>1243                  <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1244                </div>1245              </div>1246              <div class="peach-pro-feature">1247                <span class="peach-pro-feature-mark">✦</span>1248                <div class="peach-pro-feature-text">1249                  <span class="peach-pro-feature-name">Zero friction after setup</span>1250                  <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1251                </div>1252              </div>1253            </div>12541255            <button1256              class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1257              onClick={() => setActiveSection("license")}1258            >1259              Upgrade to Pro1260            </button>1261            <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1262              Already have a key? Enter it →1263            </button>1264          </div>1265        </div>1266      );1267    }12681269    return (1270      <div class="peach-settings-body">1271        <div class="peach-settings-group">1272          <div class="peach-settings-row">1273            <span>Status</span>1274            <span class="peach-settings-value peach-settings-value--status">1275              <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1276              {getStatusLabel(syncStatus())}1277            </span>1278          </div>1279          <div class="peach-settings-row peach-settings-row--meta">1280            <span>Last sync</span>1281            <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1282          </div>1283          <div class="peach-settings-row peach-settings-row--meta">1284            <button1285              class="peach-btn peach-btn--subtle peach-btn--sm"1286              onClick={() => void handleSyncNow()}1287              disabled={syncStatus() === "not_trusted" || syncingNow()}1288            >1289              <Switch>1290                <Match when={syncingNow()}>Syncing…</Match>1291                <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1292                <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1293                <Match when={true}>Sync now</Match>1294              </Switch>1295            </button>1296          </div>1297          <p class="peach-settings-hint">1298            If you edit on multiple devices before syncing, the most recently1299            changed device wins. Use Vault History above to recover older1300            versions if needed.1301          </p>13021303          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1304            <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1305            <Show when={pairedDevices().length === 0}>1306              <p class="peach-settings-hint">1307                No paired devices yet. Add a device to start syncing.1308              </p>1309            </Show>1310            <Show when={pairedDevices().length > 0}>1311              <div class="peach-history-list">1312                <For each={pairedDevices()}>1313                  {(device) => (1314                    <div class="peach-history-row">1315                      <div class="peach-history-meta">1316                        <span class="peach-history-time">{device.displayName}</span>1317                        <span class="peach-history-sub">1318                          {device.lastSyncAt1319                            ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1320                            : "Never synced"}1321                        </span>1322                      </div>1323                      <button1324                        class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1325                        onClick={() => void handleRemoveDevice(device.deviceId)}1326                      >1327                        Remove1328                      </button>1329                    </div>1330                  )}1331                </For>1332              </div>1333            </Show>1334            <div class="peach-settings-actions--row">1335              <button1336                class="peach-btn peach-btn--subtle peach-btn--sm"1337                onClick={() => props.onOpenSyncPairing?.()}1338              >1339                Add a device1340              </button>1341            </div>1342          </div>13431344          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1345            <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1346            <For each={trustedNetworks()}>1347              {(network) => (1348                <div class="peach-sync-device-row">1349                  <span class="peach-sync-device-name">1350                    {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1351                  </span>1352                  <Show1353                    when={!network.isTailscale}1354                    fallback={<span class="peach-sync-device-meta">Always trusted</span>}1355                  >1356                    <button1357                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1358                      onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1359                    >1360                      Remove1361                    </button>1362                  </Show>1363                </div>1364              )}1365            </For>1366            <div class="peach-settings-actions--row">1367              <Show when={!isCurrentNetworkTrusted()}>1368                <button1369                  class="peach-btn peach-btn--subtle peach-btn--sm"1370                  onClick={() => void handleMarkNetworkTrusted()}1371                  disabled={markingNetwork()}1372                >1373                  {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1374                </button>1375              </Show>1376            </div>1377          </div>13781379          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1380            <span class="peach-sync-section-label">Advanced</span>1381            <Show1382              when={resetIdentityConfirm()}1383              fallback={1384                <div class="peach-sync-advanced-row">1385                  <div class="peach-sync-advanced-info">1386                    <span class="peach-sync-advanced-name">Reset sync identity</span>1387                    <span class="peach-sync-device-meta">1388                      Generates a new keypair and unpairs every device.1389                    </span>1390                  </div>1391                  <button1392                    class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1393                    onClick={() => setResetIdentityConfirm(true)}1394                  >1395                    Reset1396                  </button>1397                </div>1398              }1399            >1400              <div class="peach-sync-reset-confirm">1401                <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1402                <p class="peach-sync-reset-confirm-body">1403                  This will unpair all devices. You will need to pair them again from scratch.1404                </p>1405                <div class="peach-settings-actions--row">1406                  <button1407                    class="peach-btn peach-btn--ghost peach-btn--sm"1408                    onClick={() => setResetIdentityConfirm(false)}1409                    disabled={resetIdentityRunning()}1410                  >1411                    Cancel1412                  </button>1413                  <button1414                    class="peach-btn peach-btn--danger peach-btn--sm"1415                    onClick={() => void handleResetSyncIdentity()}1416                    disabled={resetIdentityRunning()}1417                  >1418                    {resetIdentityRunning() ? "Resetting..." : "Reset"}1419                  </button>1420                </div>1421              </div>1422            </Show>1423          </div>1424        </div>1425      </div>1426    );1427  }14281429  function renderAppearance() {1430    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1431      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1432      { id: "cormorant", label: "Cormorant", preview: "Aa" },1433      { id: "inter", label: "Inter", preview: "Aa" },1434      { id: "system", label: "System", preview: "Aa" },1435    ];1436    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1437      { id: "small", label: "Small" },1438      { id: "medium", label: "Default" },1439      { id: "large", label: "Large" },1440    ];1441    return (1442      <div class="peach-settings-body">1443        <div class="peach-settings-group">1444          <p class="peach-settings-sub-label">THEME</p>1445          <div class="peach-settings-row peach-settings-row--meta">1446            <span>Theme</span>1447            <button1448              class="peach-btn peach-btn--subtle peach-btn--sm"1449              onClick={() =>1450                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1451              }1452            >1453              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1454            </button>1455          </div>14561457          <p class="peach-settings-sub-label">ACCENT</p>1458          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1459            <span>Accent</span>1460            <div class="peach-accent-picker">1461              <For each={ACCENT_COLORS}>1462                {(color) => (1463                  <button1464                    class="peach-accent-swatch"1465                    classList={{ "is-active": props.accentColor === color.name }}1466                    style={{ background: color.hex }}1467                    onClick={() => void props.onAccentChange(color.name)}1468                    aria-label={color.name}1469                    title={color.name}1470                  />1471                )}1472              </For>1473            </div>1474          </div>14751476          <p class="peach-settings-sub-label">DISPLAY FONT</p>1477          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1478            <span>Font</span>1479            <div class="peach-font-picker">1480              <For each={fontOptions}>1481                {(opt) => (1482                  <button1483                    class="peach-font-swatch"1484                    classList={{ "is-active": props.displayFont === opt.id }}1485                    data-font-preview={opt.id}1486                    onClick={() => void props.onDisplayFontChange(opt.id)}1487                    aria-label={opt.label}1488                    title={opt.label}1489                  >1490                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1491                    <span class="peach-font-swatch-name">{opt.label}</span>1492                  </button>1493                )}1494              </For>1495            </div>1496          </div>14971498          <p class="peach-settings-sub-label">TEXT SIZE</p>1499          <div class="peach-settings-row peach-settings-row--meta">1500            <span>Size</span>1501            <div class="peach-pill-group">1502              <For each={sizeOptions}>1503                {(opt) => (1504                  <button1505                    class="peach-btn peach-btn--ghost peach-btn--sm"1506                    classList={{ "is-active": props.textSize === opt.id }}1507                    onClick={() => void props.onTextSizeChange(opt.id)}1508                  >1509                    {opt.label}1510                  </button>1511                )}1512              </For>1513            </div>1514          </div>15151516          <p class="peach-settings-sub-label">MOTION</p>1517          <div class="peach-settings-row peach-settings-row--meta">1518            <span>Reduce motion</span>1519            <button1520              class="peach-btn peach-btn--subtle peach-btn--sm"1521              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1522            >1523              {props.reduceMotion ? "On" : "Off"}1524            </button>1525          </div>1526        </div>1527      </div>1528    );1529  }15301531  function renderShortcuts() {1532    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1533    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15341535    function startCapture(action: ShortcutAction) {1536      setConflictMsg(null);1537      setCapturingAction(action);1538    }1539    function cancelCapture() {1540      setCapturingAction(null);1541      setConflictMsg(null);1542    }1543    function commitBinding(action: ShortcutAction, binding: Keybinding) {1544      const conflict = findConflict(props.shortcuts, binding, action);1545      if (conflict) {1546        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1547        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1548        return;1549      }1550      const next = { ...props.shortcuts, [action]: binding };1551      setCapturingAction(null);1552      setConflictMsg(null);1553      void props.onShortcutsChange(next);1554    }1555    function resetAction(action: ShortcutAction) {1556      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1557      void props.onShortcutsChange(next);1558    }1559    function resetAll() {1560      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1561    }15621563    // Capture-mode key handler — wired via ref onto the chip element so it1564    // catches the next non-modifier keypress and ignores other bindings.1565    function handleCaptureKey(e: KeyboardEvent) {1566      e.preventDefault();1567      e.stopPropagation();1568      if (e.key === "Escape") { cancelCapture(); return; }1569      const binding = keybindingFromEvent(e);1570      if (!binding) return; // bare modifier — keep waiting1571      const action = capturingAction();1572      if (action) commitBinding(action, binding);1573    }15741575    return (1576      <div class="peach-settings-body">1577        <div class="peach-settings-group peach-shortcuts-group">1578          <p class="peach-settings-sub-label">ENTRY VIEW</p>1579          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1580            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1581          </For>1582        </div>15831584        <div class="peach-settings-group peach-shortcuts-group">1585          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1586          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1587            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1588          </For>1589        </div>15901591        <Show when={conflictMsg()}>1592          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1593        </Show>15941595        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1596          <div class="peach-settings-row peach-settings-row--meta">1597            <span>Inline autofill on page</span>1598            <a1599              class="peach-settings-link"1600              href="chrome://extensions/shortcuts"1601              onClick={(e) => {1602                e.preventDefault();1603                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1604              }}1605            >1606              ⌘⇧P — customize ↗1607            </a>1608          </div>1609          <button1610            class="peach-btn peach-btn--subtle peach-btn--sm"1611            style="align-self:flex-start"1612            onClick={resetAll}1613          >1614            Reset all to defaults1615          </button>1616        </div>1617      </div>1618    );16191620    function renderRow(1621      desc: typeof SHORTCUT_DESCRIPTORS[number],1622      capturing: () => ShortcutAction | null,1623      onStart: (a: ShortcutAction) => void,1624      onCancel: () => void,1625      onReset: (a: ShortcutAction) => void,1626      onKey: (e: KeyboardEvent) => void,1627    ) {1628      const isActive = () => capturing() === desc.action;1629      const binding = () => props.shortcuts[desc.action];1630      const isCustom = () => {1631        const b = binding();1632        const d = DEFAULT_BINDINGS[desc.action];1633        return b.key !== d.key1634          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1635          || Boolean(b.meta)  !== Boolean(d.meta)1636          || Boolean(b.shift) !== Boolean(d.shift)1637          || Boolean(b.alt)   !== Boolean(d.alt);1638      };1639      return (1640        <div class="peach-shortcut-row">1641          <span class="peach-shortcut-row-label">{desc.label}</span>1642          <button1643            class="peach-shortcut-chip"1644            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1645            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1646            onKeyDown={(e) => { if (isActive()) onKey(e); }}1647            ref={(el) => {1648              // Auto-focus when entering capture mode so the keydown listener catches the next press.1649              if (el) {1650                queueMicrotask(() => { if (isActive()) el.focus(); });1651              }1652            }}1653          >1654            {isActive() ? "Press a key…" : formatKeybinding(binding())}1655          </button>1656          <Show when={isCustom() && !isActive()}>1657            <button1658              class="peach-shortcut-reset"1659              title="Reset to default"1660              onClick={() => onReset(desc.action)}1661            >1662              ↺1663            </button>1664          </Show>1665        </div>1666      );1667    }1668  }16691670  const PRO_FEATURES: Array<{1671    feature: ProFeature;1672    name: string;1673    desc: string;1674  }> = [1675    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1676    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1677    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1678    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1679    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1680  ];16811682  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);16831684  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1685  const [deactivating, setDeactivating] = createSignal(false);16861687  async function handleDeactivateLicense() {1688    setDeactivating(true);1689    await sendMessage({ type: "DEACTIVATE_LICENSE" });1690    setLicenseInfo(null);1691    await props.onLicenseChange?.();1692    setDeactivating(false);1693  }16941695  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1696    setCanaryTestingId(entry.id);1697    setCanaryTestResult(null);1698    const res = await sendMessage({1699      type: "FIRE_CANARY_PING",1700      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1701    }) as { ok: boolean };1702    setCanaryTestResult({ id: entry.id, ok: res.ok });1703    setCanaryTestingId(null);1704    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1705  }17061707  function renderLicense() {1708    async function onActivate() {1709      await handleActivateLicense();1710      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1711        await props.onLicenseChange?.();1712      }1713    }17141715    return (1716      <div class="peach-settings-body">1717        <Show1718          when={isPro()}1719          fallback={1720            <Show1721              when={showKeyEntry()}1722              fallback={1723                <div class="peach-pro-upgrade">1724                  <div class="peach-pro-upgrade-hero">1725                    <div class="peach-pro-upgrade-mark-wrap">1726                      <PeachMark class="peach-pro-upgrade-mark" />1727                    </div>1728                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1729                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1730                  </div>17311732                  <div class="peach-pro-features">1733                    <For each={PRO_FEATURES}>1734                      {(item) => (1735                        <div1736                          class="peach-pro-feature"1737                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1738                        >1739                          <span class="peach-pro-feature-mark">✦</span>1740                          <div class="peach-pro-feature-text">1741                            <span class="peach-pro-feature-name">{item.name}</span>1742                            <span class="peach-pro-feature-desc">{item.desc}</span>1743                          </div>1744                        </div>1745                      )}1746                    </For>1747                  </div>17481749                  <div class="peach-pro-upgrade-actions">1750                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1751                    <a1752                      href="https://peachpasswords.com/#pricing"1753                      target="_blank"1754                      rel="noopener noreferrer"1755                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1756                    >1757                      Get Peach Pro1758                    </a>1759                    <div class="peach-pro-key-divider"><span>or</span></div>1760                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1761                      Activate a license key1762                    </button>1763                  </div>1764                </div>1765              }1766            >1767              <div class="peach-pro-key-ceremony">1768                <LockRuneCanvas />1769                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1770                <input1771                  class="peach-underline-input peach-pro-key-input"1772                  type="text"1773                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1774                  value={licenseKeyInput()}1775                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1776                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1777                  autocomplete="off"1778                  spellcheck={false}1779                />1780                <Show when={licenseError()}>1781                  <p class="peach-pro-key-error">That key doesn't look right.</p>1782                </Show>1783                <button1784                  class="peach-btn peach-btn--accent peach-pro-key-activate"1785                  onClick={() => void onActivate()}1786                  disabled={activatingLicense() || !licenseKeyInput().trim()}1787                >1788                  {activatingLicense() ? "activating…" : "Activate"}1789                </button>1790                <button1791                  class="peach-pro-key-cancel"1792                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1793                >1794                  cancel1795                </button>1796              </div>1797            </Show>1798          }1799        >1800          <div class="peach-pro-active">1801            <div class="peach-pro-active-hero">1802              <div class="peach-pro-active-mark-wrap">1803                <PeachMark class="peach-pro-active-mark" />1804              </div>1805              <h2 class="peach-pro-active-title">Peach Pro</h2>1806              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1807            </div>18081809            <Show when={licenseKey()}>1810              <div class="peach-settings-license">1811                <span class="peach-settings-label">LICENSE KEY</span>1812                <div class="peach-settings-license-row">1813                  <span class="peach-settings-license-key">{licenseKey()!}</span>1814                  <button1815                    class="peach-settings-license-copy"1816                    onClick={() => copyLicenseKey(licenseKey()!)}1817                  >1818                    {copied() ? "✓" : "copy"}1819                  </button>1820                </div>1821              </div>1822            </Show>18231824            <div class="peach-pro-active-features">1825              <For each={PRO_FEATURES}>1826                {(item) => (1827                  <div class="peach-pro-active-feature">1828                    <span class="peach-pro-active-feature-mark">✦</span>1829                    <span class="peach-pro-active-feature-name">{item.name}</span>1830                  </div>1831                )}1832              </For>1833            </div>18341835            <button1836              class="peach-pro-deactivate"1837              onClick={() => void handleDeactivateLicense()}1838              disabled={deactivating()}1839            >1840              {deactivating() ? "Deactivating…" : "Deactivate"}1841            </button>1842          </div>1843        </Show>1844      </div>1845    );1846  }18471848  function renderFeedback() {1849    return (1850      <div class="peach-settings-body">1851        <div class="peach-settings-group peach-feedback-group">1852          <p class="peach-feedback-intro">1853            Help us improve Peach. Every report goes directly to our inbox.1854          </p>18551856          <p class="peach-settings-sub-label">TYPE</p>1857          <div class="peach-feedback-chips">1858            <For each={FEEDBACK_CATEGORIES}>1859              {(cat) => (1860                <button1861                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1862                  onClick={() => setFbCategory(cat)}1863                >1864                  {cat}1865                </button>1866              )}1867            </For>1868          </div>18691870          <p class="peach-settings-sub-label">MESSAGE</p>1871          <textarea1872            class="peach-feedback-textarea"1873            placeholder="Describe the issue or idea..."1874            value={fbMessage()}1875            onInput={(e) => setFbMessage(e.currentTarget.value)}1876          />18771878          <p class="peach-feedback-meta">1879            Version {version} · will be included automatically1880          </p>18811882          <Show when={fbResult() === "success"}>1883            <p class="peach-feedback-result peach-feedback-result--ok">1884              Sent — thank you1885            </p>1886          </Show>1887          <Show when={fbResult() === "error"}>1888            <p class="peach-feedback-result peach-feedback-result--err">1889              Failed to send — try again1890            </p>1891          </Show>18921893          <button1894            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1895            onClick={() => void handleSubmitFeedback()}1896            disabled={fbSubmitting() || !fbMessage().trim()}1897          >1898            {fbSubmitting() ? "Sending..." : "Send feedback"}1899          </button>19001901          <p class="peach-feedback-anon">1902            Sent anonymously · no account required1903          </p>1904        </div>1905      </div>1906    );1907  }19081909  return (1910    <Show1911      when={showNewRecoveryWords()}1912      fallback={1913        <div class="peach-overlay-view ov-enter-right" style="display:flex">1914          <div class="peach-overlay-header">1915            <button1916              class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1917              onClick={handleHeaderBack}1918              aria-label="Back"1919            >1920              <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1921                <path d="M6 1L1 6L6 11" />1922              </svg>1923            </button>1924            <span class="peach-overlay-title">{currentTitle()}</span>1925            <div class="peach-overlay-spacer"></div>1926          </div>19271928          <Switch fallback={renderHome()}>1929            <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1930            <Match when={activeSection() === "vault"}>{renderVault()}</Match>1931            <Match when={activeSection() === "sync"}>{renderSync()}</Match>1932            <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1933            <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1934            <Match when={activeSection() === "license"}>{renderLicense()}</Match>1935            <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1936          </Switch>1937        </div>1938      }1939    >1940      <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />1941    </Show>1942  );1943}