Settings.tsx

79.4 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";24import { isNativePlatform, nativePlugin } from "../../shared/native";2526interface LicenseInfo {27  valid: boolean;28  plan: "free" | "pro";29  issued?: string;30}3132interface SettingsProps {33  entriesCount: number;34  breachMonitoring: boolean;35  lockOnClose: boolean;36  autoLockMinutes: number;37  trashRetentionDays: number;38  theme: "dark" | "light";39  accentColor: AccentColorName;40  displayFont: "cormorant-italic" | "cormorant" | "inter" | "system";41  textSize: "small" | "medium" | "large";42  reduceMotion: boolean;43  isPro?: boolean;44  initialSection?: string | null;45  upgradeHighlight?: ProFeature | null;46  onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;47  onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;48  onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;49  onTrashRetentionChange: (days: number) => void | Promise<void>;50  onThemeChange: (theme: "dark" | "light") => void;51  onAccentChange: (name: AccentColorName) => void | Promise<void>;52  onDisplayFontChange: (font: "cormorant-italic" | "cormorant" | "inter" | "system") => void | Promise<void>;53  onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise<void>;54  onReduceMotionChange: (enabled: boolean) => void | Promise<void>;55  shortcuts: Record<ShortcutAction, Keybinding>;56  onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;57  onLicenseChange?: () => void | Promise<void>;58  onBack: () => void;59  onImport: () => void;60  onExport: () => void;61  onBreachCheckDone: () => void;62  onOpenHealth?: () => void;63  onOpenSyncPairing?: () => void;64  onOpenFileImport?: () => void;65  onOpenPanic?: () => void;66}6768type SectionName =69  | "security"70  | "vault"71  | "sync"72  | "appearance"73  | "shortcuts"74  | "license"75  | "feedback";7677const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7879function formatActivatedSince(issued: string | undefined): string {80  if (!issued) return "Active";81  try {82    const d = new Date(issued);83    if (Number.isNaN(d.getTime())) return "Active";84    return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;85  } catch {86    return "Active";87  }88}8990function isSectionName(value: string): value is SectionName {91  return (92    value === "security" ||93    value === "vault" ||94    value === "sync" ||95    value === "appearance" ||96    value === "shortcuts" ||97    value === "license" ||98    value === "feedback"99  );100}101102export function SettingsView(props: SettingsProps) {103  const version = chrome.runtime.getManifest().version;104  const initial = props.initialSection;105  const [activeSection, setActiveSection] = createSignal<SectionName | null>(106    initial && isSectionName(initial) ? (initial as SectionName) : null107  );108109  const [checkingBreaches, setCheckingBreaches] = createSignal(false);110  const [breachResult, setBreachResult] = createSignal<string | null>(null);111  const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);112  const [licenseKeyInput, setLicenseKeyInput] = createSignal("");113  const [licenseError, setLicenseError] = createSignal<string | null>(null);114  const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);115  const [activatingLicense, setActivatingLicense] = createSignal(false);116117  const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);118  const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);119  const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");120  const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);121  // Prefer the prop (cached at App boot), fall back to the sync-state probe.122  // The prop is authoritative once present so license activation reflects123  // immediately without waiting for a sync round-trip.124  const [syncProStatus, setSyncProStatus] = createSignal(false);125  const isPro = () => props.isPro ?? syncProStatus();126  const [syncingNow, setSyncingNow] = createSignal(false);127  const [biometricSupported, setBiometricSupported] = createSignal(false);128  const [biometricStored, setBiometricStored] = createSignal(false);129  const [biometricBusy, setBiometricBusy] = createSignal(false);130  const [biometricError, setBiometricError] = createSignal<string | null>(null);131  const [syncFeedback, setSyncFeedback] = createSignal<132    { kind: "ok" | "err"; text: string } | null133  >(null);134  const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);135  const [passkeySaving, setPasskeySaving] = createSignal(false);136137  const [dedupCount, setDedupCount] = createSignal<number | null>(null);138  const [dedupRunning, setDedupRunning] = createSignal(false);139  const [dedupDone, setDedupDone] = createSignal<number | null>(null);140141  const [declinedCardCount, setDeclinedCardCount] = createSignal(0);142143  // Vault history (peach_vault_history) — last 10 snapshots, snapshotted144  // BEFORE every real-mode vault write so the user can roll back if a sync145  // or import overwrites the wrong state.146  const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);147  const [historyLoading, setHistoryLoading] = createSignal(false);148  const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);149150  async function loadVaultHistory() {151    setHistoryLoading(true);152    try {153      const res = await sendMessage({ type: "GET_VAULT_HISTORY" });154      const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];155      setHistorySnapshots(list);156    } finally {157      setHistoryLoading(false);158    }159  }160161  async function handleRestoreVaultSnapshot(id: string) {162    if (historyRestoring()) return;163    setHistoryRestoring(id);164    const res = await sendMessage({165      type: "RESTORE_VAULT_SNAPSHOT",166      payload: { snapshotId: id },167    });168    setHistoryRestoring(null);169    if ((res as { ok: boolean }).ok) {170      // Vault is now locked — let the parent close Settings; App.tsx's171      // VAULT_LOCKED listener will route back to the lock screen.172      props.onBack();173    }174  }175176  function formatSnapshotTime(savedAt: number): string {177    const diff = Date.now() - savedAt;178    if (diff < 60_000) return "Just now";179    if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;180    const d = new Date(savedAt);181    const isToday = new Date().toDateString() === d.toDateString();182    const isYest =183      new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();184    const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });185    if (isToday) return `Today ${t}`;186    if (isYest) return `Yesterday ${t}`;187    return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;188  }189190  function reasonLabel(reason: VaultSnapshot["reason"]): string {191    return (192      {193        local_save: "saved",194        sync_received: "synced",195        import: "imported",196        restore: "restored",197      } as const198    )[reason] ?? reason;199  }200201  const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);202203  const [showChangePw, setShowChangePw] = createSignal(false);204  const [changePwCurrent, setChangePwCurrent] = createSignal("");205  const [changePwNew, setChangePwNew] = createSignal("");206  const [changePwConfirm, setChangePwConfirm] = createSignal("");207  const [changePwSaving, setChangePwSaving] = createSignal(false);208  const [changePwError, setChangePwError] = createSignal<string | null>(null);209  const [changePwSuccess, setChangePwSuccess] = createSignal(false);210  const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);211212  const [duressEnabled, setDuressEnabled] = createSignal(false);213  const [showDuressSetup, setShowDuressSetup] = createSignal(false);214  const [duressPassword, setDuressPassword] = createSignal("");215  const [duressConfirm, setDuressConfirm] = createSignal("");216  const [duressMainPassword, setDuressMainPassword] = createSignal("");217  const [duressSaving, setDuressSaving] = createSignal(false);218  const [duressError, setDuressError] = createSignal<string | null>(null);219  const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);220221  const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);222223  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);224  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);225226  const [fbCategory, setFbCategory] = createSignal("Bug report");227  const [fbMessage, setFbMessage] = createSignal("");228  const [fbSubmitting, setFbSubmitting] = createSignal(false);229  const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);230231  onMount(async () => {232    const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([233      sendMessage({ type: "GET_LICENSE" }),234      sendMessage({ type: "GET_SYNC_STATE" }),235      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),236      sendMessage({ type: "IS_DURESS_ENABLED" }),237      sendMessage({ type: "GET_SETTINGS" }),238      sendMessage({ type: "GET_LICENSE_KEY" }),239    ]);240241    // Vault history loads in the background — the Vault sub-view picks it up242    // when the user navigates in. Loading it lazily keeps Settings boot fast.243    void loadVaultHistory();244245    if ((licenseRes as { ok: boolean }).ok) {246      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);247    }248249    if ((syncRes as { ok: boolean }).ok) {250      const data = syncRes as { ok: boolean; data?: {251        pairedDevices?: PairedDevice[];252        trustedNetworks?: TrustedNetwork[];253        status?: SyncStatus;254        lastSyncAt?: number;255        isPro?: boolean;256      }};257      if (data.data) {258        setPairedDevices(data.data.pairedDevices ?? []);259        setTrustedNetworks(data.data.trustedNetworks ?? []);260        setSyncStatus(data.data.status ?? "offline");261        setLastSyncAt(data.data.lastSyncAt);262        setSyncProStatus(data.data.isPro ?? false);263      }264    }265266    if ((passkeyRes as { ok: boolean }).ok) {267      setPasskeyState(268        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null269      );270    }271272    if ((duressRes as { ok: boolean }).ok) {273      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);274    }275276    if ((settingsRes as { ok: boolean }).ok) {277      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;278      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);279    }280281    if ((keyRes as { ok: boolean }).ok) {282      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);283    }284285    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });286    if ((entriesRes as { ok: boolean }).ok) {287      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];288      setCanaryEntries(289        entries290          .filter((e) => e.canary?.webhookUrl)291          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))292      );293    }294295    chrome.runtime.onMessage.addListener((msg) => {296      if (msg.type === "SYNC_STATE_UPDATE") {297        setPairedDevices(msg.pairedDevices ?? []);298        setTrustedNetworks(msg.trustedNetworks ?? []);299        setSyncStatus(msg.status ?? "offline");300        setLastSyncAt(msg.lastSyncAt);301        setSyncProStatus(msg.isPro ?? false);302      }303    });304305    if (isNativePlatform()) {306      const biometric = nativePlugin<{ isAvailable: () => Promise<{ available: boolean }> }>("BiometricPlugin");307      const keystore = nativePlugin<{ hasMasterKey: () => Promise<{ exists: boolean }> }>("KeystorePlugin");308      try {309        if (biometric) {310          const a = await biometric.isAvailable();311          setBiometricSupported(a.available);312        }313        if (keystore) {314          const k = await keystore.hasMasterKey();315          setBiometricStored(k.exists);316        }317      } catch { /* missing plugins — leave defaults */ }318    }319  });320321  async function handleEnableBiometrics() {322    setBiometricBusy(true);323    setBiometricError(null);324    try {325      const keyRes = (await sendMessage({ type: "GET_DERIVED_KEY" })) as {326        ok: boolean;327        data?: { masterKeyHex: string };328        error?: string;329      };330      if (!keyRes.ok || !keyRes.data?.masterKeyHex) {331        setBiometricError(keyRes.error === "vault_locked"332          ? "unlock the vault first"333          : "couldn't read derived key");334        return;335      }336      const keystore = nativePlugin<{ storeMasterKey: (p: { masterKeyHex: string }) => Promise<void> }>("KeystorePlugin");337      if (!keystore) {338        setBiometricError("biometric plugin unavailable");339        return;340      }341      await keystore.storeMasterKey({ masterKeyHex: keyRes.data.masterKeyHex });342      await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: true } });343      setBiometricStored(true);344    } catch (e) {345      setBiometricError(e instanceof Error ? e.message : "failed to enable biometrics");346    } finally {347      setBiometricBusy(false);348    }349  }350351  async function handleDisableBiometrics() {352    setBiometricBusy(true);353    setBiometricError(null);354    try {355      const keystore = nativePlugin<{ clearMasterKey: () => Promise<void> }>("KeystorePlugin");356      if (keystore) await keystore.clearMasterKey();357      await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: false } });358      setBiometricStored(false);359    } catch (e) {360      setBiometricError(e instanceof Error ? e.message : "failed to disable biometrics");361    } finally {362      setBiometricBusy(false);363    }364  }365366  async function handleChangePassword() {367    setChangePwError(null);368    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }369    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }370    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }371    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }372    setChangePwSaving(true);373    const res = await sendMessage({374      type: "CHANGE_PASSWORD",375      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },376    }) as { ok: boolean; error?: string };377    if (res.ok) {378      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");379      setShowChangePw(false);380      setShowNewRecoveryWords(true);381    } else {382      setChangePwError(res.error ?? "failed to change password");383    }384    setChangePwSaving(false);385  }386387  const [copied, setCopied] = createSignal(false);388389  async function copyLicenseKey(key: string) {390    await navigator.clipboard.writeText(key);391    setCopied(true);392    setTimeout(() => setCopied(false), 2000);393  }394395  async function handleActivateLicense() {396    const key = licenseKeyInput().trim();397    if (!key) return;398    setActivatingLicense(true);399    setLicenseError(null);400    setLicenseSuccess(null);401    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });402    setActivatingLicense(false);403    if ((res as { ok: boolean }).ok) {404      const data = (res as { ok: boolean; data?: LicenseInfo }).data;405      if (data?.valid) {406        setLicenseInfo(data);407        setLicenseKey(key);408        setLicenseSuccess("License activated!");409        setLicenseKeyInput("");410      } else {411        setLicenseError("Invalid license key");412      }413    } else {414      setLicenseError("Activation failed");415    }416  }417418  async function handleTogglePasskeys() {419    const current = passkeyState();420    if (!current || !current.supported) return;421    setPasskeySaving(true);422    const res = await sendMessage({423      type: "SET_PASSKEY_PROVIDER_ENABLED",424      payload: { enabled: !current.enabled },425    });426    setPasskeySaving(false);427    if ((res as { ok: boolean }).ok) {428      setPasskeyState(429        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null430      );431    }432  }433434  async function handleCheckAllBreaches() {435    setCheckingBreaches(true);436    setBreachResult(null);437    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });438    setCheckingBreaches(false);439    if ((res as { ok: boolean }).ok) {440      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};441      const breached = Object.values(data).filter((count) => count > 0).length;442      setBreachResult(443        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"444      );445      props.onBreachCheckDone();446    }447  }448449  async function handleSyncNow() {450    if (syncStatus() === "not_trusted") return;451    setSyncingNow(true);452    setSyncFeedback(null);453    const res = await sendMessage({ type: "TRIGGER_SYNC" });454    const ok = (res as { ok: boolean }).ok;455    if (ok) {456      const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;457      const count = data?.syncedCount ?? 0;458      setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });459      setLastSyncAt(Date.now());460      void refreshSyncState();461    } else {462      const error = (res as { error?: string }).error;463      const map: Record<string, string> = {464        no_paired_devices: "No paired devices",465        no_devices_reachable: "No devices found",466        vault_locked: "Vault is locked",467        pro_required: "Sync requires Pro",468      };469      setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });470    }471    setTimeout(() => {472      setSyncingNow(false);473      setSyncFeedback(null);474    }, 3500);475  }476477  async function refreshSyncState() {478    const res = await sendMessage({ type: "GET_SYNC_STATE" });479    const data = (res as { ok: boolean; data?: {480      pairedDevices?: PairedDevice[];481      trustedNetworks?: TrustedNetwork[];482      status?: SyncStatus;483      lastSyncAt?: number;484    }}).data;485    if (!data) return;486    setPairedDevices(data.pairedDevices ?? []);487    setTrustedNetworks(data.trustedNetworks ?? []);488    setSyncStatus(data.status ?? "offline");489    setLastSyncAt(data.lastSyncAt);490  }491492  async function handleRemoveDevice(deviceId: string) {493    setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));494    await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });495    void refreshSyncState();496  }497498  async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {499    await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });500    void refreshSyncState();501  }502503  const [markingNetwork, setMarkingNetwork] = createSignal(false);504505  async function handleMarkNetworkTrusted() {506    if (markingNetwork()) return;507    setMarkingNetwork(true);508    await sendMessage({ type: "MARK_NETWORK_TRUSTED" });509    await refreshSyncState();510    setMarkingNetwork(false);511  }512513  const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);514  const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);515516  async function handleResetSyncIdentity() {517    setResetIdentityRunning(true);518    const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });519    setResetIdentityRunning(false);520    if ((res as { ok: boolean }).ok) {521      setPairedDevices([]);522      setResetIdentityConfirm(false);523    }524  }525526  function formatRelativeTime(timestamp?: number): string {527    if (!timestamp) return "Never";528    const diff = Date.now() - timestamp;529    const minutes = Math.floor(diff / 60000);530    const hours = Math.floor(diff / 3600000);531    const days = Math.floor(diff / 86400000);532    if (minutes < 1) return "Just now";533    if (minutes < 60) return `${minutes}m ago`;534    if (hours < 24) return `${hours}h ago`;535    return `${days}d ago`;536  }537538  function getStatusColor(status: SyncStatus): string {539    switch (status) {540      case "synced": return "var(--peach-sync-ok)";541      case "syncing": return "var(--peach-sync-busy)";542      case "no_peers": return "var(--peach-sync-off)";543      case "not_trusted": return "var(--peach-sync-err)";544      case "offline": return "var(--peach-sync-off)";545      default: return "var(--peach-sync-off)";546    }547  }548549  function getStatusLabel(status: SyncStatus): string {550    switch (status) {551      case "synced": return "Synced";552      case "syncing": return "Syncing";553      case "no_peers": return "No peers visible";554      case "not_trusted": return "Not on a trusted network";555      case "offline": return "Offline";556      default: return "Unknown";557    }558  }559560  function isCurrentNetworkTrusted(): boolean {561    return trustedNetworks().length > 0;562  }563564  async function handleSetupDuress() {565    const mainPw = duressMainPassword().trim();566    const duressPw = duressPassword().trim();567    const confirmPw = duressConfirm().trim();568569    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }570    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }571    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }572    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }573574    setDuressSaving(true);575    setDuressError(null);576577    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };578    if (!verifyRes.ok || !verifyRes.data) {579      setDuressError("incorrect main password");580      setDuressSaving(false);581      return;582    }583584    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };585    setDuressSaving(false);586    if (res.ok) {587      setDuressEnabled(true);588      setShowDuressSetup(false);589      setDuressPassword("");590      setDuressConfirm("");591      setDuressMainPassword("");592      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");593      setTimeout(() => setDuressSuccess(null), 5000);594    } else {595      setDuressError(res.error ?? "failed to set duress password");596    }597  }598599  async function handleClearDeclinedCards() {600    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });601    setDeclinedCardCount(0);602  }603604  async function handleCountDuplicates() {605    setDedupRunning(true);606    setDedupDone(null);607    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };608    setDedupRunning(false);609    if (res.ok) setDedupCount(res.data ?? 0);610  }611612  async function handleDeduplicate() {613    setDedupRunning(true);614    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };615    setDedupRunning(false);616    if (res.ok) {617      setDedupDone(res.data ?? 0);618      setDedupCount(0);619    }620  }621622  async function handleDisableDuress() {623    const mainPw = duressMainPassword().trim();624    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }625626    setDuressSaving(true);627    setDuressError(null);628629    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };630    if (!verifyRes.ok || !verifyRes.data) {631      setDuressError("incorrect main password");632      setDuressSaving(false);633      return;634    }635636    const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };637    setDuressSaving(false);638    if (res.ok) {639      setDuressEnabled(false);640      setShowDuressSetup(false);641      setDuressMainPassword("");642      setDuressSuccess("Duress password disabled.");643      setTimeout(() => setDuressSuccess(null), 4000);644    } else {645      setDuressError(res.error ?? "failed to disable duress password");646    }647  }648649  async function handleSubmitFeedback() {650    if (!fbMessage().trim() || fbSubmitting()) return;651    setFbSubmitting(true);652    setFbResult(null);653    const res = await sendMessage({654      type: "SUBMIT_FEEDBACK",655      payload: {656        category: fbCategory(),657        message: fbMessage().trim(),658        version,659        browser: navigator.userAgent,660      },661    }) as { ok: boolean; error?: string };662    setFbSubmitting(false);663    if (res.ok) {664      setFbResult("success");665      setFbMessage("");666      setTimeout(() => setFbResult(null), 4000);667    } else {668      setFbResult("error");669    }670  }671672  function handleHeaderBack() {673    if (activeSection() !== null) {674      setActiveSection(null);675    } else {676      props.onBack();677    }678  }679680  function currentTitle(): string {681    const s = activeSection();682    if (s === null) return "settings";683    if (s === "license") return "account";684    return s;685  }686687  function renderCatRow(opts: {688    section: SectionName;689    name: string;690    desc: string;691    icon: () => any;692    pro?: boolean;693  }) {694    return (695      <button696        class="peach-settings-cat-row"697        onClick={() => setActiveSection(opts.section)}698      >699        <div class="peach-settings-cat-icon">{opts.icon()}</div>700        <div class="peach-settings-cat-content">701          <span class="peach-settings-cat-name">702            {opts.name}703            <Show when={opts.pro && !isPro()}>704              <span class="peach-pro-mark" aria-label="Pro feature">✦</span>705            </Show>706          </span>707          <span class="peach-settings-cat-desc">{opts.desc}</span>708        </div>709        <svg710          class="peach-settings-cat-chevron"711          width="6" height="10" viewBox="0 0 6 10" fill="none"712          stroke="currentColor" stroke-width="1.1"713          stroke-linecap="round" stroke-linejoin="round"714        >715          <path d="M1 1l4 4-4 4" />716        </svg>717      </button>718    );719  }720721  const iconShield = () => (722    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">723      <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>724    </svg>725  );726  const iconKey = () => (727    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">728      <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>729    </svg>730  );731  const iconLock = () => (732    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">733      <rect x="3" y="7" width="10" height="7" rx="1.5"/>734      <path d="M6 7V5a3 3 0 016 0v2"/>735    </svg>736  );737  const iconSync = () => (738    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">739      <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>740      <path d="M14 6l-2 2-2-2"/>741      <path d="M2 10l2-2 2 2"/>742    </svg>743  );744  const iconSun = () => (745    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">746      <circle cx="8" cy="8" r="3"/>747      <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"/>748    </svg>749  );750  const iconKeys = () => (751    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">752      <rect x="2" y="5" width="5" height="4" rx="1"/>753      <rect x="9" y="7" width="5" height="4" rx="1"/>754      <path d="M7 7h2m-2 2h2"/>755    </svg>756  );757  const iconBubble = () => (758    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">759      <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>760    </svg>761  );762763  function renderHome() {764    return (765      <div class="peach-settings-body">766        <p class="peach-settings-nav-label">PROTECTION</p>767        {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}768        {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}769770        <p class="peach-settings-nav-label">VAULT</p>771        {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}772        {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}773774        <p class="peach-settings-nav-label">PREFERENCES</p>775        {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}776        {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}777778        <p class="peach-settings-nav-label">SUPPORT</p>779        {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}780781        <div class="peach-settings-footer">782          <span>Peach {version}</span>783          <span class="peach-divider"></span>784          <span>Browser extension</span>785        </div>786      </div>787    );788  }789790  function renderSecurity() {791    return (792      <div class="peach-settings-body">793        <div class="peach-settings-group">794          <p class="peach-settings-sub-label">LOCKING</p>795          <div class="peach-settings-row">796            <span>Lock on close</span>797            <button798              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}799              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}800              aria-label="Toggle lock on close"801            >802              <span class="peach-toggle-knob"></span>803            </button>804          </div>805          <div class="peach-settings-row">806            <span>Auto-lock after</span>807            <select808              class="peach-settings-select"809              value={String(props.autoLockMinutes)}810              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}811            >812              <option value="1">1 minute</option>813              <option value="5">5 minutes</option>814              <option value="15">15 minutes</option>815              <option value="30">30 minutes</option>816              <option value="0">Never</option>817            </select>818          </div>819820          <Show when={isNativePlatform() && biometricSupported()}>821            <p class="peach-settings-sub-label">BIOMETRICS</p>822            <div class="peach-settings-row">823              <span>Unlock with biometrics</span>824              <button825                class={`peach-toggle ${biometricStored() ? "is-on" : ""}`}826                onClick={() => void (biometricStored() ? handleDisableBiometrics() : handleEnableBiometrics())}827                disabled={biometricBusy()}828                aria-label="Toggle biometric unlock"829              >830                <span class="peach-toggle-knob"></span>831              </button>832            </div>833            <Show when={biometricError()}>834              <div class="peach-settings-row peach-settings-row--meta">835                <span class="peach-hint peach-hint-error">{biometricError()}</span>836              </div>837            </Show>838          </Show>839840          <p class="peach-settings-sub-label">BREACH PROTECTION</p>841          <div class="peach-settings-row">842            <span>Breach monitoring</span>843            <button844              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}845              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}846              aria-label="Toggle breach monitoring"847            >848              <span class="peach-toggle-knob"></span>849            </button>850          </div>851          <div class="peach-settings-row peach-settings-row--meta">852            <span>Check all passwords</span>853            <button854              class="peach-btn peach-btn--subtle peach-btn--sm"855              onClick={() => void handleCheckAllBreaches()}856              disabled={checkingBreaches()}857            >858              {checkingBreaches() ? "checking..." : "run"}859            </button>860          </div>861          <Show when={breachResult()}>862            <div class="peach-settings-row peach-settings-row--meta">863              <span>Last check</span>864              <span class="peach-settings-value">{breachResult()}</span>865            </div>866          </Show>867868          <p class="peach-settings-sub-label">MASTER PASSWORD</p>869          <div class="peach-settings-row peach-settings-row--meta">870            <span>Master password</span>871            <button872              class="peach-btn peach-btn--subtle peach-btn--sm"873              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}874            >875              {showChangePw() ? "cancel" : "change"}876            </button>877          </div>878          <Show when={showChangePw()}>879            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">880              <input881                class="peach-settings-input"882                type="password"883                placeholder="current password"884                value={changePwCurrent()}885                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}886                autocomplete="current-password"887              />888              <input889                class="peach-settings-input"890                type="password"891                placeholder="new password (min 12 chars)"892                value={changePwNew()}893                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}894                autocomplete="new-password"895              />896              <input897                class="peach-settings-input"898                type="password"899                placeholder="confirm new password"900                value={changePwConfirm()}901                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}902                autocomplete="new-password"903                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}904              />905              <Show when={changePwError()}>906                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>907              </Show>908              <Show when={changePwSuccess()}>909                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>910              </Show>911              <button912                class="peach-btn peach-btn--accent peach-btn--sm"913                style="align-self:flex-start"914                onClick={() => void handleChangePassword()}915                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}916              >917                {changePwSaving() ? "saving…" : "save new password"}918              </button>919            </div>920          </Show>921922          <p class="peach-settings-sub-label">DURESS PASSWORD</p>923          <div class="peach-settings-row peach-settings-row--top">924            <div class="peach-settings-row-main">925              <span>Duress password</span>926              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">927                If entered under coercion, shows a decoy empty vault instead of your real one.928              </span>929            </div>930            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>931              {duressEnabled() ? "on" : "off"}932            </div>933          </div>934935          <Show when={duressEnabled() && !showDuressSetup()}>936            <div class="peach-settings-row peach-settings-row--meta">937              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">938                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.939              </span>940            </div>941          </Show>942943          <Show when={duressSuccess()}>944            <div class="peach-settings-row peach-settings-row--meta">945              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>946            </div>947          </Show>948949          <Show when={!showDuressSetup()}>950            <div class="peach-settings-row peach-settings-row--meta">951              <button952                class="peach-btn peach-btn--subtle peach-btn--sm"953                onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}954              >955                {duressEnabled() ? "Change / disable" : "Set up"}956              </button>957            </div>958          </Show>959960          <Show when={showDuressSetup()}>961            <div class="peach-duress-form">962              <label class="peach-label">main password (to confirm)</label>963              <input964                class="peach-settings-input"965                type="password"966                placeholder="your current master password"967                autocomplete="current-password"968                value={duressMainPassword()}969                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}970              />971              <Show when={!duressEnabled()}>972                <label class="peach-label">new duress password</label>973                <input974                  class="peach-settings-input"975                  type="password"976                  placeholder="duress password (min 8 chars)"977                  autocomplete="new-password"978                  value={duressPassword()}979                  onInput={(e) => setDuressPassword(e.currentTarget.value)}980                />981                <label class="peach-label">confirm duress password</label>982                <input983                  class="peach-settings-input"984                  type="password"985                  placeholder="confirm duress password"986                  autocomplete="new-password"987                  value={duressConfirm()}988                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}989                />990              </Show>991992              <Show when={duressError()}>993                <p class="peach-hint peach-hint-error">{duressError()}</p>994              </Show>995996              <div class="peach-duress-actions">997                <button998                  class="peach-btn peach-btn--subtle peach-btn--sm"999                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}1000                >1001                  cancel1002                </button>1003                <Show when={duressEnabled()}>1004                  <button1005                    class="peach-btn peach-btn--ghost peach-btn--sm"1006                    style="color:var(--peach-breach)"1007                    onClick={() => void handleDisableDuress()}1008                    disabled={duressSaving()}1009                  >1010                    {duressSaving() ? "disabling..." : "disable duress"}1011                  </button>1012                </Show>1013                <Show when={!duressEnabled()}>1014                  <button1015                    class="peach-btn peach-btn--accent peach-btn--sm"1016                    onClick={() => void handleSetupDuress()}1017                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}1018                  >1019                    {duressSaving() ? "saving..." : "save"}1020                  </button>1021                </Show>1022              </div>1023            </div>1024          </Show>10251026          <p class="peach-settings-sub-label">CANARY ENTRIES</p>1027          <div class="peach-settings-row peach-settings-row--top">1028            <div class="peach-settings-row-main">1029              <span>Canary entries</span>1030              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1031                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.1032              </span>1033            </div>1034            <span class="peach-settings-value" style="flex-shrink:0">1035              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}1036            </span>1037          </div>1038          <Show when={canaryEntries().length > 0}>1039            <For each={canaryEntries()}>1040              {(entry) => (1041                <div class="peach-settings-row peach-settings-row--meta">1042                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>1043                  <button1044                    class="peach-btn peach-btn--subtle peach-btn--sm"1045                    onClick={() => void handleTestCanary(entry)}1046                    disabled={canaryTestingId() === entry.id}1047                    style="flex-shrink:0"1048                  >1049                    {canaryTestingId() === entry.id1050                      ? "sending…"1051                      : canaryTestResult()?.id === entry.id1052                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1053                        : "test"}1054                  </button>1055                </div>1056              )}1057            </For>1058          </Show>10591060          <p class="peach-settings-sub-label">PASSKEYS</p>1061          <div class="peach-settings-row peach-settings-row--top">1062            <div class="peach-settings-row-main">1063              <span>Intercept passkey requests</span>1064              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1065                Peach can handle passkey sign-in and creation on supported Chromium browsers.1066              </span>1067            </div>1068            <button1069              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1070              onClick={() => void handleTogglePasskeys()}1071              aria-label="Toggle passkey interception"1072              disabled={!passkeyState()?.supported || passkeySaving()}1073            >1074              <span class="peach-toggle-knob"></span>1075            </button>1076          </div>1077          <div class="peach-settings-row peach-settings-row--meta">1078            <span>Status</span>1079            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1080          </div>1081          <div class="peach-settings-row peach-settings-row--meta">1082            <span>Browser</span>1083            <span class="peach-settings-value">1084              {passkeyState()?.browserLabel ?? "Unknown"}1085              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1086            </span>1087          </div>1088          <div class="peach-info-notice" style="margin-top:8px">1089            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1090          </div>1091          <Show when={passkeyState()?.conflict}>1092            <div class="peach-settings-row peach-settings-row--meta">1093              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1094            </div>1095          </Show>1096        </div>1097      </div>1098    );1099  }11001101  function renderVault() {1102    return (1103      <div class="peach-settings-body">1104        <div class="peach-settings-group">1105          <p class="peach-settings-sub-label">ENTRIES</p>1106          <div class="peach-settings-row peach-settings-row--meta">1107            <span>Entries</span>1108            <span class="peach-settings-value">{props.entriesCount}</span>1109          </div>1110          <div class="peach-settings-row peach-settings-row--meta">1111            <span>Duplicates</span>1112            <Show when={dedupDone() !== null} fallback={1113              <button1114                class="peach-btn peach-btn--subtle peach-btn--sm"1115                onClick={() => void handleCountDuplicates()}1116                disabled={dedupRunning()}1117              >1118                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1119              </button>1120            }>1121              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1122                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1123              </span>1124            </Show>1125          </div>1126          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1127            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1128              <div class="peach-restore-warning">1129                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1130              </div>1131              <div style="display:flex;gap:6px">1132                <button1133                  class="peach-btn peach-btn--subtle peach-btn--sm"1134                  onClick={() => setDedupCount(null)}1135                >1136                  cancel1137                </button>1138                <button1139                  class="peach-btn peach-btn--accent peach-btn--sm"1140                  onClick={() => void handleDeduplicate()}1141                  disabled={dedupRunning()}1142                >1143                  {dedupRunning() ? "removing..." : "remove duplicates"}1144                </button>1145              </div>1146            </div>1147          </Show>11481149          <p class="peach-settings-sub-label">MAINTENANCE</p>1150          <div class="peach-settings-row peach-settings-row--meta">1151            <span>Trash auto-delete</span>1152            <select1153              class="peach-settings-select"1154              value={String(props.trashRetentionDays)}1155              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1156            >1157              <option value="7">7 days</option>1158              <option value="30">30 days</option>1159              <option value="90">90 days</option>1160              <option value="0">Never</option>1161            </select>1162          </div>1163          <div class="peach-settings-row peach-settings-row--meta">1164            <span>Vault Health</span>1165            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1166              open1167            </button>1168          </div>1169          <Show when={declinedCardCount() > 0}>1170            <div class="peach-settings-row peach-settings-row--meta">1171              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1172              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1173                Clear1174              </button>1175            </div>1176          </Show>11771178          <p class="peach-settings-sub-label">DATA</p>1179          <div class="peach-settings-row peach-settings-row--meta">1180            <span>Import passwords</span>1181            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1182              open1183            </button>1184          </div>1185          <div class="peach-settings-row peach-settings-row--meta">1186            <span>Export backup</span>1187            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1188              open1189            </button>1190          </div>11911192          <p class="peach-settings-sub-label">VAULT HISTORY</p>1193          <Show when={historyLoading()}>1194            <p class="peach-settings-hint">Loading…</p>1195          </Show>1196          <Show when={!historyLoading() && historySnapshots().length === 0}>1197            <p class="peach-settings-hint">1198              No snapshots yet. Peach saves automatically before every change —1199              saves, syncs, and imports.1200            </p>1201          </Show>1202          <Show when={!historyLoading() && historySnapshots().length > 0}>1203            <p class="peach-settings-hint">1204              Restoring locks your vault. Unlock with the password that1205              encrypted that snapshot to confirm.1206            </p>1207            <div class="peach-history-list">1208              <For each={historySnapshots()}>1209                {(snap) => (1210                  <div class="peach-history-row">1211                    <div class="peach-history-meta">1212                      <span class="peach-history-time">1213                        {formatSnapshotTime(snap.savedAt)}1214                      </span>1215                      <span class="peach-history-sub">1216                        {[1217                          snap.entryCount !== null1218                            ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1219                            : null,1220                          reasonLabel(snap.reason),1221                        ]1222                          .filter(Boolean)1223                          .join(" · ")}1224                      </span>1225                    </div>1226                    <button1227                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1228                        historyRestoring() === snap.id ? " is-busy" : ""1229                      }`}1230                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1231                      disabled={historyRestoring() !== null}1232                    >1233                      <Show when={historyRestoring() === snap.id} fallback="Restore">1234                        …1235                      </Show>1236                    </button>1237                  </div>1238                )}1239              </For>1240            </div>1241            <p class="peach-history-footer">1242              {historySnapshots().length} of 10 snapshots1243            </p>1244          </Show>12451246          <button1247            class="peach-settings-advanced-toggle"1248            onClick={() => setShowVaultAdvanced((v) => !v)}1249            aria-expanded={showVaultAdvanced()}1250          >1251            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1252            <span class="peach-settings-advanced-chevron">1253              {showVaultAdvanced() ? "▲" : "▼"}1254            </span>1255          </button>12561257          <Show when={showVaultAdvanced()}>1258            <div class="peach-danger-card">1259              <div class="peach-danger-card-head">1260                <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1261                <h3 class="peach-danger-card-title">Replace vault from backup</h3>1262              </div>1263              <p class="peach-danger-card-copy">1264                Wipes the current vault and restores from a backup file.1265                Recoverable only if your backup is good.1266              </p>1267              <button1268                class="peach-danger-card-button"1269                onClick={() => props.onOpenFileImport?.()}1270              >1271                Choose backup file1272              </button>1273            </div>12741275            <Show when={props.onOpenPanic}>1276              <div class="peach-danger-card peach-danger-card--hazard">1277                <div class="peach-danger-card-head">1278                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1279                  <h3 class="peach-danger-card-title">Destroy vault</h3>1280                </div>1281                <p class="peach-danger-card-copy">1282                  Permanently wipes every entry, setting, and license on this1283                  device. There is no recovery.1284                </p>1285                <button1286                  class="peach-danger-card-button"1287                  onClick={() => props.onOpenPanic?.()}1288                >1289                  Begin destruction1290                </button>1291              </div>1292            </Show>1293          </Show>1294        </div>1295      </div>1296    );1297  }12981299  function renderSync() {1300    return (1301      <div class="peach-settings-body">1302        <div class="peach-settings-group">1303          {/* Pairing is free — always show Add a device and paired list */}1304          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1305            <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1306            <Show when={pairedDevices().length === 0}>1307              <p class="peach-settings-hint">1308                No paired devices yet. Add a device to start syncing.1309              </p>1310            </Show>1311            <Show when={pairedDevices().length > 0}>1312              <div class="peach-history-list">1313                <For each={pairedDevices()}>1314                  {(device) => (1315                    <div class="peach-history-row">1316                      <div class="peach-history-meta">1317                        <span class="peach-history-time">{device.displayName}</span>1318                        <span class="peach-history-sub">1319                          {device.lastSyncAt1320                            ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1321                            : "Never synced"}1322                        </span>1323                      </div>1324                      <button1325                        class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1326                        onClick={() => void handleRemoveDevice(device.deviceId)}1327                      >1328                        Remove1329                      </button>1330                    </div>1331                  )}1332                </For>1333              </div>1334            </Show>1335            <div class="peach-settings-actions--row">1336              <button1337                class="peach-btn peach-btn--subtle peach-btn--sm"1338                onClick={() => props.onOpenSyncPairing?.()}1339              >1340                Add a device1341              </button>1342            </div>1343          </div>13441345          {/* Automatic sync is Pro-gated */}1346          <Show1347            when={!isPro()}1348            fallback={1349              <>1350                <div class="peach-settings-row">1351                  <span>Status</span>1352                  <span class="peach-settings-value peach-settings-value--status">1353                    <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1354                    {getStatusLabel(syncStatus())}1355                  </span>1356                </div>1357                <div class="peach-settings-row peach-settings-row--meta">1358                  <span>Last sync</span>1359                  <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1360                </div>1361                <div class="peach-settings-row peach-settings-row--meta">1362                  <button1363                    class="peach-btn peach-btn--subtle peach-btn--sm"1364                    onClick={() => void handleSyncNow()}1365                    disabled={syncStatus() === "not_trusted" || syncingNow()}1366                  >1367                    <Switch>1368                      <Match when={syncingNow()}>Syncing…</Match>1369                      <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1370                      <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1371                      <Match when={true}>Sync now</Match>1372                    </Switch>1373                  </button>1374                </div>1375                <p class="peach-settings-hint">1376                  If you edit on multiple devices before syncing, the most recently1377                  changed device wins. Use Vault History above to recover older1378                  versions if needed.1379                </p>13801381                <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1382                  <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1383                  <For each={trustedNetworks()}>1384                    {(network) => (1385                      <div class="peach-sync-device-row">1386                        <span class="peach-sync-device-name">1387                          {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1388                        </span>1389                        <Show1390                          when={!network.isTailscale}1391                          fallback={<span class="peach-sync-device-meta">Always trusted</span>}1392                        >1393                          <button1394                            class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1395                            onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1396                          >1397                            Remove1398                          </button>1399                        </Show>1400                      </div>1401                    )}1402                  </For>1403                  <div class="peach-settings-actions--row">1404                    <Show when={!isCurrentNetworkTrusted()}>1405                      <button1406                        class="peach-btn peach-btn--subtle peach-btn--sm"1407                        onClick={() => void handleMarkNetworkTrusted()}1408                        disabled={markingNetwork()}1409                      >1410                        {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1411                      </button>1412                    </Show>1413                  </div>1414                </div>14151416                <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1417                  <span class="peach-sync-section-label">Advanced</span>1418                  <Show1419                    when={resetIdentityConfirm()}1420                    fallback={1421                      <div class="peach-sync-advanced-row">1422                        <div class="peach-sync-advanced-info">1423                          <span class="peach-sync-advanced-name">Reset sync identity</span>1424                          <span class="peach-sync-device-meta">1425                            Generates a new keypair and unpairs every device.1426                          </span>1427                        </div>1428                        <button1429                          class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1430                          onClick={() => setResetIdentityConfirm(true)}1431                        >1432                          Reset1433                        </button>1434                      </div>1435                    }1436                  >1437                    <div class="peach-sync-reset-confirm">1438                      <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1439                      <p class="peach-sync-reset-confirm-body">1440                        This will unpair all devices. You will need to pair them again from scratch.1441                      </p>1442                      <div class="peach-settings-actions--row">1443                        <button1444                          class="peach-btn peach-btn--ghost peach-btn--sm"1445                          onClick={() => setResetIdentityConfirm(false)}1446                          disabled={resetIdentityRunning()}1447                        >1448                          Cancel1449                        </button>1450                        <button1451                          class="peach-btn peach-btn--danger peach-btn--sm"1452                          onClick={() => void handleResetSyncIdentity()}1453                          disabled={resetIdentityRunning()}1454                        >1455                          {resetIdentityRunning() ? "Resetting..." : "Reset"}1456                        </button>1457                      </div>1458                    </div>1459                  </Show>1460                </div>1461              </>1462            }1463          >1464            <div class="peach-sync-pro-hint">1465              <p class="peach-settings-hint">Automatic sync is a Peach Pro feature.</p>1466              <button1467                class="peach-btn peach-btn--subtle peach-btn--sm"1468                onClick={() => setActiveSection("license")}1469              >1470                Upgrade to Pro1471              </button>1472            </div>1473          </Show>1474        </div>1475      </div>1476    );1477  }14781479  function renderAppearance() {1480    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1481      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1482      { id: "cormorant", label: "Cormorant", preview: "Aa" },1483      { id: "inter", label: "Inter", preview: "Aa" },1484      { id: "system", label: "System", preview: "Aa" },1485    ];1486    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1487      { id: "small", label: "Small" },1488      { id: "medium", label: "Default" },1489      { id: "large", label: "Large" },1490    ];1491    return (1492      <div class="peach-settings-body">1493        <div class="peach-settings-group">1494          <p class="peach-settings-sub-label">THEME</p>1495          <div class="peach-settings-row peach-settings-row--meta">1496            <span>Theme</span>1497            <button1498              class="peach-btn peach-btn--subtle peach-btn--sm"1499              onClick={() =>1500                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1501              }1502            >1503              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1504            </button>1505          </div>15061507          <p class="peach-settings-sub-label">ACCENT</p>1508          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1509            <span>Accent</span>1510            <div class="peach-accent-picker">1511              <For each={ACCENT_COLORS}>1512                {(color) => (1513                  <button1514                    class="peach-accent-swatch"1515                    classList={{ "is-active": props.accentColor === color.name }}1516                    style={{ background: color.hex }}1517                    onClick={() => void props.onAccentChange(color.name)}1518                    aria-label={color.name}1519                    title={color.name}1520                  />1521                )}1522              </For>1523            </div>1524          </div>15251526          <p class="peach-settings-sub-label">DISPLAY FONT</p>1527          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1528            <span>Font</span>1529            <div class="peach-font-picker">1530              <For each={fontOptions}>1531                {(opt) => (1532                  <button1533                    class="peach-font-swatch"1534                    classList={{ "is-active": props.displayFont === opt.id }}1535                    data-font-preview={opt.id}1536                    onClick={() => void props.onDisplayFontChange(opt.id)}1537                    aria-label={opt.label}1538                    title={opt.label}1539                  >1540                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1541                    <span class="peach-font-swatch-name">{opt.label}</span>1542                  </button>1543                )}1544              </For>1545            </div>1546          </div>15471548          <p class="peach-settings-sub-label">TEXT SIZE</p>1549          <div class="peach-settings-row peach-settings-row--meta">1550            <span>Size</span>1551            <div class="peach-pill-group">1552              <For each={sizeOptions}>1553                {(opt) => (1554                  <button1555                    class="peach-btn peach-btn--ghost peach-btn--sm"1556                    classList={{ "is-active": props.textSize === opt.id }}1557                    onClick={() => void props.onTextSizeChange(opt.id)}1558                  >1559                    {opt.label}1560                  </button>1561                )}1562              </For>1563            </div>1564          </div>15651566          <p class="peach-settings-sub-label">MOTION</p>1567          <div class="peach-settings-row peach-settings-row--meta">1568            <span>Reduce motion</span>1569            <button1570              class="peach-btn peach-btn--subtle peach-btn--sm"1571              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1572            >1573              {props.reduceMotion ? "On" : "Off"}1574            </button>1575          </div>1576        </div>1577      </div>1578    );1579  }15801581  function renderShortcuts() {1582    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1583    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15841585    function startCapture(action: ShortcutAction) {1586      setConflictMsg(null);1587      setCapturingAction(action);1588    }1589    function cancelCapture() {1590      setCapturingAction(null);1591      setConflictMsg(null);1592    }1593    function commitBinding(action: ShortcutAction, binding: Keybinding) {1594      const conflict = findConflict(props.shortcuts, binding, action);1595      if (conflict) {1596        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1597        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1598        return;1599      }1600      const next = { ...props.shortcuts, [action]: binding };1601      setCapturingAction(null);1602      setConflictMsg(null);1603      void props.onShortcutsChange(next);1604    }1605    function resetAction(action: ShortcutAction) {1606      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1607      void props.onShortcutsChange(next);1608    }1609    function resetAll() {1610      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1611    }16121613    // Capture-mode key handler — wired via ref onto the chip element so it1614    // catches the next non-modifier keypress and ignores other bindings.1615    function handleCaptureKey(e: KeyboardEvent) {1616      e.preventDefault();1617      e.stopPropagation();1618      if (e.key === "Escape") { cancelCapture(); return; }1619      const binding = keybindingFromEvent(e);1620      if (!binding) return; // bare modifier — keep waiting1621      const action = capturingAction();1622      if (action) commitBinding(action, binding);1623    }16241625    return (1626      <div class="peach-settings-body">1627        <div class="peach-settings-group peach-shortcuts-group">1628          <p class="peach-settings-sub-label">ENTRY VIEW</p>1629          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1630            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1631          </For>1632        </div>16331634        <div class="peach-settings-group peach-shortcuts-group">1635          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1636          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1637            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1638          </For>1639        </div>16401641        <Show when={conflictMsg()}>1642          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1643        </Show>16441645        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1646          <div class="peach-settings-row peach-settings-row--meta">1647            <span>Inline autofill on page</span>1648            <a1649              class="peach-settings-link"1650              href="chrome://extensions/shortcuts"1651              onClick={(e) => {1652                e.preventDefault();1653                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1654              }}1655            >1656              ⌘⇧P — customize ↗1657            </a>1658          </div>1659          <button1660            class="peach-btn peach-btn--subtle peach-btn--sm"1661            style="align-self:flex-start"1662            onClick={resetAll}1663          >1664            Reset all to defaults1665          </button>1666        </div>1667      </div>1668    );16691670    function renderRow(1671      desc: typeof SHORTCUT_DESCRIPTORS[number],1672      capturing: () => ShortcutAction | null,1673      onStart: (a: ShortcutAction) => void,1674      onCancel: () => void,1675      onReset: (a: ShortcutAction) => void,1676      onKey: (e: KeyboardEvent) => void,1677    ) {1678      const isActive = () => capturing() === desc.action;1679      const binding = () => props.shortcuts[desc.action];1680      const isCustom = () => {1681        const b = binding();1682        const d = DEFAULT_BINDINGS[desc.action];1683        return b.key !== d.key1684          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1685          || Boolean(b.meta)  !== Boolean(d.meta)1686          || Boolean(b.shift) !== Boolean(d.shift)1687          || Boolean(b.alt)   !== Boolean(d.alt);1688      };1689      return (1690        <div class="peach-shortcut-row">1691          <span class="peach-shortcut-row-label">{desc.label}</span>1692          <button1693            class="peach-shortcut-chip"1694            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1695            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1696            onKeyDown={(e) => { if (isActive()) onKey(e); }}1697            ref={(el) => {1698              // Auto-focus when entering capture mode so the keydown listener catches the next press.1699              if (el) {1700                queueMicrotask(() => { if (isActive()) el.focus(); });1701              }1702            }}1703          >1704            {isActive() ? "Press a key…" : formatKeybinding(binding())}1705          </button>1706          <Show when={isCustom() && !isActive()}>1707            <button1708              class="peach-shortcut-reset"1709              title="Reset to default"1710              onClick={() => onReset(desc.action)}1711            >1712              ↺1713            </button>1714          </Show>1715        </div>1716      );1717    }1718  }17191720  const PRO_FEATURES: Array<{1721    feature: ProFeature;1722    name: string;1723    desc: string;1724  }> = [1725    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1726    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1727    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1728    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1729    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1730  ];17311732  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17331734  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1735  const [deactivating, setDeactivating] = createSignal(false);17361737  async function handleDeactivateLicense() {1738    setDeactivating(true);1739    await sendMessage({ type: "DEACTIVATE_LICENSE" });1740    setLicenseInfo(null);1741    await props.onLicenseChange?.();1742    setDeactivating(false);1743  }17441745  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1746    setCanaryTestingId(entry.id);1747    setCanaryTestResult(null);1748    const res = await sendMessage({1749      type: "FIRE_CANARY_PING",1750      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1751    }) as { ok: boolean };1752    setCanaryTestResult({ id: entry.id, ok: res.ok });1753    setCanaryTestingId(null);1754    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1755  }17561757  function renderLicense() {1758    async function onActivate() {1759      await handleActivateLicense();1760      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1761        await props.onLicenseChange?.();1762      }1763    }17641765    return (1766      <div class="peach-settings-body">1767        <Show1768          when={isPro()}1769          fallback={1770            <Show1771              when={showKeyEntry()}1772              fallback={1773                <div class="peach-pro-upgrade">1774                  <div class="peach-pro-upgrade-hero">1775                    <div class="peach-pro-upgrade-mark-wrap">1776                      <PeachMark class="peach-pro-upgrade-mark" />1777                    </div>1778                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1779                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1780                  </div>17811782                  <div class="peach-pro-features">1783                    <For each={PRO_FEATURES}>1784                      {(item) => (1785                        <div1786                          class="peach-pro-feature"1787                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1788                        >1789                          <span class="peach-pro-feature-mark">✦</span>1790                          <div class="peach-pro-feature-text">1791                            <span class="peach-pro-feature-name">{item.name}</span>1792                            <span class="peach-pro-feature-desc">{item.desc}</span>1793                          </div>1794                        </div>1795                      )}1796                    </For>1797                  </div>17981799                  <div class="peach-pro-upgrade-actions">1800                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1801                    <a1802                      href="https://peachpasswords.com/#pricing"1803                      target="_blank"1804                      rel="noopener noreferrer"1805                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1806                    >1807                      Get Peach Pro1808                    </a>1809                    <div class="peach-pro-key-divider"><span>or</span></div>1810                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1811                      Activate a license key1812                    </button>1813                  </div>1814                </div>1815              }1816            >1817              <div class="peach-pro-key-ceremony">1818                <LockRuneCanvas />1819                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1820                <input1821                  class="peach-underline-input peach-pro-key-input"1822                  type="text"1823                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1824                  value={licenseKeyInput()}1825                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1826                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1827                  autocomplete="off"1828                  spellcheck={false}1829                />1830                <Show when={licenseError()}>1831                  <p class="peach-pro-key-error">That key doesn't look right.</p>1832                </Show>1833                <button1834                  class="peach-btn peach-btn--accent peach-pro-key-activate"1835                  onClick={() => void onActivate()}1836                  disabled={activatingLicense() || !licenseKeyInput().trim()}1837                >1838                  {activatingLicense() ? "activating…" : "Activate"}1839                </button>1840                <button1841                  class="peach-pro-key-cancel"1842                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1843                >1844                  cancel1845                </button>1846              </div>1847            </Show>1848          }1849        >1850          <div class="peach-pro-active">1851            <div class="peach-pro-active-hero">1852              <div class="peach-pro-active-mark-wrap">1853                <PeachMark class="peach-pro-active-mark" />1854              </div>1855              <h2 class="peach-pro-active-title">Peach Pro</h2>1856              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1857            </div>18581859            <Show when={licenseKey()}>1860              <div class="peach-settings-license">1861                <span class="peach-settings-label">LICENSE KEY</span>1862                <div class="peach-settings-license-row">1863                  <span class="peach-settings-license-key">{licenseKey()!}</span>1864                  <button1865                    class="peach-settings-license-copy"1866                    onClick={() => copyLicenseKey(licenseKey()!)}1867                  >1868                    {copied() ? "✓" : "copy"}1869                  </button>1870                </div>1871              </div>1872            </Show>18731874            <div class="peach-pro-active-features">1875              <For each={PRO_FEATURES}>1876                {(item) => (1877                  <div class="peach-pro-active-feature">1878                    <span class="peach-pro-active-feature-mark">✦</span>1879                    <span class="peach-pro-active-feature-name">{item.name}</span>1880                  </div>1881                )}1882              </For>1883            </div>18841885            <button1886              class="peach-pro-deactivate"1887              onClick={() => void handleDeactivateLicense()}1888              disabled={deactivating()}1889            >1890              {deactivating() ? "Deactivating…" : "Deactivate"}1891            </button>1892          </div>1893        </Show>1894      </div>1895    );1896  }18971898  function renderFeedback() {1899    return (1900      <div class="peach-settings-body">1901        <div class="peach-settings-group peach-feedback-group">1902          <p class="peach-feedback-intro">1903            Help us improve Peach. Every report goes directly to our inbox.1904          </p>19051906          <p class="peach-settings-sub-label">TYPE</p>1907          <div class="peach-feedback-chips">1908            <For each={FEEDBACK_CATEGORIES}>1909              {(cat) => (1910                <button1911                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1912                  onClick={() => setFbCategory(cat)}1913                >1914                  {cat}1915                </button>1916              )}1917            </For>1918          </div>19191920          <p class="peach-settings-sub-label">MESSAGE</p>1921          <textarea1922            class="peach-feedback-textarea"1923            placeholder="Describe the issue or idea..."1924            value={fbMessage()}1925            onInput={(e) => setFbMessage(e.currentTarget.value)}1926          />19271928          <p class="peach-feedback-meta">1929            Version {version} · will be included automatically1930          </p>19311932          <Show when={fbResult() === "success"}>1933            <p class="peach-feedback-result peach-feedback-result--ok">1934              Sent — thank you1935            </p>1936          </Show>1937          <Show when={fbResult() === "error"}>1938            <p class="peach-feedback-result peach-feedback-result--err">1939              Failed to send — try again1940            </p>1941          </Show>19421943          <button1944            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1945            onClick={() => void handleSubmitFeedback()}1946            disabled={fbSubmitting() || !fbMessage().trim()}1947          >1948            {fbSubmitting() ? "Sending..." : "Send feedback"}1949          </button>19501951          <p class="peach-feedback-anon">1952            Sent anonymously · no account required1953          </p>1954        </div>1955      </div>1956    );1957  }19581959  return (1960    <Show1961      when={showNewRecoveryWords()}1962      fallback={1963        <div class="peach-overlay-view ov-enter-right" style="display:flex">1964          <div class="peach-overlay-header">1965            <button1966              class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1967              onClick={handleHeaderBack}1968              aria-label="Back"1969            >1970              <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1971                <path d="M6 1L1 6L6 11" />1972              </svg>1973            </button>1974            <span class="peach-overlay-title">{currentTitle()}</span>1975            <div class="peach-overlay-spacer"></div>1976          </div>19771978          <Switch fallback={renderHome()}>1979            <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1980            <Match when={activeSection() === "vault"}>{renderVault()}</Match>1981            <Match when={activeSection() === "sync"}>{renderSync()}</Match>1982            <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1983            <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1984            <Match when={activeSection() === "license"}>{renderLicense()}</Match>1985            <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1986          </Switch>1987        </div>1988      }1989    >1990      <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />1991    </Show>1992  );1993}