Settings.tsx

79.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 isMobile = Capacitor.isNativePlatform();236  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);237  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);238239  const [fbCategory, setFbCategory] = createSignal("Bug report");240  const [fbMessage, setFbMessage] = createSignal("");241  const [fbSubmitting, setFbSubmitting] = createSignal(false);242  const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);243244  onMount(async () => {245    const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([246      sendMessage({ type: "GET_LICENSE" }),247      sendMessage({ type: "GET_SYNC_STATE" }),248      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),249      sendMessage({ type: "IS_DURESS_ENABLED" }),250      sendMessage({ type: "GET_SETTINGS" }),251      sendMessage({ type: "GET_LICENSE_KEY" }),252    ]);253254    // Vault history loads in the background — the Vault sub-view picks it up255    // when the user navigates in. Loading it lazily keeps Settings boot fast.256    void loadVaultHistory();257258    if ((licenseRes as { ok: boolean }).ok) {259      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);260    }261262    if ((syncRes as { ok: boolean }).ok) {263      const data = syncRes as { ok: boolean; data?: {264        pairedDevices?: PairedDevice[];265        trustedNetworks?: TrustedNetwork[];266        status?: SyncStatus;267        lastSyncAt?: number;268        isPro?: boolean;269      }};270      if (data.data) {271        setPairedDevices(data.data.pairedDevices ?? []);272        setTrustedNetworks(data.data.trustedNetworks ?? []);273        setSyncStatus(data.data.status ?? "offline");274        setLastSyncAt(data.data.lastSyncAt);275        setSyncProStatus(data.data.isPro ?? false);276      }277    }278279    if ((passkeyRes as { ok: boolean }).ok) {280      setPasskeyState(281        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null282      );283    }284285    if ((duressRes as { ok: boolean }).ok) {286      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);287    }288289    if ((settingsRes as { ok: boolean }).ok) {290      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;291      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);292    }293294    if ((keyRes as { ok: boolean }).ok) {295      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);296    }297298    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });299    if ((entriesRes as { ok: boolean }).ok) {300      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];301      setCanaryEntries(302        entries303          .filter((e) => e.canary?.webhookUrl)304          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))305      );306    }307308    // Android-only: check autofill and biometric status309    if (isAndroid) {310      if (AutofillPlugin) {311        const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();312        setAutofillEnabled(enabled);313      }314      const bio = await biometricAvailable();315      setBiometricEnabled(bio);316    }317318    chrome.runtime.onMessage.addListener((msg) => {319      if (msg.type === "SYNC_STATE_UPDATE") {320        setPairedDevices(msg.pairedDevices ?? []);321        setTrustedNetworks(msg.trustedNetworks ?? []);322        setSyncStatus(msg.status ?? "offline");323        setLastSyncAt(msg.lastSyncAt);324        setSyncProStatus(msg.isPro ?? false);325      }326    });327  });328329  async function handleChangePassword() {330    setChangePwError(null);331    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }332    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }333    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }334    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }335    setChangePwSaving(true);336    const res = await sendMessage({337      type: "CHANGE_PASSWORD",338      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },339    }) as { ok: boolean; error?: string };340    if (res.ok) {341      setChangePwSuccess(true);342      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");343      setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);344    } else {345      setChangePwError(res.error ?? "failed to change password");346    }347    setChangePwSaving(false);348  }349350  const [copied, setCopied] = createSignal(false);351352  async function copyLicenseKey(key: string) {353    await navigator.clipboard.writeText(key);354    setCopied(true);355    setTimeout(() => setCopied(false), 2000);356  }357358  async function handleActivateLicense() {359    const key = licenseKeyInput().trim();360    if (!key) return;361    setActivatingLicense(true);362    setLicenseError(null);363    setLicenseSuccess(null);364    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });365    setActivatingLicense(false);366    if ((res as { ok: boolean }).ok) {367      const data = (res as { ok: boolean; data?: LicenseInfo }).data;368      if (data?.valid) {369        setLicenseInfo(data);370        setLicenseKey(key);371        setLicenseSuccess("License activated!");372        setLicenseKeyInput("");373      } else {374        setLicenseError("Invalid license key");375      }376    } else {377      setLicenseError("Activation failed");378    }379  }380381  async function handleTogglePasskeys() {382    const current = passkeyState();383    if (!current || !current.supported) return;384    setPasskeySaving(true);385    const res = await sendMessage({386      type: "SET_PASSKEY_PROVIDER_ENABLED",387      payload: { enabled: !current.enabled },388    });389    setPasskeySaving(false);390    if ((res as { ok: boolean }).ok) {391      setPasskeyState(392        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null393      );394    }395  }396397  async function handleCheckAllBreaches() {398    setCheckingBreaches(true);399    setBreachResult(null);400    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });401    setCheckingBreaches(false);402    if ((res as { ok: boolean }).ok) {403      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};404      const breached = Object.values(data).filter((count) => count > 0).length;405      setBreachResult(406        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"407      );408      props.onBreachCheckDone();409    }410  }411412  async function handleSyncNow() {413    if (syncStatus() === "not_trusted") return;414    setSyncingNow(true);415    setSyncFeedback(null);416    const res = await sendMessage({ type: "TRIGGER_SYNC" });417    const ok = (res as { ok: boolean }).ok;418    if (ok) {419      const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;420      const count = data?.syncedCount ?? 0;421      setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });422      setLastSyncAt(Date.now());423      void refreshSyncState();424    } else {425      const error = (res as { error?: string }).error;426      const map: Record<string, string> = {427        no_paired_devices: "No paired devices",428        no_devices_reachable: "No devices found",429        vault_locked: "Vault is locked",430        pro_required: "Sync requires Pro",431      };432      setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });433    }434    setTimeout(() => {435      setSyncingNow(false);436      setSyncFeedback(null);437    }, 3500);438  }439440  async function refreshSyncState() {441    const res = await sendMessage({ type: "GET_SYNC_STATE" });442    const data = (res as { ok: boolean; data?: {443      pairedDevices?: PairedDevice[];444      trustedNetworks?: TrustedNetwork[];445      status?: SyncStatus;446      lastSyncAt?: number;447    }}).data;448    if (!data) return;449    setPairedDevices(data.pairedDevices ?? []);450    setTrustedNetworks(data.trustedNetworks ?? []);451    setSyncStatus(data.status ?? "offline");452    setLastSyncAt(data.lastSyncAt);453  }454455  async function handleRemoveDevice(deviceId: string) {456    setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));457    await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });458    void refreshSyncState();459  }460461  async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {462    await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });463    void refreshSyncState();464  }465466  const [markingNetwork, setMarkingNetwork] = createSignal(false);467468  async function handleMarkNetworkTrusted() {469    if (markingNetwork()) return;470    setMarkingNetwork(true);471    await sendMessage({ type: "MARK_NETWORK_TRUSTED" });472    await refreshSyncState();473    setMarkingNetwork(false);474  }475476  const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);477  const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);478479  async function handleResetSyncIdentity() {480    setResetIdentityRunning(true);481    if (Capacitor.isNativePlatform()) {482      const { clearAndroidSyncIdentity } = await import("../../shared/android-sync-identity");483      await clearAndroidSyncIdentity();484    }485    const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });486    setResetIdentityRunning(false);487    if ((res as { ok: boolean }).ok) {488      setPairedDevices([]);489      setResetIdentityConfirm(false);490    }491  }492493  function formatRelativeTime(timestamp?: number): string {494    if (!timestamp) return "Never";495    const diff = Date.now() - timestamp;496    const minutes = Math.floor(diff / 60000);497    const hours = Math.floor(diff / 3600000);498    const days = Math.floor(diff / 86400000);499    if (minutes < 1) return "Just now";500    if (minutes < 60) return `${minutes}m ago`;501    if (hours < 24) return `${hours}h ago`;502    return `${days}d ago`;503  }504505  function getStatusColor(status: SyncStatus): string {506    switch (status) {507      case "synced": return "var(--peach-sync-ok)";508      case "syncing": return "var(--peach-sync-busy)";509      case "no_peers": return "var(--peach-sync-off)";510      case "not_trusted": return "var(--peach-sync-err)";511      case "offline": return "var(--peach-sync-off)";512      default: return "var(--peach-sync-off)";513    }514  }515516  function getStatusLabel(status: SyncStatus): string {517    switch (status) {518      case "synced": return "Synced";519      case "syncing": return "Syncing";520      case "no_peers": return "No peers visible";521      case "not_trusted": return "Not on a trusted network";522      case "offline": return "Offline";523      default: return "Unknown";524    }525  }526527  function isCurrentNetworkTrusted(): boolean {528    return trustedNetworks().length > 0;529  }530531  async function handleSetupDuress() {532    const mainPw = duressMainPassword().trim();533    const duressPw = duressPassword().trim();534    const confirmPw = duressConfirm().trim();535536    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }537    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }538    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }539    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }540541    setDuressSaving(true);542    setDuressError(null);543544    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };545    if (!verifyRes.ok || !verifyRes.data) {546      setDuressError("incorrect main password");547      setDuressSaving(false);548      return;549    }550551    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };552    setDuressSaving(false);553    if (res.ok) {554      setDuressEnabled(true);555      setShowDuressSetup(false);556      setDuressPassword("");557      setDuressConfirm("");558      setDuressMainPassword("");559      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");560      setTimeout(() => setDuressSuccess(null), 5000);561    } else {562      setDuressError(res.error ?? "failed to set duress password");563    }564  }565566  async function handleClearDeclinedCards() {567    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });568    setDeclinedCardCount(0);569  }570571  async function handleCountDuplicates() {572    setDedupRunning(true);573    setDedupDone(null);574    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };575    setDedupRunning(false);576    if (res.ok) setDedupCount(res.data ?? 0);577  }578579  async function handleDeduplicate() {580    setDedupRunning(true);581    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };582    setDedupRunning(false);583    if (res.ok) {584      setDedupDone(res.data ?? 0);585      setDedupCount(0);586    }587  }588589  async function handleDisableDuress() {590    const mainPw = duressMainPassword().trim();591    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }592593    setDuressSaving(true);594    setDuressError(null);595596    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };597    if (!verifyRes.ok || !verifyRes.data) {598      setDuressError("incorrect main password");599      setDuressSaving(false);600      return;601    }602603    const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };604    setDuressSaving(false);605    if (res.ok) {606      setDuressEnabled(false);607      setShowDuressSetup(false);608      setDuressMainPassword("");609      setDuressSuccess("Duress password disabled.");610      setTimeout(() => setDuressSuccess(null), 4000);611    } else {612      setDuressError(res.error ?? "failed to disable duress password");613    }614  }615616  async function handleSubmitFeedback() {617    if (!fbMessage().trim() || fbSubmitting()) return;618    setFbSubmitting(true);619    setFbResult(null);620    const res = await sendMessage({621      type: "SUBMIT_FEEDBACK",622      payload: {623        category: fbCategory(),624        message: fbMessage().trim(),625        version,626        browser: navigator.userAgent,627      },628    }) as { ok: boolean; error?: string };629    setFbSubmitting(false);630    if (res.ok) {631      setFbResult("success");632      setFbMessage("");633      setTimeout(() => setFbResult(null), 4000);634    } else {635      setFbResult("error");636    }637  }638639  function handleHeaderBack() {640    if (activeSection() !== null) {641      setActiveSection(null);642    } else {643      props.onBack();644    }645  }646647  function currentTitle(): string {648    const s = activeSection();649    if (s === null) return "settings";650    if (s === "license") return "account";651    return s;652  }653654  function renderCatRow(opts: {655    section: SectionName;656    name: string;657    desc: string;658    icon: () => any;659    pro?: boolean;660  }) {661    return (662      <button663        class="peach-settings-cat-row"664        onClick={() => setActiveSection(opts.section)}665      >666        <div class="peach-settings-cat-icon">{opts.icon()}</div>667        <div class="peach-settings-cat-content">668          <span class="peach-settings-cat-name">669            {opts.name}670            <Show when={opts.pro && !isPro()}>671              <span class="peach-pro-mark" aria-label="Pro feature">✦</span>672            </Show>673          </span>674          <span class="peach-settings-cat-desc">{opts.desc}</span>675        </div>676        <svg677          class="peach-settings-cat-chevron"678          width="6" height="10" viewBox="0 0 6 10" fill="none"679          stroke="currentColor" stroke-width="1.1"680          stroke-linecap="round" stroke-linejoin="round"681        >682          <path d="M1 1l4 4-4 4" />683        </svg>684      </button>685    );686  }687688  const iconShield = () => (689    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">690      <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>691    </svg>692  );693  const iconKey = () => (694    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">695      <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>696    </svg>697  );698  const iconLock = () => (699    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">700      <rect x="3" y="7" width="10" height="7" rx="1.5"/>701      <path d="M6 7V5a3 3 0 016 0v2"/>702    </svg>703  );704  const iconSync = () => (705    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">706      <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>707      <path d="M14 6l-2 2-2-2"/>708      <path d="M2 10l2-2 2 2"/>709    </svg>710  );711  const iconSun = () => (712    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">713      <circle cx="8" cy="8" r="3"/>714      <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"/>715    </svg>716  );717  const iconKeys = () => (718    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">719      <rect x="2" y="5" width="5" height="4" rx="1"/>720      <rect x="9" y="7" width="5" height="4" rx="1"/>721      <path d="M7 7h2m-2 2h2"/>722    </svg>723  );724  const iconBubble = () => (725    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">726      <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>727    </svg>728  );729730  function renderHome() {731    return (732      <div class="peach-settings-body">733        <p class="peach-settings-nav-label">PROTECTION</p>734        {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}735        {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}736737        <p class="peach-settings-nav-label">VAULT</p>738        {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}739        {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}740741        <p class="peach-settings-nav-label">PREFERENCES</p>742        {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}743        {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}744        <Show when={isAndroid}>745          <p class="peach-settings-nav-label">MOBILE</p>746          {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}747        </Show>748749        <p class="peach-settings-nav-label">SUPPORT</p>750        {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}751752        <div class="peach-settings-footer">753          <span>Peach {version}</span>754          <span class="peach-divider"></span>755          <span>Browser extension</span>756        </div>757      </div>758    );759  }760761  function renderSecurity() {762    return (763      <div class="peach-settings-body">764        <div class="peach-settings-group">765          <p class="peach-settings-sub-label">LOCKING</p>766          <div class="peach-settings-row">767            <span>Lock on close</span>768            <button769              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}770              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}771              aria-label="Toggle lock on close"772            >773              <span class="peach-toggle-knob"></span>774            </button>775          </div>776          <div class="peach-settings-row">777            <span>Auto-lock after</span>778            <select779              class="peach-settings-select"780              value={String(props.autoLockMinutes)}781              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}782            >783              <option value="1">1 minute</option>784              <option value="5">5 minutes</option>785              <option value="15">15 minutes</option>786              <option value="30">30 minutes</option>787              <option value="0">Never</option>788            </select>789          </div>790791          <p class="peach-settings-sub-label">BREACH PROTECTION</p>792          <div class="peach-settings-row">793            <span>Breach monitoring</span>794            <button795              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}796              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}797              aria-label="Toggle breach monitoring"798            >799              <span class="peach-toggle-knob"></span>800            </button>801          </div>802          <div class="peach-settings-row peach-settings-row--meta">803            <span>Check all passwords</span>804            <button805              class="peach-btn peach-btn--subtle peach-btn--sm"806              onClick={() => void handleCheckAllBreaches()}807              disabled={checkingBreaches()}808            >809              {checkingBreaches() ? "checking..." : "run"}810            </button>811          </div>812          <Show when={breachResult()}>813            <div class="peach-settings-row peach-settings-row--meta">814              <span>Last check</span>815              <span class="peach-settings-value">{breachResult()}</span>816            </div>817          </Show>818819          <p class="peach-settings-sub-label">MASTER PASSWORD</p>820          <div class="peach-settings-row peach-settings-row--meta">821            <span>Master password</span>822            <button823              class="peach-btn peach-btn--subtle peach-btn--sm"824              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}825            >826              {showChangePw() ? "cancel" : "change"}827            </button>828          </div>829          <Show when={showChangePw()}>830            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">831              <input832                class="peach-settings-input"833                type="password"834                placeholder="current password"835                value={changePwCurrent()}836                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}837                autocomplete="current-password"838              />839              <input840                class="peach-settings-input"841                type="password"842                placeholder="new password (min 12 chars)"843                value={changePwNew()}844                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}845                autocomplete="new-password"846              />847              <input848                class="peach-settings-input"849                type="password"850                placeholder="confirm new password"851                value={changePwConfirm()}852                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}853                autocomplete="new-password"854                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}855              />856              <Show when={changePwError()}>857                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>858              </Show>859              <Show when={changePwSuccess()}>860                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>861              </Show>862              <button863                class="peach-btn peach-btn--accent peach-btn--sm"864                style="align-self:flex-start"865                onClick={() => void handleChangePassword()}866                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}867              >868                {changePwSaving() ? "saving…" : "save new password"}869              </button>870            </div>871          </Show>872873          <p class="peach-settings-sub-label">DURESS PASSWORD</p>874          <div class="peach-settings-row peach-settings-row--top">875            <div class="peach-settings-row-main">876              <span>Duress password</span>877              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">878                If entered under coercion, shows a decoy empty vault instead of your real one.879              </span>880            </div>881            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>882              {duressEnabled() ? "on" : "off"}883            </div>884          </div>885886          <Show when={duressEnabled() && !showDuressSetup()}>887            <div class="peach-settings-row peach-settings-row--meta">888              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">889                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.890              </span>891            </div>892          </Show>893894          <Show when={duressSuccess()}>895            <div class="peach-settings-row peach-settings-row--meta">896              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>897            </div>898          </Show>899900          <Show when={!showDuressSetup()}>901            <div class="peach-settings-row peach-settings-row--meta">902              <button903                class="peach-btn peach-btn--subtle peach-btn--sm"904                onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}905              >906                {duressEnabled() ? "Change / disable" : "Set up"}907              </button>908            </div>909          </Show>910911          <Show when={showDuressSetup()}>912            <div class="peach-duress-form">913              <label class="peach-label">main password (to confirm)</label>914              <input915                class="peach-settings-input"916                type="password"917                placeholder="your current master password"918                autocomplete="current-password"919                value={duressMainPassword()}920                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}921              />922              <Show when={!duressEnabled()}>923                <label class="peach-label">new duress password</label>924                <input925                  class="peach-settings-input"926                  type="password"927                  placeholder="duress password (min 8 chars)"928                  autocomplete="new-password"929                  value={duressPassword()}930                  onInput={(e) => setDuressPassword(e.currentTarget.value)}931                />932                <label class="peach-label">confirm duress password</label>933                <input934                  class="peach-settings-input"935                  type="password"936                  placeholder="confirm duress password"937                  autocomplete="new-password"938                  value={duressConfirm()}939                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}940                />941              </Show>942943              <Show when={duressError()}>944                <p class="peach-hint peach-hint-error">{duressError()}</p>945              </Show>946947              <div class="peach-duress-actions">948                <button949                  class="peach-btn peach-btn--subtle peach-btn--sm"950                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}951                >952                  cancel953                </button>954                <Show when={duressEnabled()}>955                  <button956                    class="peach-btn peach-btn--ghost peach-btn--sm"957                    style="color:var(--peach-breach)"958                    onClick={() => void handleDisableDuress()}959                    disabled={duressSaving()}960                  >961                    {duressSaving() ? "disabling..." : "disable duress"}962                  </button>963                </Show>964                <Show when={!duressEnabled()}>965                  <button966                    class="peach-btn peach-btn--accent peach-btn--sm"967                    onClick={() => void handleSetupDuress()}968                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}969                  >970                    {duressSaving() ? "saving..." : "save"}971                  </button>972                </Show>973              </div>974            </div>975          </Show>976977          <p class="peach-settings-sub-label">CANARY ENTRIES</p>978          <div class="peach-settings-row peach-settings-row--top">979            <div class="peach-settings-row-main">980              <span>Canary entries</span>981              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">982                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.983              </span>984            </div>985            <span class="peach-settings-value" style="flex-shrink:0">986              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}987            </span>988          </div>989          <Show when={canaryEntries().length > 0}>990            <For each={canaryEntries()}>991              {(entry) => (992                <div class="peach-settings-row peach-settings-row--meta">993                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>994                  <button995                    class="peach-btn peach-btn--subtle peach-btn--sm"996                    onClick={() => void handleTestCanary(entry)}997                    disabled={canaryTestingId() === entry.id}998                    style="flex-shrink:0"999                  >1000                    {canaryTestingId() === entry.id1001                      ? "sending…"1002                      : canaryTestResult()?.id === entry.id1003                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1004                        : "test"}1005                  </button>1006                </div>1007              )}1008            </For>1009          </Show>10101011          <p class="peach-settings-sub-label">PASSKEYS</p>1012          <div class="peach-settings-row peach-settings-row--top">1013            <div class="peach-settings-row-main">1014              <span>Intercept passkey requests</span>1015              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1016                Peach can handle passkey sign-in and creation on supported Chromium browsers.1017              </span>1018            </div>1019            <button1020              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1021              onClick={() => void handleTogglePasskeys()}1022              aria-label="Toggle passkey interception"1023              disabled={!passkeyState()?.supported || passkeySaving()}1024            >1025              <span class="peach-toggle-knob"></span>1026            </button>1027          </div>1028          <div class="peach-settings-row peach-settings-row--meta">1029            <span>Status</span>1030            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1031          </div>1032          <div class="peach-settings-row peach-settings-row--meta">1033            <span>Browser</span>1034            <span class="peach-settings-value">1035              {passkeyState()?.browserLabel ?? "Unknown"}1036              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1037            </span>1038          </div>1039          <div class="peach-info-notice" style="margin-top:8px">1040            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1041          </div>1042          <Show when={passkeyState()?.conflict}>1043            <div class="peach-settings-row peach-settings-row--meta">1044              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1045            </div>1046          </Show>1047        </div>1048      </div>1049    );1050  }10511052  function renderVault() {1053    return (1054      <div class="peach-settings-body">1055        <div class="peach-settings-group">1056          <p class="peach-settings-sub-label">ENTRIES</p>1057          <div class="peach-settings-row peach-settings-row--meta">1058            <span>Entries</span>1059            <span class="peach-settings-value">{props.entriesCount}</span>1060          </div>1061          <div class="peach-settings-row peach-settings-row--meta">1062            <span>Duplicates</span>1063            <Show when={dedupDone() !== null} fallback={1064              <button1065                class="peach-btn peach-btn--subtle peach-btn--sm"1066                onClick={() => void handleCountDuplicates()}1067                disabled={dedupRunning()}1068              >1069                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1070              </button>1071            }>1072              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1073                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1074              </span>1075            </Show>1076          </div>1077          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1078            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1079              <div class="peach-restore-warning">1080                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1081              </div>1082              <div style="display:flex;gap:6px">1083                <button1084                  class="peach-btn peach-btn--subtle peach-btn--sm"1085                  onClick={() => setDedupCount(null)}1086                >1087                  cancel1088                </button>1089                <button1090                  class="peach-btn peach-btn--accent peach-btn--sm"1091                  onClick={() => void handleDeduplicate()}1092                  disabled={dedupRunning()}1093                >1094                  {dedupRunning() ? "removing..." : "remove duplicates"}1095                </button>1096              </div>1097            </div>1098          </Show>10991100          <p class="peach-settings-sub-label">MAINTENANCE</p>1101          <div class="peach-settings-row peach-settings-row--meta">1102            <span>Trash auto-delete</span>1103            <select1104              class="peach-settings-select"1105              value={String(props.trashRetentionDays)}1106              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1107            >1108              <option value="7">7 days</option>1109              <option value="30">30 days</option>1110              <option value="90">90 days</option>1111              <option value="0">Never</option>1112            </select>1113          </div>1114          <div class="peach-settings-row peach-settings-row--meta">1115            <span>Vault Health</span>1116            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1117              open1118            </button>1119          </div>1120          <Show when={declinedCardCount() > 0}>1121            <div class="peach-settings-row peach-settings-row--meta">1122              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1123              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1124                Clear1125              </button>1126            </div>1127          </Show>11281129          <p class="peach-settings-sub-label">DATA</p>1130          <div class="peach-settings-row peach-settings-row--meta">1131            <span>Import passwords</span>1132            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1133              open1134            </button>1135          </div>1136          <div class="peach-settings-row peach-settings-row--meta">1137            <span>Export backup</span>1138            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1139              open1140            </button>1141          </div>11421143          <p class="peach-settings-sub-label">VAULT HISTORY</p>1144          <Show when={historyLoading()}>1145            <p class="peach-settings-hint">Loading…</p>1146          </Show>1147          <Show when={!historyLoading() && historySnapshots().length === 0}>1148            <p class="peach-settings-hint">1149              No snapshots yet. Peach saves automatically before every change —1150              saves, syncs, and imports.1151            </p>1152          </Show>1153          <Show when={!historyLoading() && historySnapshots().length > 0}>1154            <p class="peach-settings-hint">1155              Restoring locks your vault. Unlock with the password that1156              encrypted that snapshot to confirm.1157            </p>1158            <div class="peach-history-list">1159              <For each={historySnapshots()}>1160                {(snap) => (1161                  <div class="peach-history-row">1162                    <div class="peach-history-meta">1163                      <span class="peach-history-time">1164                        {formatSnapshotTime(snap.savedAt)}1165                      </span>1166                      <span class="peach-history-sub">1167                        {[1168                          snap.entryCount !== null1169                            ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1170                            : null,1171                          reasonLabel(snap.reason),1172                        ]1173                          .filter(Boolean)1174                          .join(" · ")}1175                      </span>1176                    </div>1177                    <button1178                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1179                        historyRestoring() === snap.id ? " is-busy" : ""1180                      }`}1181                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1182                      disabled={historyRestoring() !== null}1183                    >1184                      <Show when={historyRestoring() === snap.id} fallback="Restore">1185                        …1186                      </Show>1187                    </button>1188                  </div>1189                )}1190              </For>1191            </div>1192            <p class="peach-history-footer">1193              {historySnapshots().length} of 10 snapshots1194            </p>1195          </Show>11961197          <button1198            class="peach-settings-advanced-toggle"1199            onClick={() => setShowVaultAdvanced((v) => !v)}1200            aria-expanded={showVaultAdvanced()}1201          >1202            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1203            <span class="peach-settings-advanced-chevron">1204              {showVaultAdvanced() ? "▲" : "▼"}1205            </span>1206          </button>12071208          <Show when={showVaultAdvanced()}>1209            <div class="peach-danger-card">1210              <div class="peach-danger-card-head">1211                <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1212                <h3 class="peach-danger-card-title">Replace vault from backup</h3>1213              </div>1214              <p class="peach-danger-card-copy">1215                Wipes the current vault and restores from a backup file.1216                Recoverable only if your backup is good.1217              </p>1218              <button1219                class="peach-danger-card-button"1220                onClick={() => props.onOpenFileImport?.()}1221              >1222                Choose backup file1223              </button>1224            </div>12251226            <Show when={props.onOpenPanic}>1227              <div class="peach-danger-card peach-danger-card--hazard">1228                <div class="peach-danger-card-head">1229                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1230                  <h3 class="peach-danger-card-title">Destroy vault</h3>1231                </div>1232                <p class="peach-danger-card-copy">1233                  Permanently wipes every entry, setting, and license on this1234                  device. There is no recovery.1235                </p>1236                <button1237                  class="peach-danger-card-button"1238                  onClick={() => props.onOpenPanic?.()}1239                >1240                  Begin destruction1241                </button>1242              </div>1243            </Show>1244          </Show>1245        </div>1246      </div>1247    );1248  }12491250  function renderSync() {1251    if (!isPro()) {1252      return (1253        <div class="peach-settings-body">1254          <div class="peach-pro-upgrade">1255            <div class="peach-pro-upgrade-hero">1256              <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1257              <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1258            </div>12591260            <div class="peach-pro-features">1261              <div class="peach-pro-feature">1262                <span class="peach-pro-feature-mark">✦</span>1263                <div class="peach-pro-feature-text">1264                  <span class="peach-pro-feature-name">Always in sync</span>1265                  <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1266                </div>1267              </div>1268              <div class="peach-pro-feature">1269                <span class="peach-pro-feature-mark">✦</span>1270                <div class="peach-pro-feature-text">1271                  <span class="peach-pro-feature-name">No cloud. No relay.</span>1272                  <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1273                </div>1274              </div>1275              <div class="peach-pro-feature">1276                <span class="peach-pro-feature-mark">✦</span>1277                <div class="peach-pro-feature-text">1278                  <span class="peach-pro-feature-name">Tailscale supported</span>1279                  <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1280                </div>1281              </div>1282              <div class="peach-pro-feature">1283                <span class="peach-pro-feature-mark">✦</span>1284                <div class="peach-pro-feature-text">1285                  <span class="peach-pro-feature-name">Zero friction after setup</span>1286                  <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1287                </div>1288              </div>1289            </div>12901291            <button1292              class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1293              onClick={() => setActiveSection("license")}1294            >1295              Upgrade to Pro1296            </button>1297            <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1298              Already have a key? Enter it →1299            </button>1300          </div>1301        </div>1302      );1303    }13041305    return (1306      <div class="peach-settings-body">1307        <div class="peach-settings-group">1308          <div class="peach-settings-row">1309            <span>Status</span>1310            <span class="peach-settings-value peach-settings-value--status">1311              <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1312              {getStatusLabel(syncStatus())}1313            </span>1314          </div>1315          <div class="peach-settings-row peach-settings-row--meta">1316            <span>Last sync</span>1317            <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1318          </div>1319          <div class="peach-settings-row peach-settings-row--meta">1320            <button1321              class="peach-btn peach-btn--subtle peach-btn--sm"1322              onClick={() => void handleSyncNow()}1323              disabled={syncStatus() === "not_trusted" || syncingNow()}1324            >1325              <Switch>1326                <Match when={syncingNow()}>Syncing…</Match>1327                <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1328                <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1329                <Match when={true}>Sync now</Match>1330              </Switch>1331            </button>1332          </div>1333          <p class="peach-settings-hint">1334            If you edit on multiple devices before syncing, the most recently1335            changed device wins. Use Vault History above to recover older1336            versions if needed.1337          </p>13381339          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1340            <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1341            <Show when={pairedDevices().length === 0}>1342              <p class="peach-settings-hint">1343                No paired devices yet. Add a device to start syncing.1344              </p>1345            </Show>1346            <Show when={pairedDevices().length > 0}>1347              <div class="peach-history-list">1348                <For each={pairedDevices()}>1349                  {(device) => (1350                    <div class="peach-history-row">1351                      <div class="peach-history-meta">1352                        <span class="peach-history-time">{device.displayName}</span>1353                        <span class="peach-history-sub">1354                          {device.lastSyncAt1355                            ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1356                            : "Never synced"}1357                        </span>1358                      </div>1359                      <button1360                        class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1361                        onClick={() => void handleRemoveDevice(device.deviceId)}1362                      >1363                        Remove1364                      </button>1365                    </div>1366                  )}1367                </For>1368              </div>1369            </Show>1370            <div class="peach-settings-actions--row">1371              <button1372                class="peach-btn peach-btn--subtle peach-btn--sm"1373                onClick={() => props.onOpenSyncPairing?.()}1374              >1375                Add a device1376              </button>1377            </div>1378          </div>13791380          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1381            <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1382            <For each={trustedNetworks()}>1383              {(network) => (1384                <div class="peach-sync-device-row">1385                  <span class="peach-sync-device-name">1386                    {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1387                  </span>1388                  <Show1389                    when={!network.isTailscale}1390                    fallback={<span class="peach-sync-device-meta">Always trusted</span>}1391                  >1392                    <button1393                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1394                      onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1395                    >1396                      Remove1397                    </button>1398                  </Show>1399                </div>1400              )}1401            </For>1402            <div class="peach-settings-actions--row">1403              <Show when={!isCurrentNetworkTrusted()}>1404                <button1405                  class="peach-btn peach-btn--subtle peach-btn--sm"1406                  onClick={() => void handleMarkNetworkTrusted()}1407                  disabled={markingNetwork()}1408                >1409                  {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1410                </button>1411              </Show>1412            </div>1413          </div>14141415          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1416            <span class="peach-sync-section-label">Advanced</span>1417            <Show1418              when={resetIdentityConfirm()}1419              fallback={1420                <div class="peach-sync-advanced-row">1421                  <div class="peach-sync-advanced-info">1422                    <span class="peach-sync-advanced-name">Reset sync identity</span>1423                    <span class="peach-sync-device-meta">1424                      Generates a new keypair and unpairs every device.1425                    </span>1426                  </div>1427                  <button1428                    class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1429                    onClick={() => setResetIdentityConfirm(true)}1430                  >1431                    Reset1432                  </button>1433                </div>1434              }1435            >1436              <div class="peach-sync-reset-confirm">1437                <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1438                <p class="peach-sync-reset-confirm-body">1439                  This will unpair all devices. You will need to pair them again from scratch.1440                </p>1441                <div class="peach-settings-actions--row">1442                  <button1443                    class="peach-btn peach-btn--ghost peach-btn--sm"1444                    onClick={() => setResetIdentityConfirm(false)}1445                    disabled={resetIdentityRunning()}1446                  >1447                    Cancel1448                  </button>1449                  <button1450                    class="peach-btn peach-btn--danger peach-btn--sm"1451                    onClick={() => void handleResetSyncIdentity()}1452                    disabled={resetIdentityRunning()}1453                  >1454                    {resetIdentityRunning() ? "Resetting..." : "Reset"}1455                  </button>1456                </div>1457              </div>1458            </Show>1459          </div>1460        </div>1461      </div>1462    );1463  }14641465  function renderAppearance() {1466    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1467      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1468      { id: "cormorant", label: "Cormorant", preview: "Aa" },1469      { id: "inter", label: "Inter", preview: "Aa" },1470      { id: "system", label: "System", preview: "Aa" },1471    ];1472    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1473      { id: "small", label: "Small" },1474      { id: "medium", label: "Default" },1475      { id: "large", label: "Large" },1476    ];1477    return (1478      <div class="peach-settings-body">1479        <div class="peach-settings-group">1480          <p class="peach-settings-sub-label">THEME</p>1481          <div class="peach-settings-row peach-settings-row--meta">1482            <span>Theme</span>1483            <button1484              class="peach-btn peach-btn--subtle peach-btn--sm"1485              onClick={() =>1486                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1487              }1488            >1489              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1490            </button>1491          </div>14921493          <p class="peach-settings-sub-label">ACCENT</p>1494          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1495            <span>Accent</span>1496            <div class="peach-accent-picker">1497              <For each={ACCENT_COLORS}>1498                {(color) => (1499                  <button1500                    class="peach-accent-swatch"1501                    classList={{ "is-active": props.accentColor === color.name }}1502                    style={{ background: color.hex }}1503                    onClick={() => void props.onAccentChange(color.name)}1504                    aria-label={color.name}1505                    title={color.name}1506                  />1507                )}1508              </For>1509            </div>1510          </div>15111512          <p class="peach-settings-sub-label">DISPLAY FONT</p>1513          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1514            <span>Font</span>1515            <div class="peach-font-picker">1516              <For each={fontOptions}>1517                {(opt) => (1518                  <button1519                    class="peach-font-swatch"1520                    classList={{ "is-active": props.displayFont === opt.id }}1521                    data-font-preview={opt.id}1522                    onClick={() => void props.onDisplayFontChange(opt.id)}1523                    aria-label={opt.label}1524                    title={opt.label}1525                  >1526                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1527                    <span class="peach-font-swatch-name">{opt.label}</span>1528                  </button>1529                )}1530              </For>1531            </div>1532          </div>15331534          <p class="peach-settings-sub-label">TEXT SIZE</p>1535          <div class="peach-settings-row peach-settings-row--meta">1536            <span>Size</span>1537            <div class="peach-pill-group">1538              <For each={sizeOptions}>1539                {(opt) => (1540                  <button1541                    class="peach-btn peach-btn--ghost peach-btn--sm"1542                    classList={{ "is-active": props.textSize === opt.id }}1543                    onClick={() => void props.onTextSizeChange(opt.id)}1544                  >1545                    {opt.label}1546                  </button>1547                )}1548              </For>1549            </div>1550          </div>15511552          <p class="peach-settings-sub-label">MOTION</p>1553          <div class="peach-settings-row peach-settings-row--meta">1554            <span>Reduce motion</span>1555            <button1556              class="peach-btn peach-btn--subtle peach-btn--sm"1557              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1558            >1559              {props.reduceMotion ? "On" : "Off"}1560            </button>1561          </div>1562        </div>1563      </div>1564    );1565  }15661567  function renderShortcuts() {1568    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1569    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15701571    function startCapture(action: ShortcutAction) {1572      setConflictMsg(null);1573      setCapturingAction(action);1574    }1575    function cancelCapture() {1576      setCapturingAction(null);1577      setConflictMsg(null);1578    }1579    function commitBinding(action: ShortcutAction, binding: Keybinding) {1580      const conflict = findConflict(props.shortcuts, binding, action);1581      if (conflict) {1582        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1583        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1584        return;1585      }1586      const next = { ...props.shortcuts, [action]: binding };1587      setCapturingAction(null);1588      setConflictMsg(null);1589      void props.onShortcutsChange(next);1590    }1591    function resetAction(action: ShortcutAction) {1592      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1593      void props.onShortcutsChange(next);1594    }1595    function resetAll() {1596      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1597    }15981599    // Capture-mode key handler — wired via ref onto the chip element so it1600    // catches the next non-modifier keypress and ignores other bindings.1601    function handleCaptureKey(e: KeyboardEvent) {1602      e.preventDefault();1603      e.stopPropagation();1604      if (e.key === "Escape") { cancelCapture(); return; }1605      const binding = keybindingFromEvent(e);1606      if (!binding) return; // bare modifier — keep waiting1607      const action = capturingAction();1608      if (action) commitBinding(action, binding);1609    }16101611    return (1612      <div class="peach-settings-body">1613        <div class="peach-settings-group peach-shortcuts-group">1614          <p class="peach-settings-sub-label">ENTRY VIEW</p>1615          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1616            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1617          </For>1618        </div>16191620        <div class="peach-settings-group peach-shortcuts-group">1621          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1622          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1623            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1624          </For>1625        </div>16261627        <Show when={conflictMsg()}>1628          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1629        </Show>16301631        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1632          <div class="peach-settings-row peach-settings-row--meta">1633            <span>Inline autofill on page</span>1634            <a1635              class="peach-settings-link"1636              href="chrome://extensions/shortcuts"1637              onClick={(e) => {1638                e.preventDefault();1639                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1640              }}1641            >1642              ⌘⇧P — customize ↗1643            </a>1644          </div>1645          <button1646            class="peach-btn peach-btn--subtle peach-btn--sm"1647            style="align-self:flex-start"1648            onClick={resetAll}1649          >1650            Reset all to defaults1651          </button>1652        </div>1653      </div>1654    );16551656    function renderRow(1657      desc: typeof SHORTCUT_DESCRIPTORS[number],1658      capturing: () => ShortcutAction | null,1659      onStart: (a: ShortcutAction) => void,1660      onCancel: () => void,1661      onReset: (a: ShortcutAction) => void,1662      onKey: (e: KeyboardEvent) => void,1663    ) {1664      const isActive = () => capturing() === desc.action;1665      const binding = () => props.shortcuts[desc.action];1666      const isCustom = () => {1667        const b = binding();1668        const d = DEFAULT_BINDINGS[desc.action];1669        return b.key !== d.key1670          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1671          || Boolean(b.meta)  !== Boolean(d.meta)1672          || Boolean(b.shift) !== Boolean(d.shift)1673          || Boolean(b.alt)   !== Boolean(d.alt);1674      };1675      return (1676        <div class="peach-shortcut-row">1677          <span class="peach-shortcut-row-label">{desc.label}</span>1678          <button1679            class="peach-shortcut-chip"1680            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1681            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1682            onKeyDown={(e) => { if (isActive()) onKey(e); }}1683            ref={(el) => {1684              // Auto-focus when entering capture mode so the keydown listener catches the next press.1685              if (el) {1686                queueMicrotask(() => { if (isActive()) el.focus(); });1687              }1688            }}1689          >1690            {isActive() ? "Press a key…" : formatKeybinding(binding())}1691          </button>1692          <Show when={isCustom() && !isActive()}>1693            <button1694              class="peach-shortcut-reset"1695              title="Reset to default"1696              onClick={() => onReset(desc.action)}1697            >1698              ↺1699            </button>1700          </Show>1701        </div>1702      );1703    }1704  }17051706  const PRO_FEATURES: Array<{1707    feature: ProFeature;1708    name: string;1709    desc: string;1710  }> = [1711    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1712    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1713    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1714    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1715    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1716  ];17171718  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17191720  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1721  const [deactivating, setDeactivating] = createSignal(false);17221723  async function handleDeactivateLicense() {1724    setDeactivating(true);1725    await sendMessage({ type: "DEACTIVATE_LICENSE" });1726    setLicenseInfo(null);1727    await props.onLicenseChange?.();1728    setDeactivating(false);1729  }17301731  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1732    setCanaryTestingId(entry.id);1733    setCanaryTestResult(null);1734    const res = await sendMessage({1735      type: "FIRE_CANARY_PING",1736      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1737    }) as { ok: boolean };1738    setCanaryTestResult({ id: entry.id, ok: res.ok });1739    setCanaryTestingId(null);1740    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1741  }17421743  function renderLicense() {1744    async function onActivate() {1745      await handleActivateLicense();1746      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1747        await props.onLicenseChange?.();1748      }1749    }17501751    return (1752      <div class="peach-settings-body">1753        <Show1754          when={isPro()}1755          fallback={1756            <Show1757              when={showKeyEntry()}1758              fallback={1759                <div class="peach-pro-upgrade">1760                  <div class="peach-pro-upgrade-hero">1761                    <div class="peach-pro-upgrade-mark-wrap">1762                      <PeachMark class="peach-pro-upgrade-mark" />1763                    </div>1764                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1765                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1766                  </div>17671768                  <div class="peach-pro-features">1769                    <For each={PRO_FEATURES}>1770                      {(item) => (1771                        <div1772                          class="peach-pro-feature"1773                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1774                        >1775                          <span class="peach-pro-feature-mark">✦</span>1776                          <div class="peach-pro-feature-text">1777                            <span class="peach-pro-feature-name">{item.name}</span>1778                            <span class="peach-pro-feature-desc">{item.desc}</span>1779                          </div>1780                        </div>1781                      )}1782                    </For>1783                  </div>17841785                  <div class="peach-pro-upgrade-actions">1786                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1787                    <a1788                      href="https://peachpasswords.com/#pricing"1789                      target="_blank"1790                      rel="noopener noreferrer"1791                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1792                    >1793                      Get Peach Pro1794                    </a>1795                    <div class="peach-pro-key-divider"><span>or</span></div>1796                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1797                      Activate a license key1798                    </button>1799                  </div>1800                </div>1801              }1802            >1803              <div class="peach-pro-key-ceremony">1804                <LockRuneCanvas />1805                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1806                <input1807                  class="peach-underline-input peach-pro-key-input"1808                  type="text"1809                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1810                  value={licenseKeyInput()}1811                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1812                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1813                  autocomplete="off"1814                  spellcheck={false}1815                />1816                <Show when={licenseError()}>1817                  <p class="peach-pro-key-error">That key doesn't look right.</p>1818                </Show>1819                <button1820                  class="peach-btn peach-btn--accent peach-pro-key-activate"1821                  onClick={() => void onActivate()}1822                  disabled={activatingLicense() || !licenseKeyInput().trim()}1823                >1824                  {activatingLicense() ? "activating…" : "Activate"}1825                </button>1826                <button1827                  class="peach-pro-key-cancel"1828                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1829                >1830                  cancel1831                </button>1832              </div>1833            </Show>1834          }1835        >1836          <div class="peach-pro-active">1837            <div class="peach-pro-active-hero">1838              <div class="peach-pro-active-mark-wrap">1839                <PeachMark class="peach-pro-active-mark" />1840              </div>1841              <h2 class="peach-pro-active-title">Peach Pro</h2>1842              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1843            </div>18441845            <Show when={licenseKey()}>1846              <div class="peach-settings-license">1847                <span class="peach-settings-label">LICENSE KEY</span>1848                <div class="peach-settings-license-row">1849                  <span class="peach-settings-license-key">{licenseKey()!}</span>1850                  <button1851                    class="peach-settings-license-copy"1852                    onClick={() => copyLicenseKey(licenseKey()!)}1853                  >1854                    {copied() ? "✓" : "copy"}1855                  </button>1856                </div>1857              </div>1858            </Show>18591860            <div class="peach-pro-active-features">1861              <For each={PRO_FEATURES}>1862                {(item) => (1863                  <div class="peach-pro-active-feature">1864                    <span class="peach-pro-active-feature-mark">✦</span>1865                    <span class="peach-pro-active-feature-name">{item.name}</span>1866                  </div>1867                )}1868              </For>1869            </div>18701871            <button1872              class="peach-pro-deactivate"1873              onClick={() => void handleDeactivateLicense()}1874              disabled={deactivating()}1875            >1876              {deactivating() ? "Deactivating…" : "Deactivate"}1877            </button>1878          </div>1879        </Show>1880      </div>1881    );1882  }18831884  function renderFeedback() {1885    return (1886      <div class="peach-settings-body">1887        <div class="peach-settings-group peach-feedback-group">1888          <p class="peach-feedback-intro">1889            Help us improve Peach. Every report goes directly to our inbox.1890          </p>18911892          <p class="peach-settings-sub-label">TYPE</p>1893          <div class="peach-feedback-chips">1894            <For each={FEEDBACK_CATEGORIES}>1895              {(cat) => (1896                <button1897                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1898                  onClick={() => setFbCategory(cat)}1899                >1900                  {cat}1901                </button>1902              )}1903            </For>1904          </div>19051906          <p class="peach-settings-sub-label">MESSAGE</p>1907          <textarea1908            class="peach-feedback-textarea"1909            placeholder="Describe the issue or idea..."1910            value={fbMessage()}1911            onInput={(e) => setFbMessage(e.currentTarget.value)}1912          />19131914          <p class="peach-feedback-meta">1915            Version {version} · will be included automatically1916          </p>19171918          <Show when={fbResult() === "success"}>1919            <p class="peach-feedback-result peach-feedback-result--ok">1920              Sent — thank you1921            </p>1922          </Show>1923          <Show when={fbResult() === "error"}>1924            <p class="peach-feedback-result peach-feedback-result--err">1925              Failed to send — try again1926            </p>1927          </Show>19281929          <button1930            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1931            onClick={() => void handleSubmitFeedback()}1932            disabled={fbSubmitting() || !fbMessage().trim()}1933          >1934            {fbSubmitting() ? "Sending..." : "Send feedback"}1935          </button>19361937          <p class="peach-feedback-anon">1938            Sent anonymously · no account required1939          </p>1940        </div>1941      </div>1942    );1943  }19441945  function renderMobile() {1946    return (1947      <div class="peach-settings-body">1948        <div class="peach-settings-group">1949          <p class="peach-settings-label">Autofill service</p>1950          <div class="peach-mobile-row">1951            <span class="peach-mobile-status">1952              {autofillEnabled() ? "Enabled" : "Disabled"}1953            </span>1954            <button1955              class="peach-btn peach-btn--subtle peach-btn--sm"1956              onClick={() => void AutofillPlugin?.openAutofillSettings()}1957            >1958              {autofillEnabled() ? "Change" : "Enable"}1959            </button>1960          </div>1961        </div>19621963        <div class="peach-settings-group">1964          <p class="peach-settings-label">Biometric unlock</p>1965          <div class="peach-mobile-row">1966            <span class="peach-mobile-status">1967              {biometricEnabled() ? "Available" : "Not available"}1968            </span>1969          </div>1970        </div>1971      </div>1972    );1973  }19741975  return (1976    <div class="peach-overlay-view ov-enter-right" style="display:flex">1977      <div class="peach-overlay-header">1978        <button1979          class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1980          onClick={handleHeaderBack}1981          aria-label="Back"1982        >1983          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1984            <path d="M6 1L1 6L6 11" />1985          </svg>1986        </button>1987        <span class="peach-overlay-title">{currentTitle()}</span>1988        <div class="peach-overlay-spacer"></div>1989      </div>19901991      <Switch fallback={renderHome()}>1992        <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1993        <Match when={activeSection() === "vault"}>{renderVault()}</Match>1994        <Match when={activeSection() === "sync"}>{renderSync()}</Match>1995        <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1996        <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1997        <Match when={activeSection() === "license"}>{renderLicense()}</Match>1998        <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1999        <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>2000      </Switch>2001    </div>2002  );2003}