Settings.tsx

80.3 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              <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1257                <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1258                <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1259                <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"/>1260                <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"/>1261                <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1262                <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1263                <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1264              </svg>1265              <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1266              <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1267            </div>12681269            <div class="peach-pro-features">1270              <div class="peach-pro-feature">1271                <span class="peach-pro-feature-mark">✦</span>1272                <div class="peach-pro-feature-text">1273                  <span class="peach-pro-feature-name">Always in sync</span>1274                  <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1275                </div>1276              </div>1277              <div class="peach-pro-feature">1278                <span class="peach-pro-feature-mark">✦</span>1279                <div class="peach-pro-feature-text">1280                  <span class="peach-pro-feature-name">No cloud. No relay.</span>1281                  <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1282                </div>1283              </div>1284              <div class="peach-pro-feature">1285                <span class="peach-pro-feature-mark">✦</span>1286                <div class="peach-pro-feature-text">1287                  <span class="peach-pro-feature-name">Tailscale supported</span>1288                  <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1289                </div>1290              </div>1291              <div class="peach-pro-feature">1292                <span class="peach-pro-feature-mark">✦</span>1293                <div class="peach-pro-feature-text">1294                  <span class="peach-pro-feature-name">Zero friction after setup</span>1295                  <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1296                </div>1297              </div>1298            </div>12991300            <button1301              class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1302              onClick={() => setActiveSection("license")}1303            >1304              Upgrade to Pro1305            </button>1306            <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1307              Already have a key? Enter it →1308            </button>1309          </div>1310        </div>1311      );1312    }13131314    return (1315      <div class="peach-settings-body">1316        <div class="peach-settings-group">1317          <div class="peach-settings-row">1318            <span>Status</span>1319            <span class="peach-settings-value peach-settings-value--status">1320              <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1321              {getStatusLabel(syncStatus())}1322            </span>1323          </div>1324          <div class="peach-settings-row peach-settings-row--meta">1325            <span>Last sync</span>1326            <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1327          </div>1328          <div class="peach-settings-row peach-settings-row--meta">1329            <button1330              class="peach-btn peach-btn--subtle peach-btn--sm"1331              onClick={() => void handleSyncNow()}1332              disabled={syncStatus() === "not_trusted" || syncingNow()}1333            >1334              <Switch>1335                <Match when={syncingNow()}>Syncing…</Match>1336                <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1337                <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1338                <Match when={true}>Sync now</Match>1339              </Switch>1340            </button>1341          </div>1342          <p class="peach-settings-hint">1343            If you edit on multiple devices before syncing, the most recently1344            changed device wins. Use Vault History above to recover older1345            versions if needed.1346          </p>13471348          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1349            <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1350            <Show when={pairedDevices().length === 0}>1351              <p class="peach-settings-hint">1352                No paired devices yet. Add a device to start syncing.1353              </p>1354            </Show>1355            <Show when={pairedDevices().length > 0}>1356              <div class="peach-history-list">1357                <For each={pairedDevices()}>1358                  {(device) => (1359                    <div class="peach-history-row">1360                      <div class="peach-history-meta">1361                        <span class="peach-history-time">{device.displayName}</span>1362                        <span class="peach-history-sub">1363                          {device.lastSyncAt1364                            ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1365                            : "Never synced"}1366                        </span>1367                      </div>1368                      <button1369                        class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1370                        onClick={() => void handleRemoveDevice(device.deviceId)}1371                      >1372                        Remove1373                      </button>1374                    </div>1375                  )}1376                </For>1377              </div>1378            </Show>1379            <div class="peach-settings-actions--row">1380              <button1381                class="peach-btn peach-btn--subtle peach-btn--sm"1382                onClick={() => props.onOpenSyncPairing?.()}1383              >1384                Add a device1385              </button>1386            </div>1387          </div>13881389          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1390            <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1391            <For each={trustedNetworks()}>1392              {(network) => (1393                <div class="peach-sync-device-row">1394                  <span class="peach-sync-device-name">1395                    {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1396                  </span>1397                  <Show1398                    when={!network.isTailscale}1399                    fallback={<span class="peach-sync-device-meta">Always trusted</span>}1400                  >1401                    <button1402                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1403                      onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1404                    >1405                      Remove1406                    </button>1407                  </Show>1408                </div>1409              )}1410            </For>1411            <div class="peach-settings-actions--row">1412              <Show when={!isCurrentNetworkTrusted()}>1413                <button1414                  class="peach-btn peach-btn--subtle peach-btn--sm"1415                  onClick={() => void handleMarkNetworkTrusted()}1416                  disabled={markingNetwork()}1417                >1418                  {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1419                </button>1420              </Show>1421            </div>1422          </div>14231424          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1425            <span class="peach-sync-section-label">Advanced</span>1426            <Show1427              when={resetIdentityConfirm()}1428              fallback={1429                <div class="peach-sync-advanced-row">1430                  <div class="peach-sync-advanced-info">1431                    <span class="peach-sync-advanced-name">Reset sync identity</span>1432                    <span class="peach-sync-device-meta">1433                      Generates a new keypair and unpairs every device.1434                    </span>1435                  </div>1436                  <button1437                    class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1438                    onClick={() => setResetIdentityConfirm(true)}1439                  >1440                    Reset1441                  </button>1442                </div>1443              }1444            >1445              <div class="peach-sync-reset-confirm">1446                <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1447                <p class="peach-sync-reset-confirm-body">1448                  This will unpair all devices. You will need to pair them again from scratch.1449                </p>1450                <div class="peach-settings-actions--row">1451                  <button1452                    class="peach-btn peach-btn--ghost peach-btn--sm"1453                    onClick={() => setResetIdentityConfirm(false)}1454                    disabled={resetIdentityRunning()}1455                  >1456                    Cancel1457                  </button>1458                  <button1459                    class="peach-btn peach-btn--danger peach-btn--sm"1460                    onClick={() => void handleResetSyncIdentity()}1461                    disabled={resetIdentityRunning()}1462                  >1463                    {resetIdentityRunning() ? "Resetting..." : "Reset"}1464                  </button>1465                </div>1466              </div>1467            </Show>1468          </div>1469        </div>1470      </div>1471    );1472  }14731474  function renderAppearance() {1475    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1476      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1477      { id: "cormorant", label: "Cormorant", preview: "Aa" },1478      { id: "inter", label: "Inter", preview: "Aa" },1479      { id: "system", label: "System", preview: "Aa" },1480    ];1481    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1482      { id: "small", label: "Small" },1483      { id: "medium", label: "Default" },1484      { id: "large", label: "Large" },1485    ];1486    return (1487      <div class="peach-settings-body">1488        <div class="peach-settings-group">1489          <p class="peach-settings-sub-label">THEME</p>1490          <div class="peach-settings-row peach-settings-row--meta">1491            <span>Theme</span>1492            <button1493              class="peach-btn peach-btn--subtle peach-btn--sm"1494              onClick={() =>1495                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1496              }1497            >1498              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1499            </button>1500          </div>15011502          <p class="peach-settings-sub-label">ACCENT</p>1503          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1504            <span>Accent</span>1505            <div class="peach-accent-picker">1506              <For each={ACCENT_COLORS}>1507                {(color) => (1508                  <button1509                    class="peach-accent-swatch"1510                    classList={{ "is-active": props.accentColor === color.name }}1511                    style={{ background: color.hex }}1512                    onClick={() => void props.onAccentChange(color.name)}1513                    aria-label={color.name}1514                    title={color.name}1515                  />1516                )}1517              </For>1518            </div>1519          </div>15201521          <p class="peach-settings-sub-label">DISPLAY FONT</p>1522          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1523            <span>Font</span>1524            <div class="peach-font-picker">1525              <For each={fontOptions}>1526                {(opt) => (1527                  <button1528                    class="peach-font-swatch"1529                    classList={{ "is-active": props.displayFont === opt.id }}1530                    data-font-preview={opt.id}1531                    onClick={() => void props.onDisplayFontChange(opt.id)}1532                    aria-label={opt.label}1533                    title={opt.label}1534                  >1535                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1536                    <span class="peach-font-swatch-name">{opt.label}</span>1537                  </button>1538                )}1539              </For>1540            </div>1541          </div>15421543          <p class="peach-settings-sub-label">TEXT SIZE</p>1544          <div class="peach-settings-row peach-settings-row--meta">1545            <span>Size</span>1546            <div class="peach-pill-group">1547              <For each={sizeOptions}>1548                {(opt) => (1549                  <button1550                    class="peach-btn peach-btn--ghost peach-btn--sm"1551                    classList={{ "is-active": props.textSize === opt.id }}1552                    onClick={() => void props.onTextSizeChange(opt.id)}1553                  >1554                    {opt.label}1555                  </button>1556                )}1557              </For>1558            </div>1559          </div>15601561          <p class="peach-settings-sub-label">MOTION</p>1562          <div class="peach-settings-row peach-settings-row--meta">1563            <span>Reduce motion</span>1564            <button1565              class="peach-btn peach-btn--subtle peach-btn--sm"1566              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1567            >1568              {props.reduceMotion ? "On" : "Off"}1569            </button>1570          </div>1571        </div>1572      </div>1573    );1574  }15751576  function renderShortcuts() {1577    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1578    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15791580    function startCapture(action: ShortcutAction) {1581      setConflictMsg(null);1582      setCapturingAction(action);1583    }1584    function cancelCapture() {1585      setCapturingAction(null);1586      setConflictMsg(null);1587    }1588    function commitBinding(action: ShortcutAction, binding: Keybinding) {1589      const conflict = findConflict(props.shortcuts, binding, action);1590      if (conflict) {1591        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1592        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1593        return;1594      }1595      const next = { ...props.shortcuts, [action]: binding };1596      setCapturingAction(null);1597      setConflictMsg(null);1598      void props.onShortcutsChange(next);1599    }1600    function resetAction(action: ShortcutAction) {1601      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1602      void props.onShortcutsChange(next);1603    }1604    function resetAll() {1605      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1606    }16071608    // Capture-mode key handler — wired via ref onto the chip element so it1609    // catches the next non-modifier keypress and ignores other bindings.1610    function handleCaptureKey(e: KeyboardEvent) {1611      e.preventDefault();1612      e.stopPropagation();1613      if (e.key === "Escape") { cancelCapture(); return; }1614      const binding = keybindingFromEvent(e);1615      if (!binding) return; // bare modifier — keep waiting1616      const action = capturingAction();1617      if (action) commitBinding(action, binding);1618    }16191620    return (1621      <div class="peach-settings-body">1622        <div class="peach-settings-group peach-shortcuts-group">1623          <p class="peach-settings-sub-label">ENTRY VIEW</p>1624          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1625            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1626          </For>1627        </div>16281629        <div class="peach-settings-group peach-shortcuts-group">1630          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1631          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1632            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1633          </For>1634        </div>16351636        <Show when={conflictMsg()}>1637          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1638        </Show>16391640        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1641          <div class="peach-settings-row peach-settings-row--meta">1642            <span>Inline autofill on page</span>1643            <a1644              class="peach-settings-link"1645              href="chrome://extensions/shortcuts"1646              onClick={(e) => {1647                e.preventDefault();1648                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1649              }}1650            >1651              ⌘⇧P — customize ↗1652            </a>1653          </div>1654          <button1655            class="peach-btn peach-btn--subtle peach-btn--sm"1656            style="align-self:flex-start"1657            onClick={resetAll}1658          >1659            Reset all to defaults1660          </button>1661        </div>1662      </div>1663    );16641665    function renderRow(1666      desc: typeof SHORTCUT_DESCRIPTORS[number],1667      capturing: () => ShortcutAction | null,1668      onStart: (a: ShortcutAction) => void,1669      onCancel: () => void,1670      onReset: (a: ShortcutAction) => void,1671      onKey: (e: KeyboardEvent) => void,1672    ) {1673      const isActive = () => capturing() === desc.action;1674      const binding = () => props.shortcuts[desc.action];1675      const isCustom = () => {1676        const b = binding();1677        const d = DEFAULT_BINDINGS[desc.action];1678        return b.key !== d.key1679          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1680          || Boolean(b.meta)  !== Boolean(d.meta)1681          || Boolean(b.shift) !== Boolean(d.shift)1682          || Boolean(b.alt)   !== Boolean(d.alt);1683      };1684      return (1685        <div class="peach-shortcut-row">1686          <span class="peach-shortcut-row-label">{desc.label}</span>1687          <button1688            class="peach-shortcut-chip"1689            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1690            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1691            onKeyDown={(e) => { if (isActive()) onKey(e); }}1692            ref={(el) => {1693              // Auto-focus when entering capture mode so the keydown listener catches the next press.1694              if (el) {1695                queueMicrotask(() => { if (isActive()) el.focus(); });1696              }1697            }}1698          >1699            {isActive() ? "Press a key…" : formatKeybinding(binding())}1700          </button>1701          <Show when={isCustom() && !isActive()}>1702            <button1703              class="peach-shortcut-reset"1704              title="Reset to default"1705              onClick={() => onReset(desc.action)}1706            >1707              ↺1708            </button>1709          </Show>1710        </div>1711      );1712    }1713  }17141715  const PRO_FEATURES: Array<{1716    feature: ProFeature;1717    name: string;1718    desc: string;1719  }> = [1720    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1721    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1722    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1723    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1724    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1725  ];17261727  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17281729  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1730  const [deactivating, setDeactivating] = createSignal(false);17311732  async function handleDeactivateLicense() {1733    setDeactivating(true);1734    await sendMessage({ type: "DEACTIVATE_LICENSE" });1735    setLicenseInfo(null);1736    await props.onLicenseChange?.();1737    setDeactivating(false);1738  }17391740  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1741    setCanaryTestingId(entry.id);1742    setCanaryTestResult(null);1743    const res = await sendMessage({1744      type: "FIRE_CANARY_PING",1745      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1746    }) as { ok: boolean };1747    setCanaryTestResult({ id: entry.id, ok: res.ok });1748    setCanaryTestingId(null);1749    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1750  }17511752  function renderLicense() {1753    async function onActivate() {1754      await handleActivateLicense();1755      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1756        await props.onLicenseChange?.();1757      }1758    }17591760    return (1761      <div class="peach-settings-body">1762        <Show1763          when={isPro()}1764          fallback={1765            <Show1766              when={showKeyEntry()}1767              fallback={1768                <div class="peach-pro-upgrade">1769                  <div class="peach-pro-upgrade-hero">1770                    <div class="peach-pro-upgrade-mark-wrap">1771                      <PeachMark class="peach-pro-upgrade-mark" />1772                    </div>1773                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1774                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1775                  </div>17761777                  <div class="peach-pro-features">1778                    <For each={PRO_FEATURES}>1779                      {(item) => (1780                        <div1781                          class="peach-pro-feature"1782                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1783                        >1784                          <span class="peach-pro-feature-mark">✦</span>1785                          <div class="peach-pro-feature-text">1786                            <span class="peach-pro-feature-name">{item.name}</span>1787                            <span class="peach-pro-feature-desc">{item.desc}</span>1788                          </div>1789                        </div>1790                      )}1791                    </For>1792                  </div>17931794                  <div class="peach-pro-upgrade-actions">1795                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1796                    <a1797                      href="https://peachpasswords.com/#pricing"1798                      target="_blank"1799                      rel="noopener noreferrer"1800                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1801                    >1802                      Get Peach Pro1803                    </a>1804                    <div class="peach-pro-key-divider"><span>or</span></div>1805                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1806                      Activate a license key1807                    </button>1808                  </div>1809                </div>1810              }1811            >1812              <div class="peach-pro-key-ceremony">1813                <LockRuneCanvas />1814                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1815                <input1816                  class="peach-underline-input peach-pro-key-input"1817                  type="text"1818                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1819                  value={licenseKeyInput()}1820                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1821                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1822                  autocomplete="off"1823                  spellcheck={false}1824                />1825                <Show when={licenseError()}>1826                  <p class="peach-pro-key-error">That key doesn't look right.</p>1827                </Show>1828                <button1829                  class="peach-btn peach-btn--accent peach-pro-key-activate"1830                  onClick={() => void onActivate()}1831                  disabled={activatingLicense() || !licenseKeyInput().trim()}1832                >1833                  {activatingLicense() ? "activating…" : "Activate"}1834                </button>1835                <button1836                  class="peach-pro-key-cancel"1837                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1838                >1839                  cancel1840                </button>1841              </div>1842            </Show>1843          }1844        >1845          <div class="peach-pro-active">1846            <div class="peach-pro-active-hero">1847              <div class="peach-pro-active-mark-wrap">1848                <PeachMark class="peach-pro-active-mark" />1849              </div>1850              <h2 class="peach-pro-active-title">Peach Pro</h2>1851              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1852            </div>18531854            <Show when={licenseKey()}>1855              <div class="peach-settings-license">1856                <span class="peach-settings-label">LICENSE KEY</span>1857                <div class="peach-settings-license-row">1858                  <span class="peach-settings-license-key">{licenseKey()!}</span>1859                  <button1860                    class="peach-settings-license-copy"1861                    onClick={() => copyLicenseKey(licenseKey()!)}1862                  >1863                    {copied() ? "✓" : "copy"}1864                  </button>1865                </div>1866              </div>1867            </Show>18681869            <div class="peach-pro-active-features">1870              <For each={PRO_FEATURES}>1871                {(item) => (1872                  <div class="peach-pro-active-feature">1873                    <span class="peach-pro-active-feature-mark">✦</span>1874                    <span class="peach-pro-active-feature-name">{item.name}</span>1875                  </div>1876                )}1877              </For>1878            </div>18791880            <button1881              class="peach-pro-deactivate"1882              onClick={() => void handleDeactivateLicense()}1883              disabled={deactivating()}1884            >1885              {deactivating() ? "Deactivating…" : "Deactivate"}1886            </button>1887          </div>1888        </Show>1889      </div>1890    );1891  }18921893  function renderFeedback() {1894    return (1895      <div class="peach-settings-body">1896        <div class="peach-settings-group peach-feedback-group">1897          <p class="peach-feedback-intro">1898            Help us improve Peach. Every report goes directly to our inbox.1899          </p>19001901          <p class="peach-settings-sub-label">TYPE</p>1902          <div class="peach-feedback-chips">1903            <For each={FEEDBACK_CATEGORIES}>1904              {(cat) => (1905                <button1906                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1907                  onClick={() => setFbCategory(cat)}1908                >1909                  {cat}1910                </button>1911              )}1912            </For>1913          </div>19141915          <p class="peach-settings-sub-label">MESSAGE</p>1916          <textarea1917            class="peach-feedback-textarea"1918            placeholder="Describe the issue or idea..."1919            value={fbMessage()}1920            onInput={(e) => setFbMessage(e.currentTarget.value)}1921          />19221923          <p class="peach-feedback-meta">1924            Version {version} · will be included automatically1925          </p>19261927          <Show when={fbResult() === "success"}>1928            <p class="peach-feedback-result peach-feedback-result--ok">1929              Sent — thank you1930            </p>1931          </Show>1932          <Show when={fbResult() === "error"}>1933            <p class="peach-feedback-result peach-feedback-result--err">1934              Failed to send — try again1935            </p>1936          </Show>19371938          <button1939            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1940            onClick={() => void handleSubmitFeedback()}1941            disabled={fbSubmitting() || !fbMessage().trim()}1942          >1943            {fbSubmitting() ? "Sending..." : "Send feedback"}1944          </button>19451946          <p class="peach-feedback-anon">1947            Sent anonymously · no account required1948          </p>1949        </div>1950      </div>1951    );1952  }19531954  function renderMobile() {1955    return (1956      <div class="peach-settings-body">1957        <div class="peach-settings-group">1958          <p class="peach-settings-label">Autofill service</p>1959          <div class="peach-mobile-row">1960            <span class="peach-mobile-status">1961              {autofillEnabled() ? "Enabled" : "Disabled"}1962            </span>1963            <button1964              class="peach-btn peach-btn--subtle peach-btn--sm"1965              onClick={() => void AutofillPlugin?.openAutofillSettings()}1966            >1967              {autofillEnabled() ? "Change" : "Enable"}1968            </button>1969          </div>1970        </div>19711972        <div class="peach-settings-group">1973          <p class="peach-settings-label">Biometric unlock</p>1974          <div class="peach-mobile-row">1975            <span class="peach-mobile-status">1976              {biometricEnabled() ? "Available" : "Not available"}1977            </span>1978          </div>1979        </div>1980      </div>1981    );1982  }19831984  return (1985    <div class="peach-overlay-view ov-enter-right" style="display:flex">1986      <div class="peach-overlay-header">1987        <button1988          class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1989          onClick={handleHeaderBack}1990          aria-label="Back"1991        >1992          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1993            <path d="M6 1L1 6L6 11" />1994          </svg>1995        </button>1996        <span class="peach-overlay-title">{currentTitle()}</span>1997        <div class="peach-overlay-spacer"></div>1998      </div>19992000      <Switch fallback={renderHome()}>2001        <Match when={activeSection() === "security"}>{renderSecurity()}</Match>2002        <Match when={activeSection() === "vault"}>{renderVault()}</Match>2003        <Match when={activeSection() === "sync"}>{renderSync()}</Match>2004        <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2005        <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2006        <Match when={activeSection() === "license"}>{renderLicense()}</Match>2007        <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2008        <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>2009      </Switch>2010    </div>2011  );2012}