Settings.tsx

79.8 KB · raw

1import { createEffect, 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";25import { getHasPaired } from "../../background/sync-storage";26import { SyncInfoSheet } from "../components/SyncInfoSheet";2728interface LicenseInfo {29  valid: boolean;30  plan: "free" | "pro";31  issued?: string;32}3334interface SettingsProps {35  entriesCount: number;36  breachMonitoring: boolean;37  lockOnClose: boolean;38  autoLockMinutes: number;39  trashRetentionDays: number;40  theme: "dark" | "light";41  accentColor: AccentColorName;42  displayFont: "cormorant-italic" | "cormorant" | "inter" | "system";43  textSize: "small" | "medium" | "large";44  reduceMotion: boolean;45  isPro?: boolean;46  initialSection?: string | null;47  upgradeHighlight?: ProFeature | null;48  onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;49  onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;50  onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;51  onTrashRetentionChange: (days: number) => void | Promise<void>;52  onThemeChange: (theme: "dark" | "light") => void;53  onAccentChange: (name: AccentColorName) => void | Promise<void>;54  onDisplayFontChange: (font: "cormorant-italic" | "cormorant" | "inter" | "system") => void | Promise<void>;55  onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise<void>;56  onReduceMotionChange: (enabled: boolean) => void | Promise<void>;57  shortcuts: Record<ShortcutAction, Keybinding>;58  onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;59  onLicenseChange?: () => void | Promise<void>;60  onBack: () => void;61  onImport: () => void;62  onExport: () => void;63  onBreachCheckDone: () => void;64  onOpenHealth?: () => void;65  onOpenSyncPairing?: () => void;66  onOpenFileImport?: () => void;67  onOpenPanic?: () => void;68}6970type SectionName =71  | "security"72  | "vault"73  | "sync"74  | "appearance"75  | "shortcuts"76  | "license"77  | "feedback";7879const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];8081function formatActivatedSince(issued: string | undefined): string {82  if (!issued) return "Active";83  try {84    const d = new Date(issued);85    if (Number.isNaN(d.getTime())) return "Active";86    return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;87  } catch {88    return "Active";89  }90}9192function isSectionName(value: string): value is SectionName {93  return (94    value === "security" ||95    value === "vault" ||96    value === "sync" ||97    value === "appearance" ||98    value === "shortcuts" ||99    value === "license" ||100    value === "feedback"101  );102}103104export function SettingsView(props: SettingsProps) {105  const version = chrome.runtime.getManifest().version;106  const initial = props.initialSection;107  const [activeSection, setActiveSection] = createSignal<SectionName | null>(108    initial && isSectionName(initial) ? (initial as SectionName) : null109  );110111  const [checkingBreaches, setCheckingBreaches] = createSignal(false);112  const [breachResult, setBreachResult] = createSignal<string | null>(null);113  const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);114  const [licenseKeyInput, setLicenseKeyInput] = createSignal("");115  const [licenseError, setLicenseError] = createSignal<string | null>(null);116  const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);117  const [activatingLicense, setActivatingLicense] = createSignal(false);118119  const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);120  const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);121  const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");122  const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);123  // Prefer the prop (cached at App boot), fall back to the sync-state probe.124  // The prop is authoritative once present so license activation reflects125  // immediately without waiting for a sync round-trip.126  const [syncProStatus, setSyncProStatus] = createSignal(false);127  const isPro = () => props.isPro ?? syncProStatus();128  const [hasPaired, setHasPairedLocal] = createSignal(false);129  const [showSyncInfoSheet, setShowSyncInfoSheet] = createSignal(false);130  const [syncingNow, setSyncingNow] = createSignal(false);131  const [biometricSupported, setBiometricSupported] = createSignal(false);132  const [biometricStored, setBiometricStored] = createSignal(false);133  const [biometricBusy, setBiometricBusy] = createSignal(false);134  const [biometricError, setBiometricError] = createSignal<string | null>(null);135  const [syncFeedback, setSyncFeedback] = createSignal<136    { kind: "ok" | "err"; text: string } | null137  >(null);138  const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);139  const [passkeySaving, setPasskeySaving] = createSignal(false);140141  const [dedupCount, setDedupCount] = createSignal<number | null>(null);142  const [dedupRunning, setDedupRunning] = createSignal(false);143  const [dedupDone, setDedupDone] = createSignal<number | null>(null);144145  const [declinedCardCount, setDeclinedCardCount] = createSignal(0);146147  // Vault history (peach_vault_history) — last 10 snapshots, snapshotted148  // BEFORE every real-mode vault write so the user can roll back if a sync149  // or import overwrites the wrong state.150  const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);151  const [historyLoading, setHistoryLoading] = createSignal(false);152  const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);153154  async function loadVaultHistory() {155    setHistoryLoading(true);156    try {157      const res = await sendMessage({ type: "GET_VAULT_HISTORY" });158      const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];159      setHistorySnapshots(list);160    } finally {161      setHistoryLoading(false);162    }163  }164165  async function handleRestoreVaultSnapshot(id: string) {166    if (historyRestoring()) return;167    setHistoryRestoring(id);168    const res = await sendMessage({169      type: "RESTORE_VAULT_SNAPSHOT",170      payload: { snapshotId: id },171    });172    setHistoryRestoring(null);173    if ((res as { ok: boolean }).ok) {174      // Vault is now locked — let the parent close Settings; App.tsx's175      // VAULT_LOCKED listener will route back to the lock screen.176      props.onBack();177    }178  }179180  function formatSnapshotTime(savedAt: number): string {181    const diff = Date.now() - savedAt;182    if (diff < 60_000) return "Just now";183    if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;184    const d = new Date(savedAt);185    const isToday = new Date().toDateString() === d.toDateString();186    const isYest =187      new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();188    const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });189    if (isToday) return `Today ${t}`;190    if (isYest) return `Yesterday ${t}`;191    return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;192  }193194  function reasonLabel(reason: VaultSnapshot["reason"]): string {195    return (196      {197        local_save: "saved",198        sync_received: "synced",199        import: "imported",200        restore: "restored",201      } as const202    )[reason] ?? reason;203  }204205  const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);206207  const [showChangePw, setShowChangePw] = createSignal(false);208  const [changePwCurrent, setChangePwCurrent] = createSignal("");209  const [changePwNew, setChangePwNew] = createSignal("");210  const [changePwConfirm, setChangePwConfirm] = createSignal("");211  const [changePwSaving, setChangePwSaving] = createSignal(false);212  const [changePwError, setChangePwError] = createSignal<string | null>(null);213  const [changePwSuccess, setChangePwSuccess] = createSignal(false);214  const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);215216  const [duressEnabled, setDuressEnabled] = createSignal(false);217  const [showDuressSetup, setShowDuressSetup] = createSignal(false);218  const [duressPassword, setDuressPassword] = createSignal("");219  const [duressConfirm, setDuressConfirm] = createSignal("");220  const [duressMainPassword, setDuressMainPassword] = createSignal("");221  const [duressSaving, setDuressSaving] = createSignal(false);222  const [duressError, setDuressError] = createSignal<string | null>(null);223  const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);224225  const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);226227  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);228  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);229230  const [fbCategory, setFbCategory] = createSignal("Bug report");231  const [fbMessage, setFbMessage] = createSignal("");232  const [fbSubmitting, setFbSubmitting] = createSignal(false);233  const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);234235  onMount(async () => {236    const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes, pairedFlag] = await Promise.all([237      sendMessage({ type: "GET_LICENSE" }),238      sendMessage({ type: "GET_SYNC_STATE" }),239      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),240      sendMessage({ type: "IS_DURESS_ENABLED" }),241      sendMessage({ type: "GET_SETTINGS" }),242      sendMessage({ type: "GET_LICENSE_KEY" }),243      getHasPaired(),244    ]);245246    setHasPairedLocal(pairedFlag);247248    // Vault history loads in the background — the Vault sub-view picks it up249    // when the user navigates in. Loading it lazily keeps Settings boot fast.250    void loadVaultHistory();251252    if ((licenseRes as { ok: boolean }).ok) {253      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);254    }255256    if ((syncRes as { ok: boolean }).ok) {257      const data = syncRes as { ok: boolean; data?: {258        pairedDevices?: PairedDevice[];259        trustedNetworks?: TrustedNetwork[];260        status?: SyncStatus;261        lastSyncAt?: number;262        isPro?: boolean;263      }};264      if (data.data) {265        setPairedDevices(data.data.pairedDevices ?? []);266        setTrustedNetworks(data.data.trustedNetworks ?? []);267        setSyncStatus(data.data.status ?? "offline");268        setLastSyncAt(data.data.lastSyncAt);269        setSyncProStatus(data.data.isPro ?? false);270      }271    }272273    if ((passkeyRes as { ok: boolean }).ok) {274      setPasskeyState(275        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null276      );277    }278279    if ((duressRes as { ok: boolean }).ok) {280      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);281    }282283    if ((settingsRes as { ok: boolean }).ok) {284      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;285      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);286    }287288    if ((keyRes as { ok: boolean }).ok) {289      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);290    }291292    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });293    if ((entriesRes as { ok: boolean }).ok) {294      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];295      setCanaryEntries(296        entries297          .filter((e) => e.canary?.webhookUrl)298          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))299      );300    }301302    chrome.runtime.onMessage.addListener((msg) => {303      if (msg.type === "SYNC_STATE_UPDATE") {304        setPairedDevices(msg.pairedDevices ?? []);305        setTrustedNetworks(msg.trustedNetworks ?? []);306        setSyncStatus(msg.status ?? "offline");307        setLastSyncAt(msg.lastSyncAt);308        setSyncProStatus(msg.isPro ?? false);309      }310      if (msg.type === "PAIRING_COMPLETED") {311        setHasPairedLocal(true);312      }313    });314315    if (isNativePlatform()) {316      const biometric = nativePlugin<{ isAvailable: () => Promise<{ available: boolean }> }>("BiometricPlugin");317      const keystore = nativePlugin<{ hasMasterKey: () => Promise<{ exists: boolean }> }>("KeystorePlugin");318      try {319        if (biometric) {320          const a = await biometric.isAvailable();321          setBiometricSupported(a.available);322        }323        if (keystore) {324          const k = await keystore.hasMasterKey();325          setBiometricStored(k.exists);326        }327      } catch { /* missing plugins — leave defaults */ }328    }329  });330331  createEffect(() => {332    if (activeSection() === "sync") {333      setShowSyncInfoSheet(!isPro() && !hasPaired());334    } else {335      setShowSyncInfoSheet(false);336    }337  });338339  async function handleEnableBiometrics() {340    setBiometricBusy(true);341    setBiometricError(null);342    try {343      const keyRes = (await sendMessage({ type: "GET_DERIVED_KEY" })) as {344        ok: boolean;345        data?: { masterKeyHex: string };346        error?: string;347      };348      if (!keyRes.ok || !keyRes.data?.masterKeyHex) {349        setBiometricError(keyRes.error === "vault_locked"350          ? "unlock the vault first"351          : "couldn't read derived key");352        return;353      }354      const keystore = nativePlugin<{ storeMasterKey: (p: { masterKeyHex: string }) => Promise<void> }>("KeystorePlugin");355      if (!keystore) {356        setBiometricError("biometric plugin unavailable");357        return;358      }359      await keystore.storeMasterKey({ masterKeyHex: keyRes.data.masterKeyHex });360      await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: true } });361      setBiometricStored(true);362    } catch (e) {363      setBiometricError(e instanceof Error ? e.message : "failed to enable biometrics");364    } finally {365      setBiometricBusy(false);366    }367  }368369  async function handleDisableBiometrics() {370    setBiometricBusy(true);371    setBiometricError(null);372    try {373      const keystore = nativePlugin<{ clearMasterKey: () => Promise<void> }>("KeystorePlugin");374      if (keystore) await keystore.clearMasterKey();375      await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: false } });376      setBiometricStored(false);377    } catch (e) {378      setBiometricError(e instanceof Error ? e.message : "failed to disable biometrics");379    } finally {380      setBiometricBusy(false);381    }382  }383384  async function handleChangePassword() {385    setChangePwError(null);386    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }387    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }388    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }389    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }390    setChangePwSaving(true);391    const res = await sendMessage({392      type: "CHANGE_PASSWORD",393      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },394    }) as { ok: boolean; error?: string };395    if (res.ok) {396      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");397      setShowChangePw(false);398      setShowNewRecoveryWords(true);399    } else {400      setChangePwError(res.error ?? "failed to change password");401    }402    setChangePwSaving(false);403  }404405  const [copied, setCopied] = createSignal(false);406407  async function copyLicenseKey(key: string) {408    await navigator.clipboard.writeText(key);409    setCopied(true);410    setTimeout(() => setCopied(false), 2000);411  }412413  async function handleActivateLicense() {414    const key = licenseKeyInput().trim();415    if (!key) return;416    setActivatingLicense(true);417    setLicenseError(null);418    setLicenseSuccess(null);419    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });420    setActivatingLicense(false);421    if ((res as { ok: boolean }).ok) {422      const data = (res as { ok: boolean; data?: LicenseInfo }).data;423      if (data?.valid) {424        setLicenseInfo(data);425        setLicenseKey(key);426        setLicenseSuccess("License activated!");427        setLicenseKeyInput("");428      } else {429        setLicenseError("Invalid license key");430      }431    } else {432      setLicenseError("Activation failed");433    }434  }435436  async function handleTogglePasskeys() {437    const current = passkeyState();438    if (!current || !current.supported) return;439    setPasskeySaving(true);440    const res = await sendMessage({441      type: "SET_PASSKEY_PROVIDER_ENABLED",442      payload: { enabled: !current.enabled },443    });444    setPasskeySaving(false);445    if ((res as { ok: boolean }).ok) {446      setPasskeyState(447        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null448      );449    }450  }451452  async function handleCheckAllBreaches() {453    setCheckingBreaches(true);454    setBreachResult(null);455    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });456    setCheckingBreaches(false);457    if ((res as { ok: boolean }).ok) {458      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};459      const breached = Object.values(data).filter((count) => count > 0).length;460      setBreachResult(461        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"462      );463      props.onBreachCheckDone();464    }465  }466467  async function handleSyncNow() {468    if (syncStatus() === "not_trusted") return;469    setSyncingNow(true);470    setSyncFeedback(null);471    const res = await sendMessage({ type: "TRIGGER_SYNC" });472    const ok = (res as { ok: boolean }).ok;473    if (ok) {474      const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;475      const count = data?.syncedCount ?? 0;476      setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });477      setLastSyncAt(Date.now());478      void refreshSyncState();479    } else {480      const error = (res as { error?: string }).error;481      const map: Record<string, string> = {482        no_paired_devices: "No paired devices",483        no_devices_reachable: "No devices found",484        vault_locked: "Vault is locked",485        pro_required: "Sync requires Pro",486      };487      setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });488    }489    setTimeout(() => {490      setSyncingNow(false);491      setSyncFeedback(null);492    }, 3500);493  }494495  async function refreshSyncState() {496    const res = await sendMessage({ type: "GET_SYNC_STATE" });497    const data = (res as { ok: boolean; data?: {498      pairedDevices?: PairedDevice[];499      trustedNetworks?: TrustedNetwork[];500      status?: SyncStatus;501      lastSyncAt?: number;502    }}).data;503    if (!data) return;504    setPairedDevices(data.pairedDevices ?? []);505    setTrustedNetworks(data.trustedNetworks ?? []);506    setSyncStatus(data.status ?? "offline");507    setLastSyncAt(data.lastSyncAt);508  }509510  async function handleRemoveDevice(deviceId: string) {511    setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));512    await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });513    void refreshSyncState();514  }515516  async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {517    await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });518    void refreshSyncState();519  }520521  const [markingNetwork, setMarkingNetwork] = createSignal(false);522523  async function handleMarkNetworkTrusted() {524    if (markingNetwork()) return;525    setMarkingNetwork(true);526    await sendMessage({ type: "MARK_NETWORK_TRUSTED" });527    await refreshSyncState();528    setMarkingNetwork(false);529  }530531  const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);532  const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);533534  async function handleResetSyncIdentity() {535    setResetIdentityRunning(true);536    const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });537    setResetIdentityRunning(false);538    if ((res as { ok: boolean }).ok) {539      setPairedDevices([]);540      setResetIdentityConfirm(false);541    }542  }543544  function formatRelativeTime(timestamp?: number): string {545    if (!timestamp) return "Never";546    const diff = Date.now() - timestamp;547    const minutes = Math.floor(diff / 60000);548    const hours = Math.floor(diff / 3600000);549    const days = Math.floor(diff / 86400000);550    if (minutes < 1) return "Just now";551    if (minutes < 60) return `${minutes}m ago`;552    if (hours < 24) return `${hours}h ago`;553    return `${days}d ago`;554  }555556  function getStatusColor(status: SyncStatus): string {557    switch (status) {558      case "synced": return "var(--peach-sync-ok)";559      case "syncing": return "var(--peach-sync-busy)";560      case "no_peers": return "var(--peach-sync-off)";561      case "not_trusted": return "var(--peach-sync-err)";562      case "offline": return "var(--peach-sync-off)";563      default: return "var(--peach-sync-off)";564    }565  }566567  function getStatusLabel(status: SyncStatus): string {568    switch (status) {569      case "synced": return "Synced";570      case "syncing": return "Syncing";571      case "no_peers": return "No peers visible";572      case "not_trusted": return "Not on a trusted network";573      case "offline": return "Offline";574      default: return "Unknown";575    }576  }577578  function isCurrentNetworkTrusted(): boolean {579    return trustedNetworks().length > 0;580  }581582  async function handleSetupDuress() {583    const mainPw = duressMainPassword().trim();584    const duressPw = duressPassword().trim();585    const confirmPw = duressConfirm().trim();586587    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }588    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }589    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }590    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }591592    setDuressSaving(true);593    setDuressError(null);594595    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };596    if (!verifyRes.ok || !verifyRes.data) {597      setDuressError("incorrect main password");598      setDuressSaving(false);599      return;600    }601602    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };603    setDuressSaving(false);604    if (res.ok) {605      setDuressEnabled(true);606      setShowDuressSetup(false);607      setDuressPassword("");608      setDuressConfirm("");609      setDuressMainPassword("");610      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");611      setTimeout(() => setDuressSuccess(null), 5000);612    } else {613      setDuressError(res.error ?? "failed to set duress password");614    }615  }616617  async function handleClearDeclinedCards() {618    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });619    setDeclinedCardCount(0);620  }621622  async function handleCountDuplicates() {623    setDedupRunning(true);624    setDedupDone(null);625    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };626    setDedupRunning(false);627    if (res.ok) setDedupCount(res.data ?? 0);628  }629630  async function handleDeduplicate() {631    setDedupRunning(true);632    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };633    setDedupRunning(false);634    if (res.ok) {635      setDedupDone(res.data ?? 0);636      setDedupCount(0);637    }638  }639640  async function handleDisableDuress() {641    const mainPw = duressMainPassword().trim();642    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }643644    setDuressSaving(true);645    setDuressError(null);646647    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };648    if (!verifyRes.ok || !verifyRes.data) {649      setDuressError("incorrect main password");650      setDuressSaving(false);651      return;652    }653654    const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };655    setDuressSaving(false);656    if (res.ok) {657      setDuressEnabled(false);658      setShowDuressSetup(false);659      setDuressMainPassword("");660      setDuressSuccess("Duress password disabled.");661      setTimeout(() => setDuressSuccess(null), 4000);662    } else {663      setDuressError(res.error ?? "failed to disable duress password");664    }665  }666667  async function handleSubmitFeedback() {668    if (!fbMessage().trim() || fbSubmitting()) return;669    setFbSubmitting(true);670    setFbResult(null);671    const res = await sendMessage({672      type: "SUBMIT_FEEDBACK",673      payload: {674        category: fbCategory(),675        message: fbMessage().trim(),676        version,677        browser: navigator.userAgent,678      },679    }) as { ok: boolean; error?: string };680    setFbSubmitting(false);681    if (res.ok) {682      setFbResult("success");683      setFbMessage("");684      setTimeout(() => setFbResult(null), 4000);685    } else {686      setFbResult("error");687    }688  }689690  function handleHeaderBack() {691    if (activeSection() !== null) {692      setActiveSection(null);693    } else {694      props.onBack();695    }696  }697698  function currentTitle(): string {699    const s = activeSection();700    if (s === null) return "settings";701    if (s === "license") return "account";702    return s;703  }704705  function renderCatRow(opts: {706    section: SectionName;707    name: string;708    desc: string;709    icon: () => any;710    pro?: boolean;711  }) {712    return (713      <button714        class="peach-settings-cat-row"715        onClick={() => setActiveSection(opts.section)}716      >717        <div class="peach-settings-cat-icon">{opts.icon()}</div>718        <div class="peach-settings-cat-content">719          <span class="peach-settings-cat-name">720            {opts.name}721            <Show when={opts.pro && !isPro()}>722              <span class="peach-pro-mark" aria-label="Pro feature">✦</span>723            </Show>724          </span>725          <span class="peach-settings-cat-desc">{opts.desc}</span>726        </div>727        <svg728          class="peach-settings-cat-chevron"729          width="6" height="10" viewBox="0 0 6 10" fill="none"730          stroke="currentColor" stroke-width="1.1"731          stroke-linecap="round" stroke-linejoin="round"732        >733          <path d="M1 1l4 4-4 4" />734        </svg>735      </button>736    );737  }738739  const iconShield = () => (740    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">741      <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>742    </svg>743  );744  const iconKey = () => (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      <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>747    </svg>748  );749  const iconLock = () => (750    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">751      <rect x="3" y="7" width="10" height="7" rx="1.5"/>752      <path d="M6 7V5a3 3 0 016 0v2"/>753    </svg>754  );755  const iconSync = () => (756    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">757      <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>758      <path d="M14 6l-2 2-2-2"/>759      <path d="M2 10l2-2 2 2"/>760    </svg>761  );762  const iconSun = () => (763    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">764      <circle cx="8" cy="8" r="3"/>765      <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"/>766    </svg>767  );768  const iconKeys = () => (769    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">770      <rect x="2" y="5" width="5" height="4" rx="1"/>771      <rect x="9" y="7" width="5" height="4" rx="1"/>772      <path d="M7 7h2m-2 2h2"/>773    </svg>774  );775  const iconBubble = () => (776    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">777      <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>778    </svg>779  );780781  function renderHome() {782    return (783      <div class="peach-settings-body">784        <p class="peach-settings-nav-label">PROTECTION</p>785        {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}786        {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}787788        <p class="peach-settings-nav-label">VAULT</p>789        {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}790        {renderCatRow({ section: "sync", name: "Sync", desc: "Pair devices · sync is free", icon: iconSync })}791792        <p class="peach-settings-nav-label">PREFERENCES</p>793        {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}794        {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}795796        <p class="peach-settings-nav-label">SUPPORT</p>797        {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}798799        <div class="peach-settings-footer">800          <span>Peach {version}</span>801          <span class="peach-divider"></span>802          <span>Browser extension</span>803        </div>804      </div>805    );806  }807808  function renderSecurity() {809    return (810      <div class="peach-settings-body">811        <div class="peach-settings-group">812          <p class="peach-settings-sub-label">LOCKING</p>813          <div class="peach-settings-row">814            <span>Lock on close</span>815            <button816              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}817              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}818              aria-label="Toggle lock on close"819            >820              <span class="peach-toggle-knob"></span>821            </button>822          </div>823          <div class="peach-settings-row">824            <span>Auto-lock after</span>825            <select826              class="peach-settings-select"827              value={String(props.autoLockMinutes)}828              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}829            >830              <option value="1">1 minute</option>831              <option value="5">5 minutes</option>832              <option value="15">15 minutes</option>833              <option value="30">30 minutes</option>834              <option value="0">Never</option>835            </select>836          </div>837838          <Show when={isNativePlatform() && biometricSupported()}>839            <p class="peach-settings-sub-label">BIOMETRICS</p>840            <div class="peach-settings-row">841              <span>Unlock with biometrics</span>842              <button843                class={`peach-toggle ${biometricStored() ? "is-on" : ""}`}844                onClick={() => void (biometricStored() ? handleDisableBiometrics() : handleEnableBiometrics())}845                disabled={biometricBusy()}846                aria-label="Toggle biometric unlock"847              >848                <span class="peach-toggle-knob"></span>849              </button>850            </div>851            <Show when={biometricError()}>852              <div class="peach-settings-row peach-settings-row--meta">853                <span class="peach-hint peach-hint-error">{biometricError()}</span>854              </div>855            </Show>856          </Show>857858          <p class="peach-settings-sub-label">BREACH PROTECTION</p>859          <div class="peach-settings-row">860            <span>Breach monitoring</span>861            <button862              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}863              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}864              aria-label="Toggle breach monitoring"865            >866              <span class="peach-toggle-knob"></span>867            </button>868          </div>869          <div class="peach-settings-row peach-settings-row--meta">870            <span>Check all passwords</span>871            <button872              class="peach-btn peach-btn--subtle peach-btn--sm"873              onClick={() => void handleCheckAllBreaches()}874              disabled={checkingBreaches()}875            >876              {checkingBreaches() ? "checking..." : "run"}877            </button>878          </div>879          <Show when={breachResult()}>880            <div class="peach-settings-row peach-settings-row--meta">881              <span>Last check</span>882              <span class="peach-settings-value">{breachResult()}</span>883            </div>884          </Show>885886          <p class="peach-settings-sub-label">MASTER PASSWORD</p>887          <div class="peach-settings-row peach-settings-row--meta">888            <span>Master password</span>889            <button890              class="peach-btn peach-btn--subtle peach-btn--sm"891              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}892            >893              {showChangePw() ? "cancel" : "change"}894            </button>895          </div>896          <Show when={showChangePw()}>897            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">898              <input899                class="peach-settings-input"900                type="password"901                placeholder="current password"902                value={changePwCurrent()}903                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}904                autocomplete="current-password"905              />906              <input907                class="peach-settings-input"908                type="password"909                placeholder="new password (min 12 chars)"910                value={changePwNew()}911                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}912                autocomplete="new-password"913              />914              <input915                class="peach-settings-input"916                type="password"917                placeholder="confirm new password"918                value={changePwConfirm()}919                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}920                autocomplete="new-password"921                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}922              />923              <Show when={changePwError()}>924                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>925              </Show>926              <Show when={changePwSuccess()}>927                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>928              </Show>929              <button930                class="peach-btn peach-btn--accent peach-btn--sm"931                style="align-self:flex-start"932                onClick={() => void handleChangePassword()}933                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}934              >935                {changePwSaving() ? "saving…" : "save new password"}936              </button>937            </div>938          </Show>939940          <p class="peach-settings-sub-label">DURESS PASSWORD</p>941          <div class="peach-settings-row peach-settings-row--top">942            <div class="peach-settings-row-main">943              <span>Duress password</span>944              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">945                If entered under coercion, shows a decoy empty vault instead of your real one.946              </span>947            </div>948            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>949              {duressEnabled() ? "on" : "off"}950            </div>951          </div>952953          <Show when={duressEnabled() && !showDuressSetup()}>954            <div class="peach-settings-row peach-settings-row--meta">955              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">956                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.957              </span>958            </div>959          </Show>960961          <Show when={duressSuccess()}>962            <div class="peach-settings-row peach-settings-row--meta">963              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>964            </div>965          </Show>966967          <Show when={!showDuressSetup()}>968            <div class="peach-settings-row peach-settings-row--meta">969              <button970                class="peach-btn peach-btn--subtle peach-btn--sm"971                onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}972              >973                {duressEnabled() ? "Change / disable" : "Set up"}974              </button>975            </div>976          </Show>977978          <Show when={showDuressSetup()}>979            <div class="peach-duress-form">980              <label class="peach-label">main password (to confirm)</label>981              <input982                class="peach-settings-input"983                type="password"984                placeholder="your current master password"985                autocomplete="current-password"986                value={duressMainPassword()}987                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}988              />989              <Show when={!duressEnabled()}>990                <label class="peach-label">new duress password</label>991                <input992                  class="peach-settings-input"993                  type="password"994                  placeholder="duress password (min 8 chars)"995                  autocomplete="new-password"996                  value={duressPassword()}997                  onInput={(e) => setDuressPassword(e.currentTarget.value)}998                />999                <label class="peach-label">confirm duress password</label>1000                <input1001                  class="peach-settings-input"1002                  type="password"1003                  placeholder="confirm duress password"1004                  autocomplete="new-password"1005                  value={duressConfirm()}1006                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}1007                />1008              </Show>10091010              <Show when={duressError()}>1011                <p class="peach-hint peach-hint-error">{duressError()}</p>1012              </Show>10131014              <div class="peach-duress-actions">1015                <button1016                  class="peach-btn peach-btn--subtle peach-btn--sm"1017                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}1018                >1019                  cancel1020                </button>1021                <Show when={duressEnabled()}>1022                  <button1023                    class="peach-btn peach-btn--ghost peach-btn--sm"1024                    style="color:var(--peach-breach)"1025                    onClick={() => void handleDisableDuress()}1026                    disabled={duressSaving()}1027                  >1028                    {duressSaving() ? "disabling..." : "disable duress"}1029                  </button>1030                </Show>1031                <Show when={!duressEnabled()}>1032                  <button1033                    class="peach-btn peach-btn--accent peach-btn--sm"1034                    onClick={() => void handleSetupDuress()}1035                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}1036                  >1037                    {duressSaving() ? "saving..." : "save"}1038                  </button>1039                </Show>1040              </div>1041            </div>1042          </Show>10431044          <p class="peach-settings-sub-label">CANARY ENTRIES</p>1045          <div class="peach-settings-row peach-settings-row--top">1046            <div class="peach-settings-row-main">1047              <span>Canary entries</span>1048              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1049                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.1050              </span>1051            </div>1052            <span class="peach-settings-value" style="flex-shrink:0">1053              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}1054            </span>1055          </div>1056          <Show when={canaryEntries().length > 0}>1057            <For each={canaryEntries()}>1058              {(entry) => (1059                <div class="peach-settings-row peach-settings-row--meta">1060                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>1061                  <button1062                    class="peach-btn peach-btn--subtle peach-btn--sm"1063                    onClick={() => void handleTestCanary(entry)}1064                    disabled={canaryTestingId() === entry.id}1065                    style="flex-shrink:0"1066                  >1067                    {canaryTestingId() === entry.id1068                      ? "sending…"1069                      : canaryTestResult()?.id === entry.id1070                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1071                        : "test"}1072                  </button>1073                </div>1074              )}1075            </For>1076          </Show>10771078          <p class="peach-settings-sub-label">PASSKEYS</p>1079          <div class="peach-settings-row peach-settings-row--top">1080            <div class="peach-settings-row-main">1081              <span>Intercept passkey requests</span>1082              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1083                Peach can handle passkey sign-in and creation on supported Chromium browsers.1084              </span>1085            </div>1086            <button1087              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1088              onClick={() => void handleTogglePasskeys()}1089              aria-label="Toggle passkey interception"1090              disabled={!passkeyState()?.supported || passkeySaving()}1091            >1092              <span class="peach-toggle-knob"></span>1093            </button>1094          </div>1095          <div class="peach-settings-row peach-settings-row--meta">1096            <span>Status</span>1097            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1098          </div>1099          <div class="peach-settings-row peach-settings-row--meta">1100            <span>Browser</span>1101            <span class="peach-settings-value">1102              {passkeyState()?.browserLabel ?? "Unknown"}1103              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1104            </span>1105          </div>1106          <div class="peach-info-notice" style="margin-top:8px">1107            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1108          </div>1109          <Show when={passkeyState()?.conflict}>1110            <div class="peach-settings-row peach-settings-row--meta">1111              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1112            </div>1113          </Show>1114        </div>1115      </div>1116    );1117  }11181119  function renderVault() {1120    return (1121      <div class="peach-settings-body">1122        <div class="peach-settings-group">1123          <p class="peach-settings-sub-label">ENTRIES</p>1124          <div class="peach-settings-row peach-settings-row--meta">1125            <span>Entries</span>1126            <span class="peach-settings-value">{props.entriesCount}</span>1127          </div>1128          <div class="peach-settings-row peach-settings-row--meta">1129            <span>Duplicates</span>1130            <Show when={dedupDone() !== null} fallback={1131              <button1132                class="peach-btn peach-btn--subtle peach-btn--sm"1133                onClick={() => void handleCountDuplicates()}1134                disabled={dedupRunning()}1135              >1136                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1137              </button>1138            }>1139              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1140                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1141              </span>1142            </Show>1143          </div>1144          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1145            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1146              <div class="peach-restore-warning">1147                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1148              </div>1149              <div style="display:flex;gap:6px">1150                <button1151                  class="peach-btn peach-btn--subtle peach-btn--sm"1152                  onClick={() => setDedupCount(null)}1153                >1154                  cancel1155                </button>1156                <button1157                  class="peach-btn peach-btn--accent peach-btn--sm"1158                  onClick={() => void handleDeduplicate()}1159                  disabled={dedupRunning()}1160                >1161                  {dedupRunning() ? "removing..." : "remove duplicates"}1162                </button>1163              </div>1164            </div>1165          </Show>11661167          <p class="peach-settings-sub-label">MAINTENANCE</p>1168          <div class="peach-settings-row peach-settings-row--meta">1169            <span>Trash auto-delete</span>1170            <select1171              class="peach-settings-select"1172              value={String(props.trashRetentionDays)}1173              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1174            >1175              <option value="7">7 days</option>1176              <option value="30">30 days</option>1177              <option value="90">90 days</option>1178              <option value="0">Never</option>1179            </select>1180          </div>1181          <div class="peach-settings-row peach-settings-row--meta">1182            <span>Vault Health</span>1183            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1184              open1185            </button>1186          </div>1187          <Show when={declinedCardCount() > 0}>1188            <div class="peach-settings-row peach-settings-row--meta">1189              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1190              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1191                Clear1192              </button>1193            </div>1194          </Show>11951196          <p class="peach-settings-sub-label">DATA</p>1197          <div class="peach-settings-row peach-settings-row--meta">1198            <span>Import passwords</span>1199            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1200              open1201            </button>1202          </div>1203          <div class="peach-settings-row peach-settings-row--meta">1204            <span>Export backup</span>1205            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1206              open1207            </button>1208          </div>12091210          <p class="peach-settings-sub-label">VAULT HISTORY</p>1211          <Show when={historyLoading()}>1212            <p class="peach-settings-hint">Loading…</p>1213          </Show>1214          <Show when={!historyLoading() && historySnapshots().length === 0}>1215            <p class="peach-settings-hint">1216              No snapshots yet. Peach saves automatically before every change —1217              saves, syncs, and imports.1218            </p>1219          </Show>1220          <Show when={!historyLoading() && historySnapshots().length > 0}>1221            <p class="peach-settings-hint">1222              Restoring locks your vault. Unlock with the password that1223              encrypted that snapshot to confirm.1224            </p>1225            <div class="peach-history-list">1226              <For each={historySnapshots()}>1227                {(snap) => (1228                  <div class="peach-history-row">1229                    <div class="peach-history-meta">1230                      <span class="peach-history-time">1231                        {formatSnapshotTime(snap.savedAt)}1232                      </span>1233                      <span class="peach-history-sub">1234                        {[1235                          snap.entryCount !== null1236                            ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1237                            : null,1238                          reasonLabel(snap.reason),1239                        ]1240                          .filter(Boolean)1241                          .join(" · ")}1242                      </span>1243                    </div>1244                    <button1245                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1246                        historyRestoring() === snap.id ? " is-busy" : ""1247                      }`}1248                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1249                      disabled={historyRestoring() !== null}1250                    >1251                      <Show when={historyRestoring() === snap.id} fallback="Restore">1252                        …1253                      </Show>1254                    </button>1255                  </div>1256                )}1257              </For>1258            </div>1259            <p class="peach-history-footer">1260              {historySnapshots().length} of 10 snapshots1261            </p>1262          </Show>12631264          <button1265            class="peach-settings-advanced-toggle"1266            onClick={() => setShowVaultAdvanced((v) => !v)}1267            aria-expanded={showVaultAdvanced()}1268          >1269            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1270            <span class="peach-settings-advanced-chevron">1271              {showVaultAdvanced() ? "▲" : "▼"}1272            </span>1273          </button>12741275          <Show when={showVaultAdvanced()}>1276            <div class="peach-danger-card">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">Replace vault from backup</h3>1280              </div>1281              <p class="peach-danger-card-copy">1282                Wipes the current vault and restores from a backup file.1283                Recoverable only if your backup is good.1284              </p>1285              <button1286                class="peach-danger-card-button"1287                onClick={() => props.onOpenFileImport?.()}1288              >1289                Choose backup file1290              </button>1291            </div>12921293            <Show when={props.onOpenPanic}>1294              <div class="peach-danger-card peach-danger-card--hazard">1295                <div class="peach-danger-card-head">1296                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1297                  <h3 class="peach-danger-card-title">Destroy vault</h3>1298                </div>1299                <p class="peach-danger-card-copy">1300                  Permanently wipes every entry, setting, and license on this1301                  device. There is no recovery.1302                </p>1303                <button1304                  class="peach-danger-card-button"1305                  onClick={() => props.onOpenPanic?.()}1306                >1307                  Begin destruction1308                </button>1309              </div>1310            </Show>1311          </Show>1312        </div>1313      </div>1314    );1315  }13161317  function renderSync() {1318    return (1319      <>1320        <Show when={showSyncInfoSheet()}>1321          <SyncInfoSheet1322            onGotIt={() => setShowSyncInfoSheet(false)}1323            onLearnPro={() => {1324              setShowSyncInfoSheet(false);1325              setActiveSection("license");1326            }}1327          />1328        </Show>13291330        <div class="peach-settings-body">1331        <div class="peach-settings-group">1332          <p class="peach-sync-network-note">1333            Sync works on your home and trusted networks. Some public or1334            corporate networks block device connections.1335          </p>13361337          {/* Add a device — FREE, always shown */}1338          <button1339            class="peach-settings-row peach-settings-row--tappable"1340            onClick={() => props.onOpenSyncPairing?.()}1341          >1342            <span>Add a device</span>1343            <svg width="6" height="10" viewBox="0 0 6 10" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1344              <path d="M1 1l4 4-4 4" />1345            </svg>1346          </button>13471348          {/* Paired devices list — shown when any exist */}1349          <Show when={pairedDevices().length > 0}>1350            <div class="peach-history-list">1351              <For each={pairedDevices()}>1352                {(device) => (1353                  <div class="peach-history-row">1354                    <div class="peach-history-meta">1355                      <span class="peach-history-time">{device.displayName}</span>1356                      <span class="peach-history-sub">1357                        {device.lastSyncAt1358                          ? `Last synced ${formatRelativeTime(device.lastSyncAt)}`1359                          : "Never synced"}1360                      </span>1361                    </div>1362                    <button1363                      class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1364                      onClick={() => void handleRemoveDevice(device.deviceId)}1365                    >1366                      Remove1367                    </button>1368                  </div>1369                )}1370              </For>1371            </div>1372          </Show>13731374          {/* Automatic sync — PRO only */}1375          <Show1376            when={isPro()}1377            fallback={1378              <Show when={hasPaired()}>1379                <div class="peach-sync-pro-hint">1380                  <p class="peach-settings-hint">1381                    Upgrade to Pro for automatic background sync across all your devices.1382                  </p>1383                  <button1384                    class="peach-btn peach-btn--subtle peach-btn--sm"1385                    onClick={() => setActiveSection("license")}1386                  >1387                    Upgrade to Pro1388                  </button>1389                </div>1390              </Show>1391            }1392          >1393            <div class="peach-settings-row">1394              <span>Status</span>1395              <span class="peach-settings-value peach-settings-value--status">1396                <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1397                {getStatusLabel(syncStatus())}1398              </span>1399            </div>1400            <div class="peach-settings-row peach-settings-row--meta">1401              <span>Last sync</span>1402              <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1403            </div>1404            <div class="peach-settings-row peach-settings-row--meta">1405              <button1406                class="peach-btn peach-btn--subtle peach-btn--sm"1407                onClick={() => void handleSyncNow()}1408                disabled={syncStatus() === "not_trusted" || syncingNow()}1409              >1410                <Switch>1411                  <Match when={syncingNow()}>Syncing…</Match>1412                  <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1413                  <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1414                  <Match when={true}>Sync now</Match>1415                </Switch>1416              </button>1417            </div>1418            <p class="peach-settings-hint">1419              If you edit on multiple devices before syncing, the most recently1420              changed device wins. Use Vault History to recover older versions.1421            </p>14221423            <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1424              <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1425              <For each={trustedNetworks()}>1426                {(network) => (1427                  <div class="peach-sync-device-row">1428                    <span class="peach-sync-device-name">1429                      {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1430                    </span>1431                    <Show1432                      when={!network.isTailscale}1433                      fallback={<span class="peach-sync-device-meta">Always trusted</span>}1434                    >1435                      <button1436                        class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1437                        onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1438                      >1439                        Remove1440                      </button>1441                    </Show>1442                  </div>1443                )}1444              </For>1445              <div class="peach-settings-actions--row">1446                <Show when={!isCurrentNetworkTrusted()}>1447                  <button1448                    class="peach-btn peach-btn--subtle peach-btn--sm"1449                    onClick={() => void handleMarkNetworkTrusted()}1450                    disabled={markingNetwork()}1451                  >1452                    {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1453                  </button>1454                </Show>1455              </div>1456            </div>14571458            <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1459              <span class="peach-sync-section-label">Advanced</span>1460              <Show1461                when={resetIdentityConfirm()}1462                fallback={1463                  <div class="peach-sync-advanced-row">1464                    <div class="peach-sync-advanced-info">1465                      <span class="peach-sync-advanced-name">Reset sync identity</span>1466                      <span class="peach-sync-device-meta">1467                        Generates a new keypair and unpairs every device.1468                      </span>1469                    </div>1470                    <button1471                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1472                      onClick={() => setResetIdentityConfirm(true)}1473                    >1474                      Reset1475                    </button>1476                  </div>1477                }1478              >1479                <div class="peach-sync-reset-confirm">1480                  <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1481                  <p class="peach-sync-reset-confirm-body">1482                    This will unpair all devices. You will need to pair them again from scratch.1483                  </p>1484                  <div class="peach-settings-actions--row">1485                    <button1486                      class="peach-btn peach-btn--ghost peach-btn--sm"1487                      onClick={() => setResetIdentityConfirm(false)}1488                      disabled={resetIdentityRunning()}1489                    >1490                      Cancel1491                    </button>1492                    <button1493                      class="peach-btn peach-btn--danger peach-btn--sm"1494                      onClick={() => void handleResetSyncIdentity()}1495                      disabled={resetIdentityRunning()}1496                    >1497                      {resetIdentityRunning() ? "Resetting..." : "Reset"}1498                    </button>1499                  </div>1500                </div>1501              </Show>1502            </div>1503          </Show>1504        </div>1505      </div>1506      </>1507    );1508  }15091510  function renderAppearance() {1511    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1512      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1513      { id: "cormorant", label: "Cormorant", preview: "Aa" },1514      { id: "inter", label: "Inter", preview: "Aa" },1515      { id: "system", label: "System", preview: "Aa" },1516    ];1517    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1518      { id: "small", label: "Small" },1519      { id: "medium", label: "Default" },1520      { id: "large", label: "Large" },1521    ];1522    return (1523      <div class="peach-settings-body">1524        <div class="peach-settings-group">1525          <p class="peach-settings-sub-label">THEME</p>1526          <div class="peach-settings-row peach-settings-row--meta">1527            <span>Theme</span>1528            <button1529              class="peach-btn peach-btn--subtle peach-btn--sm"1530              onClick={() =>1531                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1532              }1533            >1534              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1535            </button>1536          </div>15371538          <p class="peach-settings-sub-label">ACCENT</p>1539          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1540            <span>Accent</span>1541            <div class="peach-accent-picker">1542              <For each={ACCENT_COLORS}>1543                {(color) => (1544                  <button1545                    class="peach-accent-swatch"1546                    classList={{ "is-active": props.accentColor === color.name }}1547                    style={{ background: color.hex }}1548                    onClick={() => void props.onAccentChange(color.name)}1549                    aria-label={color.name}1550                    title={color.name}1551                  />1552                )}1553              </For>1554            </div>1555          </div>15561557          <p class="peach-settings-sub-label">DISPLAY FONT</p>1558          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1559            <span>Font</span>1560            <div class="peach-font-picker">1561              <For each={fontOptions}>1562                {(opt) => (1563                  <button1564                    class="peach-font-swatch"1565                    classList={{ "is-active": props.displayFont === opt.id }}1566                    data-font-preview={opt.id}1567                    onClick={() => void props.onDisplayFontChange(opt.id)}1568                    aria-label={opt.label}1569                    title={opt.label}1570                  >1571                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1572                    <span class="peach-font-swatch-name">{opt.label}</span>1573                  </button>1574                )}1575              </For>1576            </div>1577          </div>15781579          <p class="peach-settings-sub-label">TEXT SIZE</p>1580          <div class="peach-settings-row peach-settings-row--meta">1581            <span>Size</span>1582            <div class="peach-pill-group">1583              <For each={sizeOptions}>1584                {(opt) => (1585                  <button1586                    class="peach-btn peach-btn--ghost peach-btn--sm"1587                    classList={{ "is-active": props.textSize === opt.id }}1588                    onClick={() => void props.onTextSizeChange(opt.id)}1589                  >1590                    {opt.label}1591                  </button>1592                )}1593              </For>1594            </div>1595          </div>15961597          <p class="peach-settings-sub-label">MOTION</p>1598          <div class="peach-settings-row peach-settings-row--meta">1599            <span>Reduce motion</span>1600            <button1601              class="peach-btn peach-btn--subtle peach-btn--sm"1602              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1603            >1604              {props.reduceMotion ? "On" : "Off"}1605            </button>1606          </div>1607        </div>1608      </div>1609    );1610  }16111612  function renderShortcuts() {1613    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1614    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);16151616    function startCapture(action: ShortcutAction) {1617      setConflictMsg(null);1618      setCapturingAction(action);1619    }1620    function cancelCapture() {1621      setCapturingAction(null);1622      setConflictMsg(null);1623    }1624    function commitBinding(action: ShortcutAction, binding: Keybinding) {1625      const conflict = findConflict(props.shortcuts, binding, action);1626      if (conflict) {1627        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1628        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1629        return;1630      }1631      const next = { ...props.shortcuts, [action]: binding };1632      setCapturingAction(null);1633      setConflictMsg(null);1634      void props.onShortcutsChange(next);1635    }1636    function resetAction(action: ShortcutAction) {1637      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1638      void props.onShortcutsChange(next);1639    }1640    function resetAll() {1641      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1642    }16431644    // Capture-mode key handler — wired via ref onto the chip element so it1645    // catches the next non-modifier keypress and ignores other bindings.1646    function handleCaptureKey(e: KeyboardEvent) {1647      e.preventDefault();1648      e.stopPropagation();1649      if (e.key === "Escape") { cancelCapture(); return; }1650      const binding = keybindingFromEvent(e);1651      if (!binding) return; // bare modifier — keep waiting1652      const action = capturingAction();1653      if (action) commitBinding(action, binding);1654    }16551656    return (1657      <div class="peach-settings-body">1658        <div class="peach-settings-group peach-shortcuts-group">1659          <p class="peach-settings-sub-label">ENTRY VIEW</p>1660          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1661            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1662          </For>1663        </div>16641665        <div class="peach-settings-group peach-shortcuts-group">1666          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1667          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1668            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1669          </For>1670        </div>16711672        <Show when={conflictMsg()}>1673          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1674        </Show>16751676        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1677          <div class="peach-settings-row peach-settings-row--meta">1678            <span>Inline autofill on page</span>1679            <a1680              class="peach-settings-link"1681              href="chrome://extensions/shortcuts"1682              onClick={(e) => {1683                e.preventDefault();1684                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1685              }}1686            >1687              ⌘⇧P — customize ↗1688            </a>1689          </div>1690          <button1691            class="peach-btn peach-btn--subtle peach-btn--sm"1692            style="align-self:flex-start"1693            onClick={resetAll}1694          >1695            Reset all to defaults1696          </button>1697        </div>1698      </div>1699    );17001701    function renderRow(1702      desc: typeof SHORTCUT_DESCRIPTORS[number],1703      capturing: () => ShortcutAction | null,1704      onStart: (a: ShortcutAction) => void,1705      onCancel: () => void,1706      onReset: (a: ShortcutAction) => void,1707      onKey: (e: KeyboardEvent) => void,1708    ) {1709      const isActive = () => capturing() === desc.action;1710      const binding = () => props.shortcuts[desc.action];1711      const isCustom = () => {1712        const b = binding();1713        const d = DEFAULT_BINDINGS[desc.action];1714        return b.key !== d.key1715          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1716          || Boolean(b.meta)  !== Boolean(d.meta)1717          || Boolean(b.shift) !== Boolean(d.shift)1718          || Boolean(b.alt)   !== Boolean(d.alt);1719      };1720      return (1721        <div class="peach-shortcut-row">1722          <span class="peach-shortcut-row-label">{desc.label}</span>1723          <button1724            class="peach-shortcut-chip"1725            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1726            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1727            onKeyDown={(e) => { if (isActive()) onKey(e); }}1728            ref={(el) => {1729              // Auto-focus when entering capture mode so the keydown listener catches the next press.1730              if (el) {1731                queueMicrotask(() => { if (isActive()) el.focus(); });1732              }1733            }}1734          >1735            {isActive() ? "Press a key…" : formatKeybinding(binding())}1736          </button>1737          <Show when={isCustom() && !isActive()}>1738            <button1739              class="peach-shortcut-reset"1740              title="Reset to default"1741              onClick={() => onReset(desc.action)}1742            >1743              ↺1744            </button>1745          </Show>1746        </div>1747      );1748    }1749  }17501751  const PRO_FEATURES: Array<{1752    feature: ProFeature;1753    name: string;1754    desc: string;1755  }> = [1756    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1757    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1758    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1759    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1760    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1761  ];17621763  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17641765  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1766  const [deactivating, setDeactivating] = createSignal(false);17671768  async function handleDeactivateLicense() {1769    setDeactivating(true);1770    await sendMessage({ type: "DEACTIVATE_LICENSE" });1771    setLicenseInfo(null);1772    await props.onLicenseChange?.();1773    setDeactivating(false);1774  }17751776  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1777    setCanaryTestingId(entry.id);1778    setCanaryTestResult(null);1779    const res = await sendMessage({1780      type: "FIRE_CANARY_PING",1781      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1782    }) as { ok: boolean };1783    setCanaryTestResult({ id: entry.id, ok: res.ok });1784    setCanaryTestingId(null);1785    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1786  }17871788  function renderLicense() {1789    async function onActivate() {1790      await handleActivateLicense();1791      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1792        await props.onLicenseChange?.();1793      }1794    }17951796    return (1797      <div class="peach-settings-body">1798        <Show1799          when={isPro()}1800          fallback={1801            <Show1802              when={showKeyEntry()}1803              fallback={1804                <div class="peach-pro-upgrade">1805                  <div class="peach-pro-upgrade-hero">1806                    <div class="peach-pro-upgrade-mark-wrap">1807                      <PeachMark class="peach-pro-upgrade-mark" />1808                    </div>1809                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1810                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1811                  </div>18121813                  <div class="peach-pro-features">1814                    <For each={PRO_FEATURES}>1815                      {(item) => (1816                        <div1817                          class="peach-pro-feature"1818                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1819                        >1820                          <span class="peach-pro-feature-mark">✦</span>1821                          <div class="peach-pro-feature-text">1822                            <span class="peach-pro-feature-name">{item.name}</span>1823                            <span class="peach-pro-feature-desc">{item.desc}</span>1824                          </div>1825                        </div>1826                      )}1827                    </For>1828                  </div>18291830                  <div class="peach-pro-upgrade-actions">1831                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1832                    <a1833                      href="https://peachpasswords.com/#pricing"1834                      target="_blank"1835                      rel="noopener noreferrer"1836                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1837                    >1838                      Get Peach Pro1839                    </a>1840                    <div class="peach-pro-key-divider"><span>or</span></div>1841                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1842                      Activate a license key1843                    </button>1844                  </div>1845                </div>1846              }1847            >1848              <div class="peach-pro-key-ceremony">1849                <LockRuneCanvas />1850                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1851                <input1852                  class="peach-underline-input peach-pro-key-input"1853                  type="text"1854                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1855                  value={licenseKeyInput()}1856                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1857                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1858                  autocomplete="off"1859                  spellcheck={false}1860                />1861                <Show when={licenseError()}>1862                  <p class="peach-pro-key-error">That key doesn't look right.</p>1863                </Show>1864                <button1865                  class="peach-btn peach-btn--accent peach-pro-key-activate"1866                  onClick={() => void onActivate()}1867                  disabled={activatingLicense() || !licenseKeyInput().trim()}1868                >1869                  {activatingLicense() ? "activating…" : "Activate"}1870                </button>1871                <button1872                  class="peach-pro-key-cancel"1873                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1874                >1875                  cancel1876                </button>1877              </div>1878            </Show>1879          }1880        >1881          <div class="peach-pro-active">1882            <div class="peach-pro-active-hero">1883              <div class="peach-pro-active-mark-wrap">1884                <PeachMark class="peach-pro-active-mark" />1885              </div>1886              <h2 class="peach-pro-active-title">Peach Pro</h2>1887              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1888            </div>18891890            <Show when={licenseKey()}>1891              <div class="peach-settings-license">1892                <span class="peach-settings-label">LICENSE KEY</span>1893                <div class="peach-settings-license-row">1894                  <span class="peach-settings-license-key">{licenseKey()!}</span>1895                  <button1896                    class="peach-settings-license-copy"1897                    onClick={() => copyLicenseKey(licenseKey()!)}1898                  >1899                    {copied() ? "✓" : "copy"}1900                  </button>1901                </div>1902              </div>1903            </Show>19041905            <div class="peach-pro-active-features">1906              <For each={PRO_FEATURES}>1907                {(item) => (1908                  <div class="peach-pro-active-feature">1909                    <span class="peach-pro-active-feature-mark">✦</span>1910                    <span class="peach-pro-active-feature-name">{item.name}</span>1911                  </div>1912                )}1913              </For>1914            </div>19151916            <button1917              class="peach-pro-deactivate"1918              onClick={() => void handleDeactivateLicense()}1919              disabled={deactivating()}1920            >1921              {deactivating() ? "Deactivating…" : "Deactivate"}1922            </button>1923          </div>1924        </Show>1925      </div>1926    );1927  }19281929  function renderFeedback() {1930    return (1931      <div class="peach-settings-body">1932        <div class="peach-settings-group peach-feedback-group">1933          <p class="peach-feedback-intro">1934            Help us improve Peach. Every report goes directly to our inbox.1935          </p>19361937          <p class="peach-settings-sub-label">TYPE</p>1938          <div class="peach-feedback-chips">1939            <For each={FEEDBACK_CATEGORIES}>1940              {(cat) => (1941                <button1942                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1943                  onClick={() => setFbCategory(cat)}1944                >1945                  {cat}1946                </button>1947              )}1948            </For>1949          </div>19501951          <p class="peach-settings-sub-label">MESSAGE</p>1952          <textarea1953            class="peach-feedback-textarea"1954            placeholder="Describe the issue or idea..."1955            value={fbMessage()}1956            onInput={(e) => setFbMessage(e.currentTarget.value)}1957          />19581959          <p class="peach-feedback-meta">1960            Version {version} · will be included automatically1961          </p>19621963          <Show when={fbResult() === "success"}>1964            <p class="peach-feedback-result peach-feedback-result--ok">1965              Sent — thank you1966            </p>1967          </Show>1968          <Show when={fbResult() === "error"}>1969            <p class="peach-feedback-result peach-feedback-result--err">1970              Failed to send — try again1971            </p>1972          </Show>19731974          <button1975            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1976            onClick={() => void handleSubmitFeedback()}1977            disabled={fbSubmitting() || !fbMessage().trim()}1978          >1979            {fbSubmitting() ? "Sending..." : "Send feedback"}1980          </button>19811982          <p class="peach-feedback-anon">1983            Sent anonymously · no account required1984          </p>1985        </div>1986      </div>1987    );1988  }19891990  return (1991    <Show1992      when={showNewRecoveryWords()}1993      fallback={1994        <div class="peach-overlay-view ov-enter-right" style="display:flex">1995          <div class="peach-overlay-header">1996            <button1997              class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1998              onClick={handleHeaderBack}1999              aria-label="Back"2000            >2001              <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">2002                <path d="M6 1L1 6L6 11" />2003              </svg>2004            </button>2005            <span class="peach-overlay-title">{currentTitle()}</span>2006            <div class="peach-overlay-spacer"></div>2007          </div>20082009          <Switch fallback={renderHome()}>2010            <Match when={activeSection() === "security"}>{renderSecurity()}</Match>2011            <Match when={activeSection() === "vault"}>{renderVault()}</Match>2012            <Match when={activeSection() === "sync"}>{renderSync()}</Match>2013            <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2014            <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2015            <Match when={activeSection() === "license"}>{renderLicense()}</Match>2016            <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2017          </Switch>2018        </div>2019      }2020    >2021      <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />2022    </Show>2023  );2024}