Settings.tsx

78.9 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: "Pair devices · sync is free", icon: iconSync })}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          <p class="peach-sync-network-note">1304            Sync works on your home and trusted networks. Some public or1305            corporate networks block device connections.1306          </p>13071308          {/* Add a device — FREE, always shown */}1309          <button1310            class="peach-settings-row peach-settings-row--tappable"1311            onClick={() => props.onOpenSyncPairing?.()}1312          >1313            <span>Add a device</span>1314            <svg width="6" height="10" viewBox="0 0 6 10" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1315              <path d="M1 1l4 4-4 4" />1316            </svg>1317          </button>13181319          {/* Paired devices list — shown when any exist */}1320          <Show when={pairedDevices().length > 0}>1321            <div class="peach-history-list">1322              <For each={pairedDevices()}>1323                {(device) => (1324                  <div class="peach-history-row">1325                    <div class="peach-history-meta">1326                      <span class="peach-history-time">{device.displayName}</span>1327                      <span class="peach-history-sub">1328                        {device.lastSyncAt1329                          ? `Last synced ${formatRelativeTime(device.lastSyncAt)}`1330                          : "Never synced"}1331                      </span>1332                    </div>1333                    <button1334                      class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1335                      onClick={() => void handleRemoveDevice(device.deviceId)}1336                    >1337                      Remove1338                    </button>1339                  </div>1340                )}1341              </For>1342            </div>1343          </Show>13441345          {/* Automatic sync — PRO only */}1346          <Show1347            when={isPro()}1348            fallback={1349              <Show when={pairedDevices().length > 0}>1350                <div class="peach-sync-pro-hint">1351                  <p class="peach-settings-hint">1352                    Upgrade to Pro for automatic background sync across all your devices.1353                  </p>1354                  <button1355                    class="peach-btn peach-btn--subtle peach-btn--sm"1356                    onClick={() => setActiveSection("license")}1357                  >1358                    Upgrade to Pro1359                  </button>1360                </div>1361              </Show>1362            }1363          >1364            <div class="peach-settings-row">1365              <span>Status</span>1366              <span class="peach-settings-value peach-settings-value--status">1367                <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1368                {getStatusLabel(syncStatus())}1369              </span>1370            </div>1371            <div class="peach-settings-row peach-settings-row--meta">1372              <span>Last sync</span>1373              <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1374            </div>1375            <div class="peach-settings-row peach-settings-row--meta">1376              <button1377                class="peach-btn peach-btn--subtle peach-btn--sm"1378                onClick={() => void handleSyncNow()}1379                disabled={syncStatus() === "not_trusted" || syncingNow()}1380              >1381                <Switch>1382                  <Match when={syncingNow()}>Syncing…</Match>1383                  <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1384                  <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1385                  <Match when={true}>Sync now</Match>1386                </Switch>1387              </button>1388            </div>1389            <p class="peach-settings-hint">1390              If you edit on multiple devices before syncing, the most recently1391              changed device wins. Use Vault History to recover older versions.1392            </p>13931394            <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1395              <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1396              <For each={trustedNetworks()}>1397                {(network) => (1398                  <div class="peach-sync-device-row">1399                    <span class="peach-sync-device-name">1400                      {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1401                    </span>1402                    <Show1403                      when={!network.isTailscale}1404                      fallback={<span class="peach-sync-device-meta">Always trusted</span>}1405                    >1406                      <button1407                        class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1408                        onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1409                      >1410                        Remove1411                      </button>1412                    </Show>1413                  </div>1414                )}1415              </For>1416              <div class="peach-settings-actions--row">1417                <Show when={!isCurrentNetworkTrusted()}>1418                  <button1419                    class="peach-btn peach-btn--subtle peach-btn--sm"1420                    onClick={() => void handleMarkNetworkTrusted()}1421                    disabled={markingNetwork()}1422                  >1423                    {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1424                  </button>1425                </Show>1426              </div>1427            </div>14281429            <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1430              <span class="peach-sync-section-label">Advanced</span>1431              <Show1432                when={resetIdentityConfirm()}1433                fallback={1434                  <div class="peach-sync-advanced-row">1435                    <div class="peach-sync-advanced-info">1436                      <span class="peach-sync-advanced-name">Reset sync identity</span>1437                      <span class="peach-sync-device-meta">1438                        Generates a new keypair and unpairs every device.1439                      </span>1440                    </div>1441                    <button1442                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1443                      onClick={() => setResetIdentityConfirm(true)}1444                    >1445                      Reset1446                    </button>1447                  </div>1448                }1449              >1450                <div class="peach-sync-reset-confirm">1451                  <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1452                  <p class="peach-sync-reset-confirm-body">1453                    This will unpair all devices. You will need to pair them again from scratch.1454                  </p>1455                  <div class="peach-settings-actions--row">1456                    <button1457                      class="peach-btn peach-btn--ghost peach-btn--sm"1458                      onClick={() => setResetIdentityConfirm(false)}1459                      disabled={resetIdentityRunning()}1460                    >1461                      Cancel1462                    </button>1463                    <button1464                      class="peach-btn peach-btn--danger peach-btn--sm"1465                      onClick={() => void handleResetSyncIdentity()}1466                      disabled={resetIdentityRunning()}1467                    >1468                      {resetIdentityRunning() ? "Resetting..." : "Reset"}1469                    </button>1470                  </div>1471                </div>1472              </Show>1473            </div>1474          </Show>1475        </div>1476      </div>1477    );1478  }14791480  function renderAppearance() {1481    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1482      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1483      { id: "cormorant", label: "Cormorant", preview: "Aa" },1484      { id: "inter", label: "Inter", preview: "Aa" },1485      { id: "system", label: "System", preview: "Aa" },1486    ];1487    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1488      { id: "small", label: "Small" },1489      { id: "medium", label: "Default" },1490      { id: "large", label: "Large" },1491    ];1492    return (1493      <div class="peach-settings-body">1494        <div class="peach-settings-group">1495          <p class="peach-settings-sub-label">THEME</p>1496          <div class="peach-settings-row peach-settings-row--meta">1497            <span>Theme</span>1498            <button1499              class="peach-btn peach-btn--subtle peach-btn--sm"1500              onClick={() =>1501                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1502              }1503            >1504              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1505            </button>1506          </div>15071508          <p class="peach-settings-sub-label">ACCENT</p>1509          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1510            <span>Accent</span>1511            <div class="peach-accent-picker">1512              <For each={ACCENT_COLORS}>1513                {(color) => (1514                  <button1515                    class="peach-accent-swatch"1516                    classList={{ "is-active": props.accentColor === color.name }}1517                    style={{ background: color.hex }}1518                    onClick={() => void props.onAccentChange(color.name)}1519                    aria-label={color.name}1520                    title={color.name}1521                  />1522                )}1523              </For>1524            </div>1525          </div>15261527          <p class="peach-settings-sub-label">DISPLAY FONT</p>1528          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1529            <span>Font</span>1530            <div class="peach-font-picker">1531              <For each={fontOptions}>1532                {(opt) => (1533                  <button1534                    class="peach-font-swatch"1535                    classList={{ "is-active": props.displayFont === opt.id }}1536                    data-font-preview={opt.id}1537                    onClick={() => void props.onDisplayFontChange(opt.id)}1538                    aria-label={opt.label}1539                    title={opt.label}1540                  >1541                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1542                    <span class="peach-font-swatch-name">{opt.label}</span>1543                  </button>1544                )}1545              </For>1546            </div>1547          </div>15481549          <p class="peach-settings-sub-label">TEXT SIZE</p>1550          <div class="peach-settings-row peach-settings-row--meta">1551            <span>Size</span>1552            <div class="peach-pill-group">1553              <For each={sizeOptions}>1554                {(opt) => (1555                  <button1556                    class="peach-btn peach-btn--ghost peach-btn--sm"1557                    classList={{ "is-active": props.textSize === opt.id }}1558                    onClick={() => void props.onTextSizeChange(opt.id)}1559                  >1560                    {opt.label}1561                  </button>1562                )}1563              </For>1564            </div>1565          </div>15661567          <p class="peach-settings-sub-label">MOTION</p>1568          <div class="peach-settings-row peach-settings-row--meta">1569            <span>Reduce motion</span>1570            <button1571              class="peach-btn peach-btn--subtle peach-btn--sm"1572              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1573            >1574              {props.reduceMotion ? "On" : "Off"}1575            </button>1576          </div>1577        </div>1578      </div>1579    );1580  }15811582  function renderShortcuts() {1583    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1584    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15851586    function startCapture(action: ShortcutAction) {1587      setConflictMsg(null);1588      setCapturingAction(action);1589    }1590    function cancelCapture() {1591      setCapturingAction(null);1592      setConflictMsg(null);1593    }1594    function commitBinding(action: ShortcutAction, binding: Keybinding) {1595      const conflict = findConflict(props.shortcuts, binding, action);1596      if (conflict) {1597        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1598        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1599        return;1600      }1601      const next = { ...props.shortcuts, [action]: binding };1602      setCapturingAction(null);1603      setConflictMsg(null);1604      void props.onShortcutsChange(next);1605    }1606    function resetAction(action: ShortcutAction) {1607      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1608      void props.onShortcutsChange(next);1609    }1610    function resetAll() {1611      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1612    }16131614    // Capture-mode key handler — wired via ref onto the chip element so it1615    // catches the next non-modifier keypress and ignores other bindings.1616    function handleCaptureKey(e: KeyboardEvent) {1617      e.preventDefault();1618      e.stopPropagation();1619      if (e.key === "Escape") { cancelCapture(); return; }1620      const binding = keybindingFromEvent(e);1621      if (!binding) return; // bare modifier — keep waiting1622      const action = capturingAction();1623      if (action) commitBinding(action, binding);1624    }16251626    return (1627      <div class="peach-settings-body">1628        <div class="peach-settings-group peach-shortcuts-group">1629          <p class="peach-settings-sub-label">ENTRY VIEW</p>1630          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1631            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1632          </For>1633        </div>16341635        <div class="peach-settings-group peach-shortcuts-group">1636          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1637          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1638            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1639          </For>1640        </div>16411642        <Show when={conflictMsg()}>1643          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1644        </Show>16451646        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1647          <div class="peach-settings-row peach-settings-row--meta">1648            <span>Inline autofill on page</span>1649            <a1650              class="peach-settings-link"1651              href="chrome://extensions/shortcuts"1652              onClick={(e) => {1653                e.preventDefault();1654                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1655              }}1656            >1657              ⌘⇧P — customize ↗1658            </a>1659          </div>1660          <button1661            class="peach-btn peach-btn--subtle peach-btn--sm"1662            style="align-self:flex-start"1663            onClick={resetAll}1664          >1665            Reset all to defaults1666          </button>1667        </div>1668      </div>1669    );16701671    function renderRow(1672      desc: typeof SHORTCUT_DESCRIPTORS[number],1673      capturing: () => ShortcutAction | null,1674      onStart: (a: ShortcutAction) => void,1675      onCancel: () => void,1676      onReset: (a: ShortcutAction) => void,1677      onKey: (e: KeyboardEvent) => void,1678    ) {1679      const isActive = () => capturing() === desc.action;1680      const binding = () => props.shortcuts[desc.action];1681      const isCustom = () => {1682        const b = binding();1683        const d = DEFAULT_BINDINGS[desc.action];1684        return b.key !== d.key1685          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1686          || Boolean(b.meta)  !== Boolean(d.meta)1687          || Boolean(b.shift) !== Boolean(d.shift)1688          || Boolean(b.alt)   !== Boolean(d.alt);1689      };1690      return (1691        <div class="peach-shortcut-row">1692          <span class="peach-shortcut-row-label">{desc.label}</span>1693          <button1694            class="peach-shortcut-chip"1695            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1696            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1697            onKeyDown={(e) => { if (isActive()) onKey(e); }}1698            ref={(el) => {1699              // Auto-focus when entering capture mode so the keydown listener catches the next press.1700              if (el) {1701                queueMicrotask(() => { if (isActive()) el.focus(); });1702              }1703            }}1704          >1705            {isActive() ? "Press a key…" : formatKeybinding(binding())}1706          </button>1707          <Show when={isCustom() && !isActive()}>1708            <button1709              class="peach-shortcut-reset"1710              title="Reset to default"1711              onClick={() => onReset(desc.action)}1712            >1713              ↺1714            </button>1715          </Show>1716        </div>1717      );1718    }1719  }17201721  const PRO_FEATURES: Array<{1722    feature: ProFeature;1723    name: string;1724    desc: string;1725  }> = [1726    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1727    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1728    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1729    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1730    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1731  ];17321733  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17341735  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1736  const [deactivating, setDeactivating] = createSignal(false);17371738  async function handleDeactivateLicense() {1739    setDeactivating(true);1740    await sendMessage({ type: "DEACTIVATE_LICENSE" });1741    setLicenseInfo(null);1742    await props.onLicenseChange?.();1743    setDeactivating(false);1744  }17451746  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1747    setCanaryTestingId(entry.id);1748    setCanaryTestResult(null);1749    const res = await sendMessage({1750      type: "FIRE_CANARY_PING",1751      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1752    }) as { ok: boolean };1753    setCanaryTestResult({ id: entry.id, ok: res.ok });1754    setCanaryTestingId(null);1755    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1756  }17571758  function renderLicense() {1759    async function onActivate() {1760      await handleActivateLicense();1761      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1762        await props.onLicenseChange?.();1763      }1764    }17651766    return (1767      <div class="peach-settings-body">1768        <Show1769          when={isPro()}1770          fallback={1771            <Show1772              when={showKeyEntry()}1773              fallback={1774                <div class="peach-pro-upgrade">1775                  <div class="peach-pro-upgrade-hero">1776                    <div class="peach-pro-upgrade-mark-wrap">1777                      <PeachMark class="peach-pro-upgrade-mark" />1778                    </div>1779                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1780                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1781                  </div>17821783                  <div class="peach-pro-features">1784                    <For each={PRO_FEATURES}>1785                      {(item) => (1786                        <div1787                          class="peach-pro-feature"1788                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1789                        >1790                          <span class="peach-pro-feature-mark">✦</span>1791                          <div class="peach-pro-feature-text">1792                            <span class="peach-pro-feature-name">{item.name}</span>1793                            <span class="peach-pro-feature-desc">{item.desc}</span>1794                          </div>1795                        </div>1796                      )}1797                    </For>1798                  </div>17991800                  <div class="peach-pro-upgrade-actions">1801                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1802                    <a1803                      href="https://peachpasswords.com/#pricing"1804                      target="_blank"1805                      rel="noopener noreferrer"1806                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1807                    >1808                      Get Peach Pro1809                    </a>1810                    <div class="peach-pro-key-divider"><span>or</span></div>1811                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1812                      Activate a license key1813                    </button>1814                  </div>1815                </div>1816              }1817            >1818              <div class="peach-pro-key-ceremony">1819                <LockRuneCanvas />1820                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1821                <input1822                  class="peach-underline-input peach-pro-key-input"1823                  type="text"1824                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1825                  value={licenseKeyInput()}1826                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1827                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1828                  autocomplete="off"1829                  spellcheck={false}1830                />1831                <Show when={licenseError()}>1832                  <p class="peach-pro-key-error">That key doesn't look right.</p>1833                </Show>1834                <button1835                  class="peach-btn peach-btn--accent peach-pro-key-activate"1836                  onClick={() => void onActivate()}1837                  disabled={activatingLicense() || !licenseKeyInput().trim()}1838                >1839                  {activatingLicense() ? "activating…" : "Activate"}1840                </button>1841                <button1842                  class="peach-pro-key-cancel"1843                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1844                >1845                  cancel1846                </button>1847              </div>1848            </Show>1849          }1850        >1851          <div class="peach-pro-active">1852            <div class="peach-pro-active-hero">1853              <div class="peach-pro-active-mark-wrap">1854                <PeachMark class="peach-pro-active-mark" />1855              </div>1856              <h2 class="peach-pro-active-title">Peach Pro</h2>1857              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1858            </div>18591860            <Show when={licenseKey()}>1861              <div class="peach-settings-license">1862                <span class="peach-settings-label">LICENSE KEY</span>1863                <div class="peach-settings-license-row">1864                  <span class="peach-settings-license-key">{licenseKey()!}</span>1865                  <button1866                    class="peach-settings-license-copy"1867                    onClick={() => copyLicenseKey(licenseKey()!)}1868                  >1869                    {copied() ? "✓" : "copy"}1870                  </button>1871                </div>1872              </div>1873            </Show>18741875            <div class="peach-pro-active-features">1876              <For each={PRO_FEATURES}>1877                {(item) => (1878                  <div class="peach-pro-active-feature">1879                    <span class="peach-pro-active-feature-mark">✦</span>1880                    <span class="peach-pro-active-feature-name">{item.name}</span>1881                  </div>1882                )}1883              </For>1884            </div>18851886            <button1887              class="peach-pro-deactivate"1888              onClick={() => void handleDeactivateLicense()}1889              disabled={deactivating()}1890            >1891              {deactivating() ? "Deactivating…" : "Deactivate"}1892            </button>1893          </div>1894        </Show>1895      </div>1896    );1897  }18981899  function renderFeedback() {1900    return (1901      <div class="peach-settings-body">1902        <div class="peach-settings-group peach-feedback-group">1903          <p class="peach-feedback-intro">1904            Help us improve Peach. Every report goes directly to our inbox.1905          </p>19061907          <p class="peach-settings-sub-label">TYPE</p>1908          <div class="peach-feedback-chips">1909            <For each={FEEDBACK_CATEGORIES}>1910              {(cat) => (1911                <button1912                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1913                  onClick={() => setFbCategory(cat)}1914                >1915                  {cat}1916                </button>1917              )}1918            </For>1919          </div>19201921          <p class="peach-settings-sub-label">MESSAGE</p>1922          <textarea1923            class="peach-feedback-textarea"1924            placeholder="Describe the issue or idea..."1925            value={fbMessage()}1926            onInput={(e) => setFbMessage(e.currentTarget.value)}1927          />19281929          <p class="peach-feedback-meta">1930            Version {version} · will be included automatically1931          </p>19321933          <Show when={fbResult() === "success"}>1934            <p class="peach-feedback-result peach-feedback-result--ok">1935              Sent — thank you1936            </p>1937          </Show>1938          <Show when={fbResult() === "error"}>1939            <p class="peach-feedback-result peach-feedback-result--err">1940              Failed to send — try again1941            </p>1942          </Show>19431944          <button1945            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1946            onClick={() => void handleSubmitFeedback()}1947            disabled={fbSubmitting() || !fbMessage().trim()}1948          >1949            {fbSubmitting() ? "Sending..." : "Send feedback"}1950          </button>19511952          <p class="peach-feedback-anon">1953            Sent anonymously · no account required1954          </p>1955        </div>1956      </div>1957    );1958  }19591960  return (1961    <Show1962      when={showNewRecoveryWords()}1963      fallback={1964        <div class="peach-overlay-view ov-enter-right" style="display:flex">1965          <div class="peach-overlay-header">1966            <button1967              class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1968              onClick={handleHeaderBack}1969              aria-label="Back"1970            >1971              <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1972                <path d="M6 1L1 6L6 11" />1973              </svg>1974            </button>1975            <span class="peach-overlay-title">{currentTitle()}</span>1976            <div class="peach-overlay-spacer"></div>1977          </div>19781979          <Switch fallback={renderHome()}>1980            <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1981            <Match when={activeSection() === "vault"}>{renderVault()}</Match>1982            <Match when={activeSection() === "sync"}>{renderSync()}</Match>1983            <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1984            <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1985            <Match when={activeSection() === "license"}>{renderLicense()}</Match>1986            <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1987          </Switch>1988        </div>1989      }1990    >1991      <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />1992    </Show>1993  );1994}