Settings.tsx

80.2 KB · raw

1import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2import { Capacitor, registerPlugin } from "@capacitor/core";3import { sendMessage } from "../App";4import { PeachMark } from "../components/PeachMark";5import { LockRuneCanvas } from "../components/LockRuneCanvas";6import { biometricAvailable } from "../../shared/biometric";7import type { Canary } from "../../shared/types";8import { ACCENT_COLORS } from "../../shared/types";9import type {10  AccentColorName,11  PasskeyProviderState,12  ProFeature,13  VaultSnapshot,14} from "../../shared/types";15import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types";16import {17  DEFAULT_BINDINGS,18  findConflict,19  formatKeybinding,20  keybindingFromEvent,21  SHORTCUT_DESCRIPTORS,22  type Keybinding,23  type ShortcutAction,24} from "../../shared/shortcuts";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"76  | "mobile";7778const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7980function formatActivatedSince(issued: string | undefined): string {81  if (!issued) return "Active";82  try {83    const d = new Date(issued);84    if (Number.isNaN(d.getTime())) return "Active";85    return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;86  } catch {87    return "Active";88  }89}9091function isSectionName(value: string): value is SectionName {92  return (93    value === "security" ||94    value === "vault" ||95    value === "sync" ||96    value === "appearance" ||97    value === "shortcuts" ||98    value === "license" ||99    value === "feedback" ||100    value === "mobile"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 [syncingNow, setSyncingNow] = createSignal(false);129  const [syncFeedback, setSyncFeedback] = createSignal<130    { kind: "ok" | "err"; text: string } | null131  >(null);132  const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);133  const [passkeySaving, setPasskeySaving] = createSignal(false);134135  const [dedupCount, setDedupCount] = createSignal<number | null>(null);136  const [dedupRunning, setDedupRunning] = createSignal(false);137  const [dedupDone, setDedupDone] = createSignal<number | null>(null);138139  const [declinedCardCount, setDeclinedCardCount] = createSignal(0);140141  // Vault history (peach_vault_history) — last 10 snapshots, snapshotted142  // BEFORE every real-mode vault write so the user can roll back if a sync143  // or import overwrites the wrong state.144  const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);145  const [historyLoading, setHistoryLoading] = createSignal(false);146  const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);147148  async function loadVaultHistory() {149    setHistoryLoading(true);150    try {151      const res = await sendMessage({ type: "GET_VAULT_HISTORY" });152      const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];153      setHistorySnapshots(list);154    } finally {155      setHistoryLoading(false);156    }157  }158159  async function handleRestoreVaultSnapshot(id: string) {160    if (historyRestoring()) return;161    setHistoryRestoring(id);162    const res = await sendMessage({163      type: "RESTORE_VAULT_SNAPSHOT",164      payload: { snapshotId: id },165    });166    setHistoryRestoring(null);167    if ((res as { ok: boolean }).ok) {168      // Vault is now locked — let the parent close Settings; App.tsx's169      // VAULT_LOCKED listener will route back to the lock screen.170      props.onBack();171    }172  }173174  function formatSnapshotTime(savedAt: number): string {175    const diff = Date.now() - savedAt;176    if (diff < 60_000) return "Just now";177    if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;178    const d = new Date(savedAt);179    const isToday = new Date().toDateString() === d.toDateString();180    const isYest =181      new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();182    const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });183    if (isToday) return `Today ${t}`;184    if (isYest) return `Yesterday ${t}`;185    return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;186  }187188  function reasonLabel(reason: VaultSnapshot["reason"]): string {189    return (190      {191        local_save: "saved",192        sync_received: "synced",193        import: "imported",194        restore: "restored",195      } as const196    )[reason] ?? reason;197  }198199  const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);200201  const [showChangePw, setShowChangePw] = createSignal(false);202  const [changePwCurrent, setChangePwCurrent] = createSignal("");203  const [changePwNew, setChangePwNew] = createSignal("");204  const [changePwConfirm, setChangePwConfirm] = createSignal("");205  const [changePwSaving, setChangePwSaving] = createSignal(false);206  const [changePwError, setChangePwError] = createSignal<string | null>(null);207  const [changePwSuccess, setChangePwSuccess] = createSignal(false);208209  const [duressEnabled, setDuressEnabled] = createSignal(false);210  const [showDuressSetup, setShowDuressSetup] = createSignal(false);211  const [duressPassword, setDuressPassword] = createSignal("");212  const [duressConfirm, setDuressConfirm] = createSignal("");213  const [duressMainPassword, setDuressMainPassword] = createSignal("");214  const [duressSaving, setDuressSaving] = createSignal(false);215  const [duressError, setDuressError] = createSignal<string | null>(null);216  const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);217218  const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);219220  // Mobile-only state221  function getAutofillPlugin() {222    try {223      return registerPlugin<{224        isEnabledAsAutofillService(): Promise<{ enabled: boolean }>;225        openAutofillSettings(): Promise<void>;226      }>("Autofill");227    } catch {228      return null;229    }230  }231  const AutofillPlugin = getAutofillPlugin();232  const [autofillEnabled, setAutofillEnabled] = createSignal(false);233  const [biometricEnabled, setBiometricEnabled] = createSignal(false);234  const isAndroid = Capacitor.getPlatform() === "android";235  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);236  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);237238  const [fbCategory, setFbCategory] = createSignal("Bug report");239  const [fbMessage, setFbMessage] = createSignal("");240  const [fbSubmitting, setFbSubmitting] = createSignal(false);241  const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);242243  onMount(async () => {244    const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([245      sendMessage({ type: "GET_LICENSE" }),246      sendMessage({ type: "GET_SYNC_STATE" }),247      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),248      sendMessage({ type: "IS_DURESS_ENABLED" }),249      sendMessage({ type: "GET_SETTINGS" }),250      sendMessage({ type: "GET_LICENSE_KEY" }),251    ]);252253    // Vault history loads in the background — the Vault sub-view picks it up254    // when the user navigates in. Loading it lazily keeps Settings boot fast.255    void loadVaultHistory();256257    if ((licenseRes as { ok: boolean }).ok) {258      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);259    }260261    if ((syncRes as { ok: boolean }).ok) {262      const data = syncRes as { ok: boolean; data?: {263        pairedDevices?: PairedDevice[];264        trustedNetworks?: TrustedNetwork[];265        status?: SyncStatus;266        lastSyncAt?: number;267        isPro?: boolean;268      }};269      if (data.data) {270        setPairedDevices(data.data.pairedDevices ?? []);271        setTrustedNetworks(data.data.trustedNetworks ?? []);272        setSyncStatus(data.data.status ?? "offline");273        setLastSyncAt(data.data.lastSyncAt);274        setSyncProStatus(data.data.isPro ?? false);275      }276    }277278    if ((passkeyRes as { ok: boolean }).ok) {279      setPasskeyState(280        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null281      );282    }283284    if ((duressRes as { ok: boolean }).ok) {285      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);286    }287288    if ((settingsRes as { ok: boolean }).ok) {289      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;290      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);291    }292293    if ((keyRes as { ok: boolean }).ok) {294      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);295    }296297    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });298    if ((entriesRes as { ok: boolean }).ok) {299      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];300      setCanaryEntries(301        entries302          .filter((e) => e.canary?.webhookUrl)303          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))304      );305    }306307    // Android-only: check autofill and biometric status308    if (isAndroid) {309      if (AutofillPlugin) {310        const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();311        setAutofillEnabled(enabled);312      }313      const bio = await biometricAvailable();314      setBiometricEnabled(bio);315    }316317    chrome.runtime.onMessage.addListener((msg) => {318      if (msg.type === "SYNC_STATE_UPDATE") {319        setPairedDevices(msg.pairedDevices ?? []);320        setTrustedNetworks(msg.trustedNetworks ?? []);321        setSyncStatus(msg.status ?? "offline");322        setLastSyncAt(msg.lastSyncAt);323        setSyncProStatus(msg.isPro ?? false);324      }325    });326  });327328  async function handleChangePassword() {329    setChangePwError(null);330    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }331    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }332    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }333    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }334    setChangePwSaving(true);335    const res = await sendMessage({336      type: "CHANGE_PASSWORD",337      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },338    }) as { ok: boolean; error?: string };339    if (res.ok) {340      setChangePwSuccess(true);341      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");342      setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);343    } else {344      setChangePwError(res.error ?? "failed to change password");345    }346    setChangePwSaving(false);347  }348349  const [copied, setCopied] = createSignal(false);350351  async function copyLicenseKey(key: string) {352    await navigator.clipboard.writeText(key);353    setCopied(true);354    setTimeout(() => setCopied(false), 2000);355  }356357  async function handleActivateLicense() {358    const key = licenseKeyInput().trim();359    if (!key) return;360    setActivatingLicense(true);361    setLicenseError(null);362    setLicenseSuccess(null);363    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });364    setActivatingLicense(false);365    if ((res as { ok: boolean }).ok) {366      const data = (res as { ok: boolean; data?: LicenseInfo }).data;367      if (data?.valid) {368        setLicenseInfo(data);369        setLicenseKey(key);370        setLicenseSuccess("License activated!");371        setLicenseKeyInput("");372      } else {373        setLicenseError("Invalid license key");374      }375    } else {376      setLicenseError("Activation failed");377    }378  }379380  async function handleTogglePasskeys() {381    const current = passkeyState();382    if (!current || !current.supported) return;383    setPasskeySaving(true);384    const res = await sendMessage({385      type: "SET_PASSKEY_PROVIDER_ENABLED",386      payload: { enabled: !current.enabled },387    });388    setPasskeySaving(false);389    if ((res as { ok: boolean }).ok) {390      setPasskeyState(391        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null392      );393    }394  }395396  async function handleCheckAllBreaches() {397    setCheckingBreaches(true);398    setBreachResult(null);399    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });400    setCheckingBreaches(false);401    if ((res as { ok: boolean }).ok) {402      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};403      const breached = Object.values(data).filter((count) => count > 0).length;404      setBreachResult(405        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"406      );407      props.onBreachCheckDone();408    }409  }410411  async function handleSyncNow() {412    if (syncStatus() === "not_trusted") return;413    setSyncingNow(true);414    setSyncFeedback(null);415    const res = await sendMessage({ type: "TRIGGER_SYNC" });416    const ok = (res as { ok: boolean }).ok;417    if (ok) {418      const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;419      const count = data?.syncedCount ?? 0;420      setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });421      setLastSyncAt(Date.now());422      void refreshSyncState();423    } else {424      const error = (res as { error?: string }).error;425      const map: Record<string, string> = {426        no_paired_devices: "No paired devices",427        no_devices_reachable: "No devices found",428        vault_locked: "Vault is locked",429        pro_required: "Sync requires Pro",430      };431      setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });432    }433    setTimeout(() => {434      setSyncingNow(false);435      setSyncFeedback(null);436    }, 3500);437  }438439  async function refreshSyncState() {440    const res = await sendMessage({ type: "GET_SYNC_STATE" });441    const data = (res as { ok: boolean; data?: {442      pairedDevices?: PairedDevice[];443      trustedNetworks?: TrustedNetwork[];444      status?: SyncStatus;445      lastSyncAt?: number;446    }}).data;447    if (!data) return;448    setPairedDevices(data.pairedDevices ?? []);449    setTrustedNetworks(data.trustedNetworks ?? []);450    setSyncStatus(data.status ?? "offline");451    setLastSyncAt(data.lastSyncAt);452  }453454  async function handleRemoveDevice(deviceId: string) {455    setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));456    await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });457    void refreshSyncState();458  }459460  async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {461    await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });462    void refreshSyncState();463  }464465  const [markingNetwork, setMarkingNetwork] = createSignal(false);466467  async function handleMarkNetworkTrusted() {468    if (markingNetwork()) return;469    setMarkingNetwork(true);470    await sendMessage({ type: "MARK_NETWORK_TRUSTED" });471    await refreshSyncState();472    setMarkingNetwork(false);473  }474475  const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);476  const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);477478  async function handleResetSyncIdentity() {479    setResetIdentityRunning(true);480    if (Capacitor.isNativePlatform()) {481      const { clearAndroidSyncIdentity } = await import("../../shared/android-sync-identity");482      await clearAndroidSyncIdentity();483    }484    const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });485    setResetIdentityRunning(false);486    if ((res as { ok: boolean }).ok) {487      setPairedDevices([]);488      setResetIdentityConfirm(false);489    }490  }491492  function formatRelativeTime(timestamp?: number): string {493    if (!timestamp) return "Never";494    const diff = Date.now() - timestamp;495    const minutes = Math.floor(diff / 60000);496    const hours = Math.floor(diff / 3600000);497    const days = Math.floor(diff / 86400000);498    if (minutes < 1) return "Just now";499    if (minutes < 60) return `${minutes}m ago`;500    if (hours < 24) return `${hours}h ago`;501    return `${days}d ago`;502  }503504  function getStatusColor(status: SyncStatus): string {505    switch (status) {506      case "synced": return "var(--peach-sync-ok)";507      case "syncing": return "var(--peach-sync-busy)";508      case "no_peers": return "var(--peach-sync-off)";509      case "not_trusted": return "var(--peach-sync-err)";510      case "offline": return "var(--peach-sync-off)";511      default: return "var(--peach-sync-off)";512    }513  }514515  function getStatusLabel(status: SyncStatus): string {516    switch (status) {517      case "synced": return "Synced";518      case "syncing": return "Syncing";519      case "no_peers": return "No peers visible";520      case "not_trusted": return "Not on a trusted network";521      case "offline": return "Offline";522      default: return "Unknown";523    }524  }525526  function isCurrentNetworkTrusted(): boolean {527    return trustedNetworks().length > 0;528  }529530  async function handleSetupDuress() {531    const mainPw = duressMainPassword().trim();532    const duressPw = duressPassword().trim();533    const confirmPw = duressConfirm().trim();534535    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }536    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }537    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }538    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }539540    setDuressSaving(true);541    setDuressError(null);542543    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };544    if (!verifyRes.ok || !verifyRes.data) {545      setDuressError("incorrect main password");546      setDuressSaving(false);547      return;548    }549550    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };551    setDuressSaving(false);552    if (res.ok) {553      setDuressEnabled(true);554      setShowDuressSetup(false);555      setDuressPassword("");556      setDuressConfirm("");557      setDuressMainPassword("");558      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");559      setTimeout(() => setDuressSuccess(null), 5000);560    } else {561      setDuressError(res.error ?? "failed to set duress password");562    }563  }564565  async function handleClearDeclinedCards() {566    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });567    setDeclinedCardCount(0);568  }569570  async function handleCountDuplicates() {571    setDedupRunning(true);572    setDedupDone(null);573    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };574    setDedupRunning(false);575    if (res.ok) setDedupCount(res.data ?? 0);576  }577578  async function handleDeduplicate() {579    setDedupRunning(true);580    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };581    setDedupRunning(false);582    if (res.ok) {583      setDedupDone(res.data ?? 0);584      setDedupCount(0);585    }586  }587588  async function handleDisableDuress() {589    const mainPw = duressMainPassword().trim();590    if (!mainPw) { setDuressError("enter your main password to confirm"); 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: "DISABLE_DURESS" }) as { ok: boolean; error?: string };603    setDuressSaving(false);604    if (res.ok) {605      setDuressEnabled(false);606      setShowDuressSetup(false);607      setDuressMainPassword("");608      setDuressSuccess("Duress password disabled.");609      setTimeout(() => setDuressSuccess(null), 4000);610    } else {611      setDuressError(res.error ?? "failed to disable duress password");612    }613  }614615  async function handleSubmitFeedback() {616    if (!fbMessage().trim() || fbSubmitting()) return;617    setFbSubmitting(true);618    setFbResult(null);619    const res = await sendMessage({620      type: "SUBMIT_FEEDBACK",621      payload: {622        category: fbCategory(),623        message: fbMessage().trim(),624        version,625        browser: navigator.userAgent,626      },627    }) as { ok: boolean; error?: string };628    setFbSubmitting(false);629    if (res.ok) {630      setFbResult("success");631      setFbMessage("");632      setTimeout(() => setFbResult(null), 4000);633    } else {634      setFbResult("error");635    }636  }637638  function handleHeaderBack() {639    if (activeSection() !== null) {640      setActiveSection(null);641    } else {642      props.onBack();643    }644  }645646  function currentTitle(): string {647    const s = activeSection();648    if (s === null) return "settings";649    if (s === "license") return "account";650    return s;651  }652653  function renderCatRow(opts: {654    section: SectionName;655    name: string;656    desc: string;657    icon: () => any;658    pro?: boolean;659  }) {660    return (661      <button662        class="peach-settings-cat-row"663        onClick={() => setActiveSection(opts.section)}664      >665        <div class="peach-settings-cat-icon">{opts.icon()}</div>666        <div class="peach-settings-cat-content">667          <span class="peach-settings-cat-name">668            {opts.name}669            <Show when={opts.pro && !isPro()}>670              <span class="peach-pro-mark" aria-label="Pro feature">✦</span>671            </Show>672          </span>673          <span class="peach-settings-cat-desc">{opts.desc}</span>674        </div>675        <svg676          class="peach-settings-cat-chevron"677          width="6" height="10" viewBox="0 0 6 10" fill="none"678          stroke="currentColor" stroke-width="1.1"679          stroke-linecap="round" stroke-linejoin="round"680        >681          <path d="M1 1l4 4-4 4" />682        </svg>683      </button>684    );685  }686687  const iconShield = () => (688    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">689      <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>690    </svg>691  );692  const iconKey = () => (693    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">694      <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>695    </svg>696  );697  const iconLock = () => (698    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">699      <rect x="3" y="7" width="10" height="7" rx="1.5"/>700      <path d="M6 7V5a3 3 0 016 0v2"/>701    </svg>702  );703  const iconSync = () => (704    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">705      <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>706      <path d="M14 6l-2 2-2-2"/>707      <path d="M2 10l2-2 2 2"/>708    </svg>709  );710  const iconSun = () => (711    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">712      <circle cx="8" cy="8" r="3"/>713      <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"/>714    </svg>715  );716  const iconKeys = () => (717    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">718      <rect x="2" y="5" width="5" height="4" rx="1"/>719      <rect x="9" y="7" width="5" height="4" rx="1"/>720      <path d="M7 7h2m-2 2h2"/>721    </svg>722  );723  const iconBubble = () => (724    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">725      <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>726    </svg>727  );728729  function renderHome() {730    return (731      <div class="peach-settings-body">732        <p class="peach-settings-nav-label">PROTECTION</p>733        {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}734        {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}735736        <p class="peach-settings-nav-label">VAULT</p>737        {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}738        {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}739740        <p class="peach-settings-nav-label">PREFERENCES</p>741        {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}742        {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}743        <Show when={isAndroid}>744          <p class="peach-settings-nav-label">MOBILE</p>745          {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}746        </Show>747748        <p class="peach-settings-nav-label">SUPPORT</p>749        {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}750751        <div class="peach-settings-footer">752          <span>Peach {version}</span>753          <span class="peach-divider"></span>754          <span>Browser extension</span>755        </div>756      </div>757    );758  }759760  function renderSecurity() {761    return (762      <div class="peach-settings-body">763        <div class="peach-settings-group">764          <p class="peach-settings-sub-label">LOCKING</p>765          <div class="peach-settings-row">766            <span>Lock on close</span>767            <button768              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}769              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}770              aria-label="Toggle lock on close"771            >772              <span class="peach-toggle-knob"></span>773            </button>774          </div>775          <div class="peach-settings-row">776            <span>Auto-lock after</span>777            <select778              class="peach-settings-select"779              value={String(props.autoLockMinutes)}780              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}781            >782              <option value="1">1 minute</option>783              <option value="5">5 minutes</option>784              <option value="15">15 minutes</option>785              <option value="30">30 minutes</option>786              <option value="0">Never</option>787            </select>788          </div>789790          <p class="peach-settings-sub-label">BREACH PROTECTION</p>791          <div class="peach-settings-row">792            <span>Breach monitoring</span>793            <button794              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}795              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}796              aria-label="Toggle breach monitoring"797            >798              <span class="peach-toggle-knob"></span>799            </button>800          </div>801          <div class="peach-settings-row peach-settings-row--meta">802            <span>Check all passwords</span>803            <button804              class="peach-btn peach-btn--subtle peach-btn--sm"805              onClick={() => void handleCheckAllBreaches()}806              disabled={checkingBreaches()}807            >808              {checkingBreaches() ? "checking..." : "run"}809            </button>810          </div>811          <Show when={breachResult()}>812            <div class="peach-settings-row peach-settings-row--meta">813              <span>Last check</span>814              <span class="peach-settings-value">{breachResult()}</span>815            </div>816          </Show>817818          <p class="peach-settings-sub-label">MASTER PASSWORD</p>819          <div class="peach-settings-row peach-settings-row--meta">820            <span>Master password</span>821            <button822              class="peach-btn peach-btn--subtle peach-btn--sm"823              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}824            >825              {showChangePw() ? "cancel" : "change"}826            </button>827          </div>828          <Show when={showChangePw()}>829            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">830              <input831                class="peach-settings-input"832                type="password"833                placeholder="current password"834                value={changePwCurrent()}835                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}836                autocomplete="current-password"837              />838              <input839                class="peach-settings-input"840                type="password"841                placeholder="new password (min 12 chars)"842                value={changePwNew()}843                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}844                autocomplete="new-password"845              />846              <input847                class="peach-settings-input"848                type="password"849                placeholder="confirm new password"850                value={changePwConfirm()}851                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}852                autocomplete="new-password"853                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}854              />855              <Show when={changePwError()}>856                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>857              </Show>858              <Show when={changePwSuccess()}>859                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>860              </Show>861              <button862                class="peach-btn peach-btn--accent peach-btn--sm"863                style="align-self:flex-start"864                onClick={() => void handleChangePassword()}865                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}866              >867                {changePwSaving() ? "saving…" : "save new password"}868              </button>869            </div>870          </Show>871872          <p class="peach-settings-sub-label">DURESS PASSWORD</p>873          <div class="peach-settings-row peach-settings-row--top">874            <div class="peach-settings-row-main">875              <span>Duress password</span>876              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">877                If entered under coercion, shows a decoy empty vault instead of your real one.878              </span>879            </div>880            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>881              {duressEnabled() ? "on" : "off"}882            </div>883          </div>884885          <Show when={duressEnabled() && !showDuressSetup()}>886            <div class="peach-settings-row peach-settings-row--meta">887              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">888                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.889              </span>890            </div>891          </Show>892893          <Show when={duressSuccess()}>894            <div class="peach-settings-row peach-settings-row--meta">895              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>896            </div>897          </Show>898899          <Show when={!showDuressSetup()}>900            <div class="peach-settings-row peach-settings-row--meta">901              <button902                class="peach-btn peach-btn--subtle peach-btn--sm"903                onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}904              >905                {duressEnabled() ? "Change / disable" : "Set up"}906              </button>907            </div>908          </Show>909910          <Show when={showDuressSetup()}>911            <div class="peach-duress-form">912              <label class="peach-label">main password (to confirm)</label>913              <input914                class="peach-settings-input"915                type="password"916                placeholder="your current master password"917                autocomplete="current-password"918                value={duressMainPassword()}919                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}920              />921              <Show when={!duressEnabled()}>922                <label class="peach-label">new duress password</label>923                <input924                  class="peach-settings-input"925                  type="password"926                  placeholder="duress password (min 8 chars)"927                  autocomplete="new-password"928                  value={duressPassword()}929                  onInput={(e) => setDuressPassword(e.currentTarget.value)}930                />931                <label class="peach-label">confirm duress password</label>932                <input933                  class="peach-settings-input"934                  type="password"935                  placeholder="confirm duress password"936                  autocomplete="new-password"937                  value={duressConfirm()}938                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}939                />940              </Show>941942              <Show when={duressError()}>943                <p class="peach-hint peach-hint-error">{duressError()}</p>944              </Show>945946              <div class="peach-duress-actions">947                <button948                  class="peach-btn peach-btn--subtle peach-btn--sm"949                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}950                >951                  cancel952                </button>953                <Show when={duressEnabled()}>954                  <button955                    class="peach-btn peach-btn--ghost peach-btn--sm"956                    style="color:var(--peach-breach)"957                    onClick={() => void handleDisableDuress()}958                    disabled={duressSaving()}959                  >960                    {duressSaving() ? "disabling..." : "disable duress"}961                  </button>962                </Show>963                <Show when={!duressEnabled()}>964                  <button965                    class="peach-btn peach-btn--accent peach-btn--sm"966                    onClick={() => void handleSetupDuress()}967                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}968                  >969                    {duressSaving() ? "saving..." : "save"}970                  </button>971                </Show>972              </div>973            </div>974          </Show>975976          <p class="peach-settings-sub-label">CANARY ENTRIES</p>977          <div class="peach-settings-row peach-settings-row--top">978            <div class="peach-settings-row-main">979              <span>Canary entries</span>980              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">981                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.982              </span>983            </div>984            <span class="peach-settings-value" style="flex-shrink:0">985              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}986            </span>987          </div>988          <Show when={canaryEntries().length > 0}>989            <For each={canaryEntries()}>990              {(entry) => (991                <div class="peach-settings-row peach-settings-row--meta">992                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>993                  <button994                    class="peach-btn peach-btn--subtle peach-btn--sm"995                    onClick={() => void handleTestCanary(entry)}996                    disabled={canaryTestingId() === entry.id}997                    style="flex-shrink:0"998                  >999                    {canaryTestingId() === entry.id1000                      ? "sending…"1001                      : canaryTestResult()?.id === entry.id1002                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1003                        : "test"}1004                  </button>1005                </div>1006              )}1007            </For>1008          </Show>10091010          <p class="peach-settings-sub-label">PASSKEYS</p>1011          <div class="peach-settings-row peach-settings-row--top">1012            <div class="peach-settings-row-main">1013              <span>Intercept passkey requests</span>1014              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1015                Peach can handle passkey sign-in and creation on supported Chromium browsers.1016              </span>1017            </div>1018            <button1019              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1020              onClick={() => void handleTogglePasskeys()}1021              aria-label="Toggle passkey interception"1022              disabled={!passkeyState()?.supported || passkeySaving()}1023            >1024              <span class="peach-toggle-knob"></span>1025            </button>1026          </div>1027          <div class="peach-settings-row peach-settings-row--meta">1028            <span>Status</span>1029            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1030          </div>1031          <div class="peach-settings-row peach-settings-row--meta">1032            <span>Browser</span>1033            <span class="peach-settings-value">1034              {passkeyState()?.browserLabel ?? "Unknown"}1035              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1036            </span>1037          </div>1038          <div class="peach-info-notice" style="margin-top:8px">1039            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1040          </div>1041          <Show when={passkeyState()?.conflict}>1042            <div class="peach-settings-row peach-settings-row--meta">1043              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1044            </div>1045          </Show>1046        </div>1047      </div>1048    );1049  }10501051  function renderVault() {1052    return (1053      <div class="peach-settings-body">1054        <div class="peach-settings-group">1055          <p class="peach-settings-sub-label">ENTRIES</p>1056          <div class="peach-settings-row peach-settings-row--meta">1057            <span>Entries</span>1058            <span class="peach-settings-value">{props.entriesCount}</span>1059          </div>1060          <div class="peach-settings-row peach-settings-row--meta">1061            <span>Duplicates</span>1062            <Show when={dedupDone() !== null} fallback={1063              <button1064                class="peach-btn peach-btn--subtle peach-btn--sm"1065                onClick={() => void handleCountDuplicates()}1066                disabled={dedupRunning()}1067              >1068                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1069              </button>1070            }>1071              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1072                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1073              </span>1074            </Show>1075          </div>1076          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1077            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1078              <div class="peach-restore-warning">1079                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1080              </div>1081              <div style="display:flex;gap:6px">1082                <button1083                  class="peach-btn peach-btn--subtle peach-btn--sm"1084                  onClick={() => setDedupCount(null)}1085                >1086                  cancel1087                </button>1088                <button1089                  class="peach-btn peach-btn--accent peach-btn--sm"1090                  onClick={() => void handleDeduplicate()}1091                  disabled={dedupRunning()}1092                >1093                  {dedupRunning() ? "removing..." : "remove duplicates"}1094                </button>1095              </div>1096            </div>1097          </Show>10981099          <p class="peach-settings-sub-label">MAINTENANCE</p>1100          <div class="peach-settings-row peach-settings-row--meta">1101            <span>Trash auto-delete</span>1102            <select1103              class="peach-settings-select"1104              value={String(props.trashRetentionDays)}1105              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1106            >1107              <option value="7">7 days</option>1108              <option value="30">30 days</option>1109              <option value="90">90 days</option>1110              <option value="0">Never</option>1111            </select>1112          </div>1113          <div class="peach-settings-row peach-settings-row--meta">1114            <span>Vault Health</span>1115            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1116              open1117            </button>1118          </div>1119          <Show when={declinedCardCount() > 0}>1120            <div class="peach-settings-row peach-settings-row--meta">1121              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1122              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1123                Clear1124              </button>1125            </div>1126          </Show>11271128          <p class="peach-settings-sub-label">DATA</p>1129          <div class="peach-settings-row peach-settings-row--meta">1130            <span>Import passwords</span>1131            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1132              open1133            </button>1134          </div>1135          <div class="peach-settings-row peach-settings-row--meta">1136            <span>Export backup</span>1137            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1138              open1139            </button>1140          </div>11411142          <p class="peach-settings-sub-label">VAULT HISTORY</p>1143          <Show when={historyLoading()}>1144            <p class="peach-settings-hint">Loading…</p>1145          </Show>1146          <Show when={!historyLoading() && historySnapshots().length === 0}>1147            <p class="peach-settings-hint">1148              No snapshots yet. Peach saves automatically before every change —1149              saves, syncs, and imports.1150            </p>1151          </Show>1152          <Show when={!historyLoading() && historySnapshots().length > 0}>1153            <p class="peach-settings-hint">1154              Restoring locks your vault. Unlock with the password that1155              encrypted that snapshot to confirm.1156            </p>1157            <div class="peach-history-list">1158              <For each={historySnapshots()}>1159                {(snap) => (1160                  <div class="peach-history-row">1161                    <div class="peach-history-meta">1162                      <span class="peach-history-time">1163                        {formatSnapshotTime(snap.savedAt)}1164                      </span>1165                      <span class="peach-history-sub">1166                        {[1167                          snap.entryCount !== null1168                            ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1169                            : null,1170                          reasonLabel(snap.reason),1171                        ]1172                          .filter(Boolean)1173                          .join(" · ")}1174                      </span>1175                    </div>1176                    <button1177                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1178                        historyRestoring() === snap.id ? " is-busy" : ""1179                      }`}1180                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1181                      disabled={historyRestoring() !== null}1182                    >1183                      <Show when={historyRestoring() === snap.id} fallback="Restore">1184                        …1185                      </Show>1186                    </button>1187                  </div>1188                )}1189              </For>1190            </div>1191            <p class="peach-history-footer">1192              {historySnapshots().length} of 10 snapshots1193            </p>1194          </Show>11951196          <button1197            class="peach-settings-advanced-toggle"1198            onClick={() => setShowVaultAdvanced((v) => !v)}1199            aria-expanded={showVaultAdvanced()}1200          >1201            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1202            <span class="peach-settings-advanced-chevron">1203              {showVaultAdvanced() ? "▲" : "▼"}1204            </span>1205          </button>12061207          <Show when={showVaultAdvanced()}>1208            <div class="peach-danger-card">1209              <div class="peach-danger-card-head">1210                <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1211                <h3 class="peach-danger-card-title">Replace vault from backup</h3>1212              </div>1213              <p class="peach-danger-card-copy">1214                Wipes the current vault and restores from a backup file.1215                Recoverable only if your backup is good.1216              </p>1217              <button1218                class="peach-danger-card-button"1219                onClick={() => props.onOpenFileImport?.()}1220              >1221                Choose backup file1222              </button>1223            </div>12241225            <Show when={props.onOpenPanic}>1226              <div class="peach-danger-card peach-danger-card--hazard">1227                <div class="peach-danger-card-head">1228                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1229                  <h3 class="peach-danger-card-title">Destroy vault</h3>1230                </div>1231                <p class="peach-danger-card-copy">1232                  Permanently wipes every entry, setting, and license on this1233                  device. There is no recovery.1234                </p>1235                <button1236                  class="peach-danger-card-button"1237                  onClick={() => props.onOpenPanic?.()}1238                >1239                  Begin destruction1240                </button>1241              </div>1242            </Show>1243          </Show>1244        </div>1245      </div>1246    );1247  }12481249  function renderSync() {1250    if (!isPro()) {1251      return (1252        <div class="peach-settings-body">1253          <div class="peach-pro-upgrade">1254            <div class="peach-pro-upgrade-hero">1255              <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1256                <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1257                <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1258                <path d="M13.5 15.5C15 12 17 10 18 10C19 10 21 12 22.5 15.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.6"/>1259                <path d="M13.5 20.5C15 24 17 26 18 26C19 26 21 24 22.5 20.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.6"/>1260                <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1261                <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1262                <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1263              </svg>1264              <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1265              <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1266            </div>12671268            <div class="peach-pro-features">1269              <div class="peach-pro-feature">1270                <span class="peach-pro-feature-mark">✦</span>1271                <div class="peach-pro-feature-text">1272                  <span class="peach-pro-feature-name">Always in sync</span>1273                  <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1274                </div>1275              </div>1276              <div class="peach-pro-feature">1277                <span class="peach-pro-feature-mark">✦</span>1278                <div class="peach-pro-feature-text">1279                  <span class="peach-pro-feature-name">No cloud. No relay.</span>1280                  <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1281                </div>1282              </div>1283              <div class="peach-pro-feature">1284                <span class="peach-pro-feature-mark">✦</span>1285                <div class="peach-pro-feature-text">1286                  <span class="peach-pro-feature-name">Tailscale supported</span>1287                  <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1288                </div>1289              </div>1290              <div class="peach-pro-feature">1291                <span class="peach-pro-feature-mark">✦</span>1292                <div class="peach-pro-feature-text">1293                  <span class="peach-pro-feature-name">Zero friction after setup</span>1294                  <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1295                </div>1296              </div>1297            </div>12981299            <button1300              class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1301              onClick={() => setActiveSection("license")}1302            >1303              Upgrade to Pro1304            </button>1305            <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1306              Already have a key? Enter it →1307            </button>1308          </div>1309        </div>1310      );1311    }13121313    return (1314      <div class="peach-settings-body">1315        <div class="peach-settings-group">1316          <div class="peach-settings-row">1317            <span>Status</span>1318            <span class="peach-settings-value peach-settings-value--status">1319              <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1320              {getStatusLabel(syncStatus())}1321            </span>1322          </div>1323          <div class="peach-settings-row peach-settings-row--meta">1324            <span>Last sync</span>1325            <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1326          </div>1327          <div class="peach-settings-row peach-settings-row--meta">1328            <button1329              class="peach-btn peach-btn--subtle peach-btn--sm"1330              onClick={() => void handleSyncNow()}1331              disabled={syncStatus() === "not_trusted" || syncingNow()}1332            >1333              <Switch>1334                <Match when={syncingNow()}>Syncing…</Match>1335                <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1336                <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1337                <Match when={true}>Sync now</Match>1338              </Switch>1339            </button>1340          </div>1341          <p class="peach-settings-hint">1342            If you edit on multiple devices before syncing, the most recently1343            changed device wins. Use Vault History above to recover older1344            versions if needed.1345          </p>13461347          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1348            <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1349            <Show when={pairedDevices().length === 0}>1350              <p class="peach-settings-hint">1351                No paired devices yet. Add a device to start syncing.1352              </p>1353            </Show>1354            <Show when={pairedDevices().length > 0}>1355              <div class="peach-history-list">1356                <For each={pairedDevices()}>1357                  {(device) => (1358                    <div class="peach-history-row">1359                      <div class="peach-history-meta">1360                        <span class="peach-history-time">{device.displayName}</span>1361                        <span class="peach-history-sub">1362                          {device.lastSyncAt1363                            ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1364                            : "Never synced"}1365                        </span>1366                      </div>1367                      <button1368                        class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1369                        onClick={() => void handleRemoveDevice(device.deviceId)}1370                      >1371                        Remove1372                      </button>1373                    </div>1374                  )}1375                </For>1376              </div>1377            </Show>1378            <div class="peach-settings-actions--row">1379              <button1380                class="peach-btn peach-btn--subtle peach-btn--sm"1381                onClick={() => props.onOpenSyncPairing?.()}1382              >1383                Add a device1384              </button>1385            </div>1386          </div>13871388          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1389            <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1390            <For each={trustedNetworks()}>1391              {(network) => (1392                <div class="peach-sync-device-row">1393                  <span class="peach-sync-device-name">1394                    {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1395                  </span>1396                  <Show1397                    when={!network.isTailscale}1398                    fallback={<span class="peach-sync-device-meta">Always trusted</span>}1399                  >1400                    <button1401                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1402                      onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1403                    >1404                      Remove1405                    </button>1406                  </Show>1407                </div>1408              )}1409            </For>1410            <div class="peach-settings-actions--row">1411              <Show when={!isCurrentNetworkTrusted()}>1412                <button1413                  class="peach-btn peach-btn--subtle peach-btn--sm"1414                  onClick={() => void handleMarkNetworkTrusted()}1415                  disabled={markingNetwork()}1416                >1417                  {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1418                </button>1419              </Show>1420            </div>1421          </div>14221423          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1424            <span class="peach-sync-section-label">Advanced</span>1425            <Show1426              when={resetIdentityConfirm()}1427              fallback={1428                <div class="peach-sync-advanced-row">1429                  <div class="peach-sync-advanced-info">1430                    <span class="peach-sync-advanced-name">Reset sync identity</span>1431                    <span class="peach-sync-device-meta">1432                      Generates a new keypair and unpairs every device.1433                    </span>1434                  </div>1435                  <button1436                    class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1437                    onClick={() => setResetIdentityConfirm(true)}1438                  >1439                    Reset1440                  </button>1441                </div>1442              }1443            >1444              <div class="peach-sync-reset-confirm">1445                <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1446                <p class="peach-sync-reset-confirm-body">1447                  This will unpair all devices. You will need to pair them again from scratch.1448                </p>1449                <div class="peach-settings-actions--row">1450                  <button1451                    class="peach-btn peach-btn--ghost peach-btn--sm"1452                    onClick={() => setResetIdentityConfirm(false)}1453                    disabled={resetIdentityRunning()}1454                  >1455                    Cancel1456                  </button>1457                  <button1458                    class="peach-btn peach-btn--danger peach-btn--sm"1459                    onClick={() => void handleResetSyncIdentity()}1460                    disabled={resetIdentityRunning()}1461                  >1462                    {resetIdentityRunning() ? "Resetting..." : "Reset"}1463                  </button>1464                </div>1465              </div>1466            </Show>1467          </div>1468        </div>1469      </div>1470    );1471  }14721473  function renderAppearance() {1474    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1475      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1476      { id: "cormorant", label: "Cormorant", preview: "Aa" },1477      { id: "inter", label: "Inter", preview: "Aa" },1478      { id: "system", label: "System", preview: "Aa" },1479    ];1480    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1481      { id: "small", label: "Small" },1482      { id: "medium", label: "Default" },1483      { id: "large", label: "Large" },1484    ];1485    return (1486      <div class="peach-settings-body">1487        <div class="peach-settings-group">1488          <p class="peach-settings-sub-label">THEME</p>1489          <div class="peach-settings-row peach-settings-row--meta">1490            <span>Theme</span>1491            <button1492              class="peach-btn peach-btn--subtle peach-btn--sm"1493              onClick={() =>1494                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1495              }1496            >1497              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1498            </button>1499          </div>15001501          <p class="peach-settings-sub-label">ACCENT</p>1502          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1503            <span>Accent</span>1504            <div class="peach-accent-picker">1505              <For each={ACCENT_COLORS}>1506                {(color) => (1507                  <button1508                    class="peach-accent-swatch"1509                    classList={{ "is-active": props.accentColor === color.name }}1510                    style={{ background: color.hex }}1511                    onClick={() => void props.onAccentChange(color.name)}1512                    aria-label={color.name}1513                    title={color.name}1514                  />1515                )}1516              </For>1517            </div>1518          </div>15191520          <p class="peach-settings-sub-label">DISPLAY FONT</p>1521          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1522            <span>Font</span>1523            <div class="peach-font-picker">1524              <For each={fontOptions}>1525                {(opt) => (1526                  <button1527                    class="peach-font-swatch"1528                    classList={{ "is-active": props.displayFont === opt.id }}1529                    data-font-preview={opt.id}1530                    onClick={() => void props.onDisplayFontChange(opt.id)}1531                    aria-label={opt.label}1532                    title={opt.label}1533                  >1534                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1535                    <span class="peach-font-swatch-name">{opt.label}</span>1536                  </button>1537                )}1538              </For>1539            </div>1540          </div>15411542          <p class="peach-settings-sub-label">TEXT SIZE</p>1543          <div class="peach-settings-row peach-settings-row--meta">1544            <span>Size</span>1545            <div class="peach-pill-group">1546              <For each={sizeOptions}>1547                {(opt) => (1548                  <button1549                    class="peach-btn peach-btn--ghost peach-btn--sm"1550                    classList={{ "is-active": props.textSize === opt.id }}1551                    onClick={() => void props.onTextSizeChange(opt.id)}1552                  >1553                    {opt.label}1554                  </button>1555                )}1556              </For>1557            </div>1558          </div>15591560          <p class="peach-settings-sub-label">MOTION</p>1561          <div class="peach-settings-row peach-settings-row--meta">1562            <span>Reduce motion</span>1563            <button1564              class="peach-btn peach-btn--subtle peach-btn--sm"1565              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1566            >1567              {props.reduceMotion ? "On" : "Off"}1568            </button>1569          </div>1570        </div>1571      </div>1572    );1573  }15741575  function renderShortcuts() {1576    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1577    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15781579    function startCapture(action: ShortcutAction) {1580      setConflictMsg(null);1581      setCapturingAction(action);1582    }1583    function cancelCapture() {1584      setCapturingAction(null);1585      setConflictMsg(null);1586    }1587    function commitBinding(action: ShortcutAction, binding: Keybinding) {1588      const conflict = findConflict(props.shortcuts, binding, action);1589      if (conflict) {1590        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1591        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1592        return;1593      }1594      const next = { ...props.shortcuts, [action]: binding };1595      setCapturingAction(null);1596      setConflictMsg(null);1597      void props.onShortcutsChange(next);1598    }1599    function resetAction(action: ShortcutAction) {1600      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1601      void props.onShortcutsChange(next);1602    }1603    function resetAll() {1604      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1605    }16061607    // Capture-mode key handler — wired via ref onto the chip element so it1608    // catches the next non-modifier keypress and ignores other bindings.1609    function handleCaptureKey(e: KeyboardEvent) {1610      e.preventDefault();1611      e.stopPropagation();1612      if (e.key === "Escape") { cancelCapture(); return; }1613      const binding = keybindingFromEvent(e);1614      if (!binding) return; // bare modifier — keep waiting1615      const action = capturingAction();1616      if (action) commitBinding(action, binding);1617    }16181619    return (1620      <div class="peach-settings-body">1621        <div class="peach-settings-group peach-shortcuts-group">1622          <p class="peach-settings-sub-label">ENTRY VIEW</p>1623          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1624            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1625          </For>1626        </div>16271628        <div class="peach-settings-group peach-shortcuts-group">1629          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1630          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1631            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1632          </For>1633        </div>16341635        <Show when={conflictMsg()}>1636          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1637        </Show>16381639        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1640          <div class="peach-settings-row peach-settings-row--meta">1641            <span>Inline autofill on page</span>1642            <a1643              class="peach-settings-link"1644              href="chrome://extensions/shortcuts"1645              onClick={(e) => {1646                e.preventDefault();1647                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1648              }}1649            >1650              ⌘⇧P — customize ↗1651            </a>1652          </div>1653          <button1654            class="peach-btn peach-btn--subtle peach-btn--sm"1655            style="align-self:flex-start"1656            onClick={resetAll}1657          >1658            Reset all to defaults1659          </button>1660        </div>1661      </div>1662    );16631664    function renderRow(1665      desc: typeof SHORTCUT_DESCRIPTORS[number],1666      capturing: () => ShortcutAction | null,1667      onStart: (a: ShortcutAction) => void,1668      onCancel: () => void,1669      onReset: (a: ShortcutAction) => void,1670      onKey: (e: KeyboardEvent) => void,1671    ) {1672      const isActive = () => capturing() === desc.action;1673      const binding = () => props.shortcuts[desc.action];1674      const isCustom = () => {1675        const b = binding();1676        const d = DEFAULT_BINDINGS[desc.action];1677        return b.key !== d.key1678          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1679          || Boolean(b.meta)  !== Boolean(d.meta)1680          || Boolean(b.shift) !== Boolean(d.shift)1681          || Boolean(b.alt)   !== Boolean(d.alt);1682      };1683      return (1684        <div class="peach-shortcut-row">1685          <span class="peach-shortcut-row-label">{desc.label}</span>1686          <button1687            class="peach-shortcut-chip"1688            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1689            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1690            onKeyDown={(e) => { if (isActive()) onKey(e); }}1691            ref={(el) => {1692              // Auto-focus when entering capture mode so the keydown listener catches the next press.1693              if (el) {1694                queueMicrotask(() => { if (isActive()) el.focus(); });1695              }1696            }}1697          >1698            {isActive() ? "Press a key…" : formatKeybinding(binding())}1699          </button>1700          <Show when={isCustom() && !isActive()}>1701            <button1702              class="peach-shortcut-reset"1703              title="Reset to default"1704              onClick={() => onReset(desc.action)}1705            >1706              ↺1707            </button>1708          </Show>1709        </div>1710      );1711    }1712  }17131714  const PRO_FEATURES: Array<{1715    feature: ProFeature;1716    name: string;1717    desc: string;1718  }> = [1719    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1720    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1721    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1722    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1723    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1724  ];17251726  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17271728  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1729  const [deactivating, setDeactivating] = createSignal(false);17301731  async function handleDeactivateLicense() {1732    setDeactivating(true);1733    await sendMessage({ type: "DEACTIVATE_LICENSE" });1734    setLicenseInfo(null);1735    await props.onLicenseChange?.();1736    setDeactivating(false);1737  }17381739  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1740    setCanaryTestingId(entry.id);1741    setCanaryTestResult(null);1742    const res = await sendMessage({1743      type: "FIRE_CANARY_PING",1744      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1745    }) as { ok: boolean };1746    setCanaryTestResult({ id: entry.id, ok: res.ok });1747    setCanaryTestingId(null);1748    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1749  }17501751  function renderLicense() {1752    async function onActivate() {1753      await handleActivateLicense();1754      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1755        await props.onLicenseChange?.();1756      }1757    }17581759    return (1760      <div class="peach-settings-body">1761        <Show1762          when={isPro()}1763          fallback={1764            <Show1765              when={showKeyEntry()}1766              fallback={1767                <div class="peach-pro-upgrade">1768                  <div class="peach-pro-upgrade-hero">1769                    <div class="peach-pro-upgrade-mark-wrap">1770                      <PeachMark class="peach-pro-upgrade-mark" />1771                    </div>1772                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1773                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1774                  </div>17751776                  <div class="peach-pro-features">1777                    <For each={PRO_FEATURES}>1778                      {(item) => (1779                        <div1780                          class="peach-pro-feature"1781                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1782                        >1783                          <span class="peach-pro-feature-mark">✦</span>1784                          <div class="peach-pro-feature-text">1785                            <span class="peach-pro-feature-name">{item.name}</span>1786                            <span class="peach-pro-feature-desc">{item.desc}</span>1787                          </div>1788                        </div>1789                      )}1790                    </For>1791                  </div>17921793                  <div class="peach-pro-upgrade-actions">1794                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1795                    <a1796                      href="https://peachpasswords.com/#pricing"1797                      target="_blank"1798                      rel="noopener noreferrer"1799                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1800                    >1801                      Get Peach Pro1802                    </a>1803                    <div class="peach-pro-key-divider"><span>or</span></div>1804                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1805                      Activate a license key1806                    </button>1807                  </div>1808                </div>1809              }1810            >1811              <div class="peach-pro-key-ceremony">1812                <LockRuneCanvas />1813                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1814                <input1815                  class="peach-underline-input peach-pro-key-input"1816                  type="text"1817                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1818                  value={licenseKeyInput()}1819                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1820                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1821                  autocomplete="off"1822                  spellcheck={false}1823                />1824                <Show when={licenseError()}>1825                  <p class="peach-pro-key-error">That key doesn't look right.</p>1826                </Show>1827                <button1828                  class="peach-btn peach-btn--accent peach-pro-key-activate"1829                  onClick={() => void onActivate()}1830                  disabled={activatingLicense() || !licenseKeyInput().trim()}1831                >1832                  {activatingLicense() ? "activating…" : "Activate"}1833                </button>1834                <button1835                  class="peach-pro-key-cancel"1836                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1837                >1838                  cancel1839                </button>1840              </div>1841            </Show>1842          }1843        >1844          <div class="peach-pro-active">1845            <div class="peach-pro-active-hero">1846              <div class="peach-pro-active-mark-wrap">1847                <PeachMark class="peach-pro-active-mark" />1848              </div>1849              <h2 class="peach-pro-active-title">Peach Pro</h2>1850              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1851            </div>18521853            <Show when={licenseKey()}>1854              <div class="peach-settings-license">1855                <span class="peach-settings-label">LICENSE KEY</span>1856                <div class="peach-settings-license-row">1857                  <span class="peach-settings-license-key">{licenseKey()!}</span>1858                  <button1859                    class="peach-settings-license-copy"1860                    onClick={() => copyLicenseKey(licenseKey()!)}1861                  >1862                    {copied() ? "✓" : "copy"}1863                  </button>1864                </div>1865              </div>1866            </Show>18671868            <div class="peach-pro-active-features">1869              <For each={PRO_FEATURES}>1870                {(item) => (1871                  <div class="peach-pro-active-feature">1872                    <span class="peach-pro-active-feature-mark">✦</span>1873                    <span class="peach-pro-active-feature-name">{item.name}</span>1874                  </div>1875                )}1876              </For>1877            </div>18781879            <button1880              class="peach-pro-deactivate"1881              onClick={() => void handleDeactivateLicense()}1882              disabled={deactivating()}1883            >1884              {deactivating() ? "Deactivating…" : "Deactivate"}1885            </button>1886          </div>1887        </Show>1888      </div>1889    );1890  }18911892  function renderFeedback() {1893    return (1894      <div class="peach-settings-body">1895        <div class="peach-settings-group peach-feedback-group">1896          <p class="peach-feedback-intro">1897            Help us improve Peach. Every report goes directly to our inbox.1898          </p>18991900          <p class="peach-settings-sub-label">TYPE</p>1901          <div class="peach-feedback-chips">1902            <For each={FEEDBACK_CATEGORIES}>1903              {(cat) => (1904                <button1905                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1906                  onClick={() => setFbCategory(cat)}1907                >1908                  {cat}1909                </button>1910              )}1911            </For>1912          </div>19131914          <p class="peach-settings-sub-label">MESSAGE</p>1915          <textarea1916            class="peach-feedback-textarea"1917            placeholder="Describe the issue or idea..."1918            value={fbMessage()}1919            onInput={(e) => setFbMessage(e.currentTarget.value)}1920          />19211922          <p class="peach-feedback-meta">1923            Version {version} · will be included automatically1924          </p>19251926          <Show when={fbResult() === "success"}>1927            <p class="peach-feedback-result peach-feedback-result--ok">1928              Sent — thank you1929            </p>1930          </Show>1931          <Show when={fbResult() === "error"}>1932            <p class="peach-feedback-result peach-feedback-result--err">1933              Failed to send — try again1934            </p>1935          </Show>19361937          <button1938            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1939            onClick={() => void handleSubmitFeedback()}1940            disabled={fbSubmitting() || !fbMessage().trim()}1941          >1942            {fbSubmitting() ? "Sending..." : "Send feedback"}1943          </button>19441945          <p class="peach-feedback-anon">1946            Sent anonymously · no account required1947          </p>1948        </div>1949      </div>1950    );1951  }19521953  function renderMobile() {1954    return (1955      <div class="peach-settings-body">1956        <div class="peach-settings-group">1957          <p class="peach-settings-label">Autofill service</p>1958          <div class="peach-mobile-row">1959            <span class="peach-mobile-status">1960              {autofillEnabled() ? "Enabled" : "Disabled"}1961            </span>1962            <button1963              class="peach-btn peach-btn--subtle peach-btn--sm"1964              onClick={() => void AutofillPlugin?.openAutofillSettings()}1965            >1966              {autofillEnabled() ? "Change" : "Enable"}1967            </button>1968          </div>1969        </div>19701971        <div class="peach-settings-group">1972          <p class="peach-settings-label">Biometric unlock</p>1973          <div class="peach-mobile-row">1974            <span class="peach-mobile-status">1975              {biometricEnabled() ? "Available" : "Not available"}1976            </span>1977          </div>1978        </div>1979      </div>1980    );1981  }19821983  return (1984    <div class="peach-overlay-view ov-enter-right" style="display:flex">1985      <div class="peach-overlay-header">1986        <button1987          class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1988          onClick={handleHeaderBack}1989          aria-label="Back"1990        >1991          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1992            <path d="M6 1L1 6L6 11" />1993          </svg>1994        </button>1995        <span class="peach-overlay-title">{currentTitle()}</span>1996        <div class="peach-overlay-spacer"></div>1997      </div>19981999      <Switch fallback={renderHome()}>2000        <Match when={activeSection() === "security"}>{renderSecurity()}</Match>2001        <Match when={activeSection() === "vault"}>{renderVault()}</Match>2002        <Match when={activeSection() === "sync"}>{renderSync()}</Match>2003        <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2004        <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2005        <Match when={activeSection() === "license"}>{renderLicense()}</Match>2006        <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2007        <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>2008      </Switch>2009    </div>2010  );2011}