Settings.tsx

80.0 KB · raw

1import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2import { Capacitor, registerPlugin } from "@capacitor/core";3import { sendMessage } from "../App";4import { PeachMark } from "../components/PeachMark";5import { LockRuneCanvas } from "../components/LockRuneCanvas";6import { biometricAvailable } from "../../shared/biometric";7import type { Canary } from "../../shared/types";8import { ACCENT_COLORS } from "../../shared/types";9import type {10  AccentColorName,11  PasskeyProviderState,12  ProFeature,13  VaultSnapshot,14} from "../../shared/types";15import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types";16import {17  DEFAULT_BINDINGS,18  findConflict,19  formatKeybinding,20  keybindingFromEvent,21  SHORTCUT_DESCRIPTORS,22  type Keybinding,23  type ShortcutAction,24} from "../../shared/shortcuts";2526interface LicenseInfo {27  valid: boolean;28  plan: "free" | "pro";29  issued?: string;30}3132interface SettingsProps {33  entriesCount: number;34  breachMonitoring: boolean;35  lockOnClose: boolean;36  autoLockMinutes: number;37  trashRetentionDays: number;38  theme: "dark" | "light";39  accentColor: AccentColorName;40  displayFont: "cormorant-italic" | "cormorant" | "inter" | "system";41  textSize: "small" | "medium" | "large";42  reduceMotion: boolean;43  isPro?: boolean;44  initialSection?: string | null;45  upgradeHighlight?: ProFeature | null;46  onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;47  onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;48  onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;49  onTrashRetentionChange: (days: number) => void | Promise<void>;50  onThemeChange: (theme: "dark" | "light") => void;51  onAccentChange: (name: AccentColorName) => void | Promise<void>;52  onDisplayFontChange: (font: "cormorant-italic" | "cormorant" | "inter" | "system") => void | Promise<void>;53  onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise<void>;54  onReduceMotionChange: (enabled: boolean) => void | Promise<void>;55  shortcuts: Record<ShortcutAction, Keybinding>;56  onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;57  onLicenseChange?: () => void | Promise<void>;58  onBack: () => void;59  onImport: () => void;60  onExport: () => void;61  onBreachCheckDone: () => void;62  onOpenHealth?: () => void;63  onOpenSyncPairing?: () => void;64  onOpenFileImport?: () => void;65  onOpenPanic?: () => void;66}6768type SectionName =69  | "security"70  | "vault"71  | "sync"72  | "appearance"73  | "shortcuts"74  | "license"75  | "feedback"76  | "mobile";7778const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7980function formatActivatedSince(issued: string | undefined): string {81  if (!issued) return "Active";82  try {83    const d = new Date(issued);84    if (Number.isNaN(d.getTime())) return "Active";85    return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;86  } catch {87    return "Active";88  }89}9091function isSectionName(value: string): value is SectionName {92  return (93    value === "security" ||94    value === "vault" ||95    value === "sync" ||96    value === "appearance" ||97    value === "shortcuts" ||98    value === "license" ||99    value === "feedback" ||100    value === "mobile"101  );102}103104export function SettingsView(props: SettingsProps) {105  const version = chrome.runtime.getManifest().version;106  const initial = props.initialSection;107  const [activeSection, setActiveSection] = createSignal<SectionName | null>(108    initial && isSectionName(initial) ? (initial as SectionName) : null109  );110111  const [checkingBreaches, setCheckingBreaches] = createSignal(false);112  const [breachResult, setBreachResult] = createSignal<string | null>(null);113  const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);114  const [licenseKeyInput, setLicenseKeyInput] = createSignal("");115  const [licenseError, setLicenseError] = createSignal<string | null>(null);116  const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);117  const [activatingLicense, setActivatingLicense] = createSignal(false);118119  const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);120  const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);121  const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");122  const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);123  // Prefer the prop (cached at App boot), fall back to the sync-state probe.124  // The prop is authoritative once present so license activation reflects125  // immediately without waiting for a sync round-trip.126  const [syncProStatus, setSyncProStatus] = createSignal(false);127  const isPro = () => props.isPro ?? syncProStatus();128  const [syncingNow, setSyncingNow] = createSignal(false);129  const [syncFeedback, setSyncFeedback] = createSignal<130    { kind: "ok" | "err"; text: string } | null131  >(null);132  const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);133  const [passkeySaving, setPasskeySaving] = createSignal(false);134135  const [dedupCount, setDedupCount] = createSignal<number | null>(null);136  const [dedupRunning, setDedupRunning] = createSignal(false);137  const [dedupDone, setDedupDone] = createSignal<number | null>(null);138139  const [declinedCardCount, setDeclinedCardCount] = createSignal(0);140141  // Vault history (peach_vault_history) — last 10 snapshots, snapshotted142  // BEFORE every real-mode vault write so the user can roll back if a sync143  // or import overwrites the wrong state.144  const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);145  const [historyLoading, setHistoryLoading] = createSignal(false);146  const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);147148  async function loadVaultHistory() {149    setHistoryLoading(true);150    try {151      const res = await sendMessage({ type: "GET_VAULT_HISTORY" });152      const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];153      setHistorySnapshots(list);154    } finally {155      setHistoryLoading(false);156    }157  }158159  async function handleRestoreVaultSnapshot(id: string) {160    if (historyRestoring()) return;161    setHistoryRestoring(id);162    const res = await sendMessage({163      type: "RESTORE_VAULT_SNAPSHOT",164      payload: { snapshotId: id },165    });166    setHistoryRestoring(null);167    if ((res as { ok: boolean }).ok) {168      // Vault is now locked — let the parent close Settings; App.tsx's169      // VAULT_LOCKED listener will route back to the lock screen.170      props.onBack();171    }172  }173174  function formatSnapshotTime(savedAt: number): string {175    const diff = Date.now() - savedAt;176    if (diff < 60_000) return "Just now";177    if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;178    const d = new Date(savedAt);179    const isToday = new Date().toDateString() === d.toDateString();180    const isYest =181      new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();182    const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });183    if (isToday) return `Today ${t}`;184    if (isYest) return `Yesterday ${t}`;185    return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;186  }187188  function reasonLabel(reason: VaultSnapshot["reason"]): string {189    return (190      {191        local_save: "saved",192        sync_received: "synced",193        import: "imported",194        restore: "restored",195      } as const196    )[reason] ?? reason;197  }198199  const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);200201  const [showChangePw, setShowChangePw] = createSignal(false);202  const [changePwCurrent, setChangePwCurrent] = createSignal("");203  const [changePwNew, setChangePwNew] = createSignal("");204  const [changePwConfirm, setChangePwConfirm] = createSignal("");205  const [changePwSaving, setChangePwSaving] = createSignal(false);206  const [changePwError, setChangePwError] = createSignal<string | null>(null);207  const [changePwSuccess, setChangePwSuccess] = createSignal(false);208209  const [duressEnabled, setDuressEnabled] = createSignal(false);210  const [showDuressSetup, setShowDuressSetup] = createSignal(false);211  const [duressPassword, setDuressPassword] = createSignal("");212  const [duressConfirm, setDuressConfirm] = createSignal("");213  const [duressMainPassword, setDuressMainPassword] = createSignal("");214  const [duressSaving, setDuressSaving] = createSignal(false);215  const [duressError, setDuressError] = createSignal<string | null>(null);216  const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);217218  const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);219220  // Mobile-only state221  function getAutofillPlugin() {222    try {223      return registerPlugin<{224        isEnabledAsAutofillService(): Promise<{ enabled: boolean }>;225        openAutofillSettings(): Promise<void>;226      }>("Autofill");227    } catch {228      return null;229    }230  }231  const AutofillPlugin = getAutofillPlugin();232  const [autofillEnabled, setAutofillEnabled] = createSignal(false);233  const [biometricEnabled, setBiometricEnabled] = createSignal(false);234  const isAndroid = Capacitor.getPlatform() === "android";235  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);236  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);237238  const [fbCategory, setFbCategory] = createSignal("Bug report");239  const [fbMessage, setFbMessage] = createSignal("");240  const [fbSubmitting, setFbSubmitting] = createSignal(false);241  const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);242243  onMount(async () => {244    const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([245      sendMessage({ type: "GET_LICENSE" }),246      sendMessage({ type: "GET_SYNC_STATE" }),247      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),248      sendMessage({ type: "IS_DURESS_ENABLED" }),249      sendMessage({ type: "GET_SETTINGS" }),250      sendMessage({ type: "GET_LICENSE_KEY" }),251    ]);252253    // Vault history loads in the background — the Vault sub-view picks it up254    // when the user navigates in. Loading it lazily keeps Settings boot fast.255    void loadVaultHistory();256257    if ((licenseRes as { ok: boolean }).ok) {258      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);259    }260261    if ((syncRes as { ok: boolean }).ok) {262      const data = syncRes as { ok: boolean; data?: {263        pairedDevices?: PairedDevice[];264        trustedNetworks?: TrustedNetwork[];265        status?: SyncStatus;266        lastSyncAt?: number;267        isPro?: boolean;268      }};269      if (data.data) {270        setPairedDevices(data.data.pairedDevices ?? []);271        setTrustedNetworks(data.data.trustedNetworks ?? []);272        setSyncStatus(data.data.status ?? "offline");273        setLastSyncAt(data.data.lastSyncAt);274        setSyncProStatus(data.data.isPro ?? false);275      }276    }277278    if ((passkeyRes as { ok: boolean }).ok) {279      setPasskeyState(280        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null281      );282    }283284    if ((duressRes as { ok: boolean }).ok) {285      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);286    }287288    if ((settingsRes as { ok: boolean }).ok) {289      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;290      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);291    }292293    if ((keyRes as { ok: boolean }).ok) {294      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);295    }296297    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });298    if ((entriesRes as { ok: boolean }).ok) {299      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];300      setCanaryEntries(301        entries302          .filter((e) => e.canary?.webhookUrl)303          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))304      );305    }306307    // Android-only: check autofill and biometric status308    if (isAndroid) {309      if (AutofillPlugin) {310        const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();311        setAutofillEnabled(enabled);312      }313      const bio = await biometricAvailable();314      setBiometricEnabled(bio);315    }316317    chrome.runtime.onMessage.addListener((msg) => {318      if (msg.type === "SYNC_STATE_UPDATE") {319        setPairedDevices(msg.pairedDevices ?? []);320        setTrustedNetworks(msg.trustedNetworks ?? []);321        setSyncStatus(msg.status ?? "offline");322        setLastSyncAt(msg.lastSyncAt);323        setSyncProStatus(msg.isPro ?? false);324      }325    });326  });327328  async function handleChangePassword() {329    setChangePwError(null);330    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }331    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }332    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }333    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }334    setChangePwSaving(true);335    const res = await sendMessage({336      type: "CHANGE_PASSWORD",337      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },338    }) as { ok: boolean; error?: string };339    if (res.ok) {340      setChangePwSuccess(true);341      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");342      setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);343    } else {344      setChangePwError(res.error ?? "failed to change password");345    }346    setChangePwSaving(false);347  }348349  const [copied, setCopied] = createSignal(false);350351  async function copyLicenseKey(key: string) {352    await navigator.clipboard.writeText(key);353    setCopied(true);354    setTimeout(() => setCopied(false), 2000);355  }356357  async function handleActivateLicense() {358    const key = licenseKeyInput().trim();359    if (!key) return;360    setActivatingLicense(true);361    setLicenseError(null);362    setLicenseSuccess(null);363    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });364    setActivatingLicense(false);365    if ((res as { ok: boolean }).ok) {366      const data = (res as { ok: boolean; data?: LicenseInfo }).data;367      if (data?.valid) {368        setLicenseInfo(data);369        setLicenseKey(key);370        setLicenseSuccess("License activated!");371        setLicenseKeyInput("");372      } else {373        setLicenseError("Invalid license key");374      }375    } else {376      setLicenseError("Activation failed");377    }378  }379380  async function handleTogglePasskeys() {381    const current = passkeyState();382    if (!current || !current.supported) return;383    setPasskeySaving(true);384    const res = await sendMessage({385      type: "SET_PASSKEY_PROVIDER_ENABLED",386      payload: { enabled: !current.enabled },387    });388    setPasskeySaving(false);389    if ((res as { ok: boolean }).ok) {390      setPasskeyState(391        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null392      );393    }394  }395396  async function handleCheckAllBreaches() {397    setCheckingBreaches(true);398    setBreachResult(null);399    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });400    setCheckingBreaches(false);401    if ((res as { ok: boolean }).ok) {402      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};403      const breached = Object.values(data).filter((count) => count > 0).length;404      setBreachResult(405        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"406      );407      props.onBreachCheckDone();408    }409  }410411  async function handleSyncNow() {412    if (syncStatus() === "not_trusted") return;413    setSyncingNow(true);414    setSyncFeedback(null);415    const res = await sendMessage({ type: "TRIGGER_SYNC" });416    const ok = (res as { ok: boolean }).ok;417    if (ok) {418      const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;419      const count = data?.syncedCount ?? 0;420      setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });421      setLastSyncAt(Date.now());422      void refreshSyncState();423    } else {424      const error = (res as { error?: string }).error;425      const map: Record<string, string> = {426        no_paired_devices: "No paired devices",427        no_devices_reachable: "No devices found",428        vault_locked: "Vault is locked",429        pro_required: "Sync requires Pro",430      };431      setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });432    }433    setTimeout(() => {434      setSyncingNow(false);435      setSyncFeedback(null);436    }, 3500);437  }438439  async function refreshSyncState() {440    const res = await sendMessage({ type: "GET_SYNC_STATE" });441    const data = (res as { ok: boolean; data?: {442      pairedDevices?: PairedDevice[];443      trustedNetworks?: TrustedNetwork[];444      status?: SyncStatus;445      lastSyncAt?: number;446    }}).data;447    if (!data) return;448    setPairedDevices(data.pairedDevices ?? []);449    setTrustedNetworks(data.trustedNetworks ?? []);450    setSyncStatus(data.status ?? "offline");451    setLastSyncAt(data.lastSyncAt);452  }453454  async function handleRemoveDevice(deviceId: string) {455    setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));456    await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });457    void refreshSyncState();458  }459460  async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {461    await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });462    void refreshSyncState();463  }464465  const [markingNetwork, setMarkingNetwork] = createSignal(false);466467  async function handleMarkNetworkTrusted() {468    if (markingNetwork()) return;469    setMarkingNetwork(true);470    await sendMessage({ type: "MARK_NETWORK_TRUSTED" });471    await refreshSyncState();472    setMarkingNetwork(false);473  }474475  const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);476  const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);477478  async function handleResetSyncIdentity() {479    setResetIdentityRunning(true);480    const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });481    setResetIdentityRunning(false);482    if ((res as { ok: boolean }).ok) {483      setPairedDevices([]);484      setResetIdentityConfirm(false);485    }486  }487488  function formatRelativeTime(timestamp?: number): string {489    if (!timestamp) return "Never";490    const diff = Date.now() - timestamp;491    const minutes = Math.floor(diff / 60000);492    const hours = Math.floor(diff / 3600000);493    const days = Math.floor(diff / 86400000);494    if (minutes < 1) return "Just now";495    if (minutes < 60) return `${minutes}m ago`;496    if (hours < 24) return `${hours}h ago`;497    return `${days}d ago`;498  }499500  function getStatusColor(status: SyncStatus): string {501    switch (status) {502      case "synced": return "var(--peach-sync-ok)";503      case "syncing": return "var(--peach-sync-busy)";504      case "no_peers": return "var(--peach-sync-off)";505      case "not_trusted": return "var(--peach-sync-err)";506      case "offline": return "var(--peach-sync-off)";507      default: return "var(--peach-sync-off)";508    }509  }510511  function getStatusLabel(status: SyncStatus): string {512    switch (status) {513      case "synced": return "Synced";514      case "syncing": return "Syncing";515      case "no_peers": return "No peers visible";516      case "not_trusted": return "Not on a trusted network";517      case "offline": return "Offline";518      default: return "Unknown";519    }520  }521522  function isCurrentNetworkTrusted(): boolean {523    return trustedNetworks().length > 0;524  }525526  async function handleSetupDuress() {527    const mainPw = duressMainPassword().trim();528    const duressPw = duressPassword().trim();529    const confirmPw = duressConfirm().trim();530531    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }532    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }533    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }534    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }535536    setDuressSaving(true);537    setDuressError(null);538539    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };540    if (!verifyRes.ok || !verifyRes.data) {541      setDuressError("incorrect main password");542      setDuressSaving(false);543      return;544    }545546    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };547    setDuressSaving(false);548    if (res.ok) {549      setDuressEnabled(true);550      setShowDuressSetup(false);551      setDuressPassword("");552      setDuressConfirm("");553      setDuressMainPassword("");554      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");555      setTimeout(() => setDuressSuccess(null), 5000);556    } else {557      setDuressError(res.error ?? "failed to set duress password");558    }559  }560561  async function handleClearDeclinedCards() {562    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });563    setDeclinedCardCount(0);564  }565566  async function handleCountDuplicates() {567    setDedupRunning(true);568    setDedupDone(null);569    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };570    setDedupRunning(false);571    if (res.ok) setDedupCount(res.data ?? 0);572  }573574  async function handleDeduplicate() {575    setDedupRunning(true);576    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };577    setDedupRunning(false);578    if (res.ok) {579      setDedupDone(res.data ?? 0);580      setDedupCount(0);581    }582  }583584  async function handleDisableDuress() {585    const mainPw = duressMainPassword().trim();586    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }587588    setDuressSaving(true);589    setDuressError(null);590591    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };592    if (!verifyRes.ok || !verifyRes.data) {593      setDuressError("incorrect main password");594      setDuressSaving(false);595      return;596    }597598    const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };599    setDuressSaving(false);600    if (res.ok) {601      setDuressEnabled(false);602      setShowDuressSetup(false);603      setDuressMainPassword("");604      setDuressSuccess("Duress password disabled.");605      setTimeout(() => setDuressSuccess(null), 4000);606    } else {607      setDuressError(res.error ?? "failed to disable duress password");608    }609  }610611  async function handleSubmitFeedback() {612    if (!fbMessage().trim() || fbSubmitting()) return;613    setFbSubmitting(true);614    setFbResult(null);615    const res = await sendMessage({616      type: "SUBMIT_FEEDBACK",617      payload: {618        category: fbCategory(),619        message: fbMessage().trim(),620        version,621        browser: navigator.userAgent,622      },623    }) as { ok: boolean; error?: string };624    setFbSubmitting(false);625    if (res.ok) {626      setFbResult("success");627      setFbMessage("");628      setTimeout(() => setFbResult(null), 4000);629    } else {630      setFbResult("error");631    }632  }633634  function handleHeaderBack() {635    if (activeSection() !== null) {636      setActiveSection(null);637    } else {638      props.onBack();639    }640  }641642  function currentTitle(): string {643    const s = activeSection();644    if (s === null) return "settings";645    if (s === "license") return "account";646    return s;647  }648649  function renderCatRow(opts: {650    section: SectionName;651    name: string;652    desc: string;653    icon: () => any;654    pro?: boolean;655  }) {656    return (657      <button658        class="peach-settings-cat-row"659        onClick={() => setActiveSection(opts.section)}660      >661        <div class="peach-settings-cat-icon">{opts.icon()}</div>662        <div class="peach-settings-cat-content">663          <span class="peach-settings-cat-name">664            {opts.name}665            <Show when={opts.pro && !isPro()}>666              <span class="peach-pro-mark" aria-label="Pro feature">✦</span>667            </Show>668          </span>669          <span class="peach-settings-cat-desc">{opts.desc}</span>670        </div>671        <svg672          class="peach-settings-cat-chevron"673          width="6" height="10" viewBox="0 0 6 10" fill="none"674          stroke="currentColor" stroke-width="1.1"675          stroke-linecap="round" stroke-linejoin="round"676        >677          <path d="M1 1l4 4-4 4" />678        </svg>679      </button>680    );681  }682683  const iconShield = () => (684    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">685      <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>686    </svg>687  );688  const iconKey = () => (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="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>691    </svg>692  );693  const iconLock = () => (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      <rect x="3" y="7" width="10" height="7" rx="1.5"/>696      <path d="M6 7V5a3 3 0 016 0v2"/>697    </svg>698  );699  const iconSync = () => (700    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">701      <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>702      <path d="M14 6l-2 2-2-2"/>703      <path d="M2 10l2-2 2 2"/>704    </svg>705  );706  const iconSun = () => (707    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">708      <circle cx="8" cy="8" r="3"/>709      <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"/>710    </svg>711  );712  const iconKeys = () => (713    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">714      <rect x="2" y="5" width="5" height="4" rx="1"/>715      <rect x="9" y="7" width="5" height="4" rx="1"/>716      <path d="M7 7h2m-2 2h2"/>717    </svg>718  );719  const iconBubble = () => (720    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">721      <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>722    </svg>723  );724725  function renderHome() {726    return (727      <div class="peach-settings-body">728        <p class="peach-settings-nav-label">PROTECTION</p>729        {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}730        {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}731732        <p class="peach-settings-nav-label">VAULT</p>733        {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}734        {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}735736        <p class="peach-settings-nav-label">PREFERENCES</p>737        {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}738        {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}739        <Show when={isAndroid}>740          <p class="peach-settings-nav-label">MOBILE</p>741          {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}742        </Show>743744        <p class="peach-settings-nav-label">SUPPORT</p>745        {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}746747        <div class="peach-settings-footer">748          <span>Peach {version}</span>749          <span class="peach-divider"></span>750          <span>Browser extension</span>751        </div>752      </div>753    );754  }755756  function renderSecurity() {757    return (758      <div class="peach-settings-body">759        <div class="peach-settings-group">760          <p class="peach-settings-sub-label">LOCKING</p>761          <div class="peach-settings-row">762            <span>Lock on close</span>763            <button764              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}765              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}766              aria-label="Toggle lock on close"767            >768              <span class="peach-toggle-knob"></span>769            </button>770          </div>771          <div class="peach-settings-row">772            <span>Auto-lock after</span>773            <select774              class="peach-settings-select"775              value={String(props.autoLockMinutes)}776              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}777            >778              <option value="1">1 minute</option>779              <option value="5">5 minutes</option>780              <option value="15">15 minutes</option>781              <option value="30">30 minutes</option>782              <option value="0">Never</option>783            </select>784          </div>785786          <p class="peach-settings-sub-label">BREACH PROTECTION</p>787          <div class="peach-settings-row">788            <span>Breach monitoring</span>789            <button790              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}791              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}792              aria-label="Toggle breach monitoring"793            >794              <span class="peach-toggle-knob"></span>795            </button>796          </div>797          <div class="peach-settings-row peach-settings-row--meta">798            <span>Check all passwords</span>799            <button800              class="peach-btn peach-btn--subtle peach-btn--sm"801              onClick={() => void handleCheckAllBreaches()}802              disabled={checkingBreaches()}803            >804              {checkingBreaches() ? "checking..." : "run"}805            </button>806          </div>807          <Show when={breachResult()}>808            <div class="peach-settings-row peach-settings-row--meta">809              <span>Last check</span>810              <span class="peach-settings-value">{breachResult()}</span>811            </div>812          </Show>813814          <p class="peach-settings-sub-label">MASTER PASSWORD</p>815          <div class="peach-settings-row peach-settings-row--meta">816            <span>Master password</span>817            <button818              class="peach-btn peach-btn--subtle peach-btn--sm"819              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}820            >821              {showChangePw() ? "cancel" : "change"}822            </button>823          </div>824          <Show when={showChangePw()}>825            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">826              <input827                class="peach-settings-input"828                type="password"829                placeholder="current password"830                value={changePwCurrent()}831                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}832                autocomplete="current-password"833              />834              <input835                class="peach-settings-input"836                type="password"837                placeholder="new password (min 12 chars)"838                value={changePwNew()}839                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}840                autocomplete="new-password"841              />842              <input843                class="peach-settings-input"844                type="password"845                placeholder="confirm new password"846                value={changePwConfirm()}847                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}848                autocomplete="new-password"849                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}850              />851              <Show when={changePwError()}>852                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>853              </Show>854              <Show when={changePwSuccess()}>855                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>856              </Show>857              <button858                class="peach-btn peach-btn--accent peach-btn--sm"859                style="align-self:flex-start"860                onClick={() => void handleChangePassword()}861                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}862              >863                {changePwSaving() ? "saving…" : "save new password"}864              </button>865            </div>866          </Show>867868          <p class="peach-settings-sub-label">DURESS PASSWORD</p>869          <div class="peach-settings-row peach-settings-row--top">870            <div class="peach-settings-row-main">871              <span>Duress password</span>872              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">873                If entered under coercion, shows a decoy empty vault instead of your real one.874              </span>875            </div>876            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>877              {duressEnabled() ? "on" : "off"}878            </div>879          </div>880881          <Show when={duressEnabled() && !showDuressSetup()}>882            <div class="peach-settings-row peach-settings-row--meta">883              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">884                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.885              </span>886            </div>887          </Show>888889          <Show when={duressSuccess()}>890            <div class="peach-settings-row peach-settings-row--meta">891              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>892            </div>893          </Show>894895          <Show when={!showDuressSetup()}>896            <div class="peach-settings-row peach-settings-row--meta">897              <button898                class="peach-btn peach-btn--subtle peach-btn--sm"899                onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}900              >901                {duressEnabled() ? "Change / disable" : "Set up"}902              </button>903            </div>904          </Show>905906          <Show when={showDuressSetup()}>907            <div class="peach-duress-form">908              <label class="peach-label">main password (to confirm)</label>909              <input910                class="peach-settings-input"911                type="password"912                placeholder="your current master password"913                autocomplete="current-password"914                value={duressMainPassword()}915                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}916              />917              <Show when={!duressEnabled()}>918                <label class="peach-label">new duress password</label>919                <input920                  class="peach-settings-input"921                  type="password"922                  placeholder="duress password (min 8 chars)"923                  autocomplete="new-password"924                  value={duressPassword()}925                  onInput={(e) => setDuressPassword(e.currentTarget.value)}926                />927                <label class="peach-label">confirm duress password</label>928                <input929                  class="peach-settings-input"930                  type="password"931                  placeholder="confirm duress password"932                  autocomplete="new-password"933                  value={duressConfirm()}934                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}935                />936              </Show>937938              <Show when={duressError()}>939                <p class="peach-hint peach-hint-error">{duressError()}</p>940              </Show>941942              <div class="peach-duress-actions">943                <button944                  class="peach-btn peach-btn--subtle peach-btn--sm"945                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}946                >947                  cancel948                </button>949                <Show when={duressEnabled()}>950                  <button951                    class="peach-btn peach-btn--ghost peach-btn--sm"952                    style="color:var(--peach-breach)"953                    onClick={() => void handleDisableDuress()}954                    disabled={duressSaving()}955                  >956                    {duressSaving() ? "disabling..." : "disable duress"}957                  </button>958                </Show>959                <Show when={!duressEnabled()}>960                  <button961                    class="peach-btn peach-btn--accent peach-btn--sm"962                    onClick={() => void handleSetupDuress()}963                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}964                  >965                    {duressSaving() ? "saving..." : "save"}966                  </button>967                </Show>968              </div>969            </div>970          </Show>971972          <p class="peach-settings-sub-label">CANARY ENTRIES</p>973          <div class="peach-settings-row peach-settings-row--top">974            <div class="peach-settings-row-main">975              <span>Canary entries</span>976              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">977                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.978              </span>979            </div>980            <span class="peach-settings-value" style="flex-shrink:0">981              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}982            </span>983          </div>984          <Show when={canaryEntries().length > 0}>985            <For each={canaryEntries()}>986              {(entry) => (987                <div class="peach-settings-row peach-settings-row--meta">988                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>989                  <button990                    class="peach-btn peach-btn--subtle peach-btn--sm"991                    onClick={() => void handleTestCanary(entry)}992                    disabled={canaryTestingId() === entry.id}993                    style="flex-shrink:0"994                  >995                    {canaryTestingId() === entry.id996                      ? "sending…"997                      : canaryTestResult()?.id === entry.id998                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")999                        : "test"}1000                  </button>1001                </div>1002              )}1003            </For>1004          </Show>10051006          <p class="peach-settings-sub-label">PASSKEYS</p>1007          <div class="peach-settings-row peach-settings-row--top">1008            <div class="peach-settings-row-main">1009              <span>Intercept passkey requests</span>1010              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1011                Peach can handle passkey sign-in and creation on supported Chromium browsers.1012              </span>1013            </div>1014            <button1015              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1016              onClick={() => void handleTogglePasskeys()}1017              aria-label="Toggle passkey interception"1018              disabled={!passkeyState()?.supported || passkeySaving()}1019            >1020              <span class="peach-toggle-knob"></span>1021            </button>1022          </div>1023          <div class="peach-settings-row peach-settings-row--meta">1024            <span>Status</span>1025            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1026          </div>1027          <div class="peach-settings-row peach-settings-row--meta">1028            <span>Browser</span>1029            <span class="peach-settings-value">1030              {passkeyState()?.browserLabel ?? "Unknown"}1031              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1032            </span>1033          </div>1034          <div class="peach-info-notice" style="margin-top:8px">1035            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1036          </div>1037          <Show when={passkeyState()?.conflict}>1038            <div class="peach-settings-row peach-settings-row--meta">1039              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1040            </div>1041          </Show>1042        </div>1043      </div>1044    );1045  }10461047  function renderVault() {1048    return (1049      <div class="peach-settings-body">1050        <div class="peach-settings-group">1051          <p class="peach-settings-sub-label">ENTRIES</p>1052          <div class="peach-settings-row peach-settings-row--meta">1053            <span>Entries</span>1054            <span class="peach-settings-value">{props.entriesCount}</span>1055          </div>1056          <div class="peach-settings-row peach-settings-row--meta">1057            <span>Duplicates</span>1058            <Show when={dedupDone() !== null} fallback={1059              <button1060                class="peach-btn peach-btn--subtle peach-btn--sm"1061                onClick={() => void handleCountDuplicates()}1062                disabled={dedupRunning()}1063              >1064                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1065              </button>1066            }>1067              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1068                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1069              </span>1070            </Show>1071          </div>1072          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1073            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1074              <div class="peach-restore-warning">1075                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1076              </div>1077              <div style="display:flex;gap:6px">1078                <button1079                  class="peach-btn peach-btn--subtle peach-btn--sm"1080                  onClick={() => setDedupCount(null)}1081                >1082                  cancel1083                </button>1084                <button1085                  class="peach-btn peach-btn--accent peach-btn--sm"1086                  onClick={() => void handleDeduplicate()}1087                  disabled={dedupRunning()}1088                >1089                  {dedupRunning() ? "removing..." : "remove duplicates"}1090                </button>1091              </div>1092            </div>1093          </Show>10941095          <p class="peach-settings-sub-label">MAINTENANCE</p>1096          <div class="peach-settings-row peach-settings-row--meta">1097            <span>Trash auto-delete</span>1098            <select1099              class="peach-settings-select"1100              value={String(props.trashRetentionDays)}1101              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1102            >1103              <option value="7">7 days</option>1104              <option value="30">30 days</option>1105              <option value="90">90 days</option>1106              <option value="0">Never</option>1107            </select>1108          </div>1109          <div class="peach-settings-row peach-settings-row--meta">1110            <span>Vault Health</span>1111            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1112              open1113            </button>1114          </div>1115          <Show when={declinedCardCount() > 0}>1116            <div class="peach-settings-row peach-settings-row--meta">1117              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1118              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1119                Clear1120              </button>1121            </div>1122          </Show>11231124          <p class="peach-settings-sub-label">DATA</p>1125          <div class="peach-settings-row peach-settings-row--meta">1126            <span>Import passwords</span>1127            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1128              open1129            </button>1130          </div>1131          <div class="peach-settings-row peach-settings-row--meta">1132            <span>Export backup</span>1133            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1134              open1135            </button>1136          </div>11371138          <p class="peach-settings-sub-label">VAULT HISTORY</p>1139          <Show when={historyLoading()}>1140            <p class="peach-settings-hint">Loading…</p>1141          </Show>1142          <Show when={!historyLoading() && historySnapshots().length === 0}>1143            <p class="peach-settings-hint">1144              No snapshots yet. Peach saves automatically before every change —1145              saves, syncs, and imports.1146            </p>1147          </Show>1148          <Show when={!historyLoading() && historySnapshots().length > 0}>1149            <p class="peach-settings-hint">1150              Restoring locks your vault. Unlock with the password that1151              encrypted that snapshot to confirm.1152            </p>1153            <div class="peach-history-list">1154              <For each={historySnapshots()}>1155                {(snap) => (1156                  <div class="peach-history-row">1157                    <div class="peach-history-meta">1158                      <span class="peach-history-time">1159                        {formatSnapshotTime(snap.savedAt)}1160                      </span>1161                      <span class="peach-history-sub">1162                        {[1163                          snap.entryCount !== null1164                            ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1165                            : null,1166                          reasonLabel(snap.reason),1167                        ]1168                          .filter(Boolean)1169                          .join(" · ")}1170                      </span>1171                    </div>1172                    <button1173                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1174                        historyRestoring() === snap.id ? " is-busy" : ""1175                      }`}1176                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1177                      disabled={historyRestoring() !== null}1178                    >1179                      <Show when={historyRestoring() === snap.id} fallback="Restore">1180                        …1181                      </Show>1182                    </button>1183                  </div>1184                )}1185              </For>1186            </div>1187            <p class="peach-history-footer">1188              {historySnapshots().length} of 10 snapshots1189            </p>1190          </Show>11911192          <button1193            class="peach-settings-advanced-toggle"1194            onClick={() => setShowVaultAdvanced((v) => !v)}1195            aria-expanded={showVaultAdvanced()}1196          >1197            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1198            <span class="peach-settings-advanced-chevron">1199              {showVaultAdvanced() ? "▲" : "▼"}1200            </span>1201          </button>12021203          <Show when={showVaultAdvanced()}>1204            <div class="peach-danger-card">1205              <div class="peach-danger-card-head">1206                <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1207                <h3 class="peach-danger-card-title">Replace vault from backup</h3>1208              </div>1209              <p class="peach-danger-card-copy">1210                Wipes the current vault and restores from a backup file.1211                Recoverable only if your backup is good.1212              </p>1213              <button1214                class="peach-danger-card-button"1215                onClick={() => props.onOpenFileImport?.()}1216              >1217                Choose backup file1218              </button>1219            </div>12201221            <Show when={props.onOpenPanic}>1222              <div class="peach-danger-card peach-danger-card--hazard">1223                <div class="peach-danger-card-head">1224                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1225                  <h3 class="peach-danger-card-title">Destroy vault</h3>1226                </div>1227                <p class="peach-danger-card-copy">1228                  Permanently wipes every entry, setting, and license on this1229                  device. There is no recovery.1230                </p>1231                <button1232                  class="peach-danger-card-button"1233                  onClick={() => props.onOpenPanic?.()}1234                >1235                  Begin destruction1236                </button>1237              </div>1238            </Show>1239          </Show>1240        </div>1241      </div>1242    );1243  }12441245  function renderSync() {1246    if (!isPro()) {1247      return (1248        <div class="peach-settings-body">1249          <div class="peach-pro-upgrade">1250            <div class="peach-pro-upgrade-hero">1251              <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1252                <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1253                <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1254                <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"/>1255                <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"/>1256                <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1257                <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1258                <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1259              </svg>1260              <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1261              <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1262            </div>12631264            <div class="peach-pro-features">1265              <div class="peach-pro-feature">1266                <span class="peach-pro-feature-mark">✦</span>1267                <div class="peach-pro-feature-text">1268                  <span class="peach-pro-feature-name">Always in sync</span>1269                  <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1270                </div>1271              </div>1272              <div class="peach-pro-feature">1273                <span class="peach-pro-feature-mark">✦</span>1274                <div class="peach-pro-feature-text">1275                  <span class="peach-pro-feature-name">No cloud. No relay.</span>1276                  <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1277                </div>1278              </div>1279              <div class="peach-pro-feature">1280                <span class="peach-pro-feature-mark">✦</span>1281                <div class="peach-pro-feature-text">1282                  <span class="peach-pro-feature-name">Tailscale supported</span>1283                  <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1284                </div>1285              </div>1286              <div class="peach-pro-feature">1287                <span class="peach-pro-feature-mark">✦</span>1288                <div class="peach-pro-feature-text">1289                  <span class="peach-pro-feature-name">Zero friction after setup</span>1290                  <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1291                </div>1292              </div>1293            </div>12941295            <button1296              class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1297              onClick={() => setActiveSection("license")}1298            >1299              Upgrade to Pro1300            </button>1301            <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1302              Already have a key? Enter it →1303            </button>1304          </div>1305        </div>1306      );1307    }13081309    return (1310      <div class="peach-settings-body">1311        <div class="peach-settings-group">1312          <div class="peach-settings-row">1313            <span>Status</span>1314            <span class="peach-settings-value peach-settings-value--status">1315              <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1316              {getStatusLabel(syncStatus())}1317            </span>1318          </div>1319          <div class="peach-settings-row peach-settings-row--meta">1320            <span>Last sync</span>1321            <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1322          </div>1323          <div class="peach-settings-row peach-settings-row--meta">1324            <button1325              class="peach-btn peach-btn--subtle peach-btn--sm"1326              onClick={() => void handleSyncNow()}1327              disabled={syncStatus() === "not_trusted" || syncingNow()}1328            >1329              <Switch>1330                <Match when={syncingNow()}>Syncing…</Match>1331                <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1332                <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1333                <Match when={true}>Sync now</Match>1334              </Switch>1335            </button>1336          </div>1337          <p class="peach-settings-hint">1338            If you edit on multiple devices before syncing, the most recently1339            changed device wins. Use Vault History above to recover older1340            versions if needed.1341          </p>13421343          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1344            <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1345            <Show when={pairedDevices().length === 0}>1346              <p class="peach-settings-hint">1347                No paired devices yet. Add a device to start syncing.1348              </p>1349            </Show>1350            <Show when={pairedDevices().length > 0}>1351              <div class="peach-history-list">1352                <For each={pairedDevices()}>1353                  {(device) => (1354                    <div class="peach-history-row">1355                      <div class="peach-history-meta">1356                        <span class="peach-history-time">{device.displayName}</span>1357                        <span class="peach-history-sub">1358                          {device.lastSyncAt1359                            ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1360                            : "Never synced"}1361                        </span>1362                      </div>1363                      <button1364                        class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1365                        onClick={() => void handleRemoveDevice(device.deviceId)}1366                      >1367                        Remove1368                      </button>1369                    </div>1370                  )}1371                </For>1372              </div>1373            </Show>1374            <div class="peach-settings-actions--row">1375              <button1376                class="peach-btn peach-btn--subtle peach-btn--sm"1377                onClick={() => props.onOpenSyncPairing?.()}1378              >1379                Add a device1380              </button>1381            </div>1382          </div>13831384          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1385            <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1386            <For each={trustedNetworks()}>1387              {(network) => (1388                <div class="peach-sync-device-row">1389                  <span class="peach-sync-device-name">1390                    {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1391                  </span>1392                  <Show1393                    when={!network.isTailscale}1394                    fallback={<span class="peach-sync-device-meta">Always trusted</span>}1395                  >1396                    <button1397                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1398                      onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1399                    >1400                      Remove1401                    </button>1402                  </Show>1403                </div>1404              )}1405            </For>1406            <div class="peach-settings-actions--row">1407              <Show when={!isCurrentNetworkTrusted()}>1408                <button1409                  class="peach-btn peach-btn--subtle peach-btn--sm"1410                  onClick={() => void handleMarkNetworkTrusted()}1411                  disabled={markingNetwork()}1412                >1413                  {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1414                </button>1415              </Show>1416            </div>1417          </div>14181419          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1420            <span class="peach-sync-section-label">Advanced</span>1421            <Show1422              when={resetIdentityConfirm()}1423              fallback={1424                <div class="peach-sync-advanced-row">1425                  <div class="peach-sync-advanced-info">1426                    <span class="peach-sync-advanced-name">Reset sync identity</span>1427                    <span class="peach-sync-device-meta">1428                      Generates a new keypair and unpairs every device.1429                    </span>1430                  </div>1431                  <button1432                    class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1433                    onClick={() => setResetIdentityConfirm(true)}1434                  >1435                    Reset1436                  </button>1437                </div>1438              }1439            >1440              <div class="peach-sync-reset-confirm">1441                <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1442                <p class="peach-sync-reset-confirm-body">1443                  This will unpair all devices. You will need to pair them again from scratch.1444                </p>1445                <div class="peach-settings-actions--row">1446                  <button1447                    class="peach-btn peach-btn--ghost peach-btn--sm"1448                    onClick={() => setResetIdentityConfirm(false)}1449                    disabled={resetIdentityRunning()}1450                  >1451                    Cancel1452                  </button>1453                  <button1454                    class="peach-btn peach-btn--danger peach-btn--sm"1455                    onClick={() => void handleResetSyncIdentity()}1456                    disabled={resetIdentityRunning()}1457                  >1458                    {resetIdentityRunning() ? "Resetting..." : "Reset"}1459                  </button>1460                </div>1461              </div>1462            </Show>1463          </div>1464        </div>1465      </div>1466    );1467  }14681469  function renderAppearance() {1470    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1471      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1472      { id: "cormorant", label: "Cormorant", preview: "Aa" },1473      { id: "inter", label: "Inter", preview: "Aa" },1474      { id: "system", label: "System", preview: "Aa" },1475    ];1476    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1477      { id: "small", label: "Small" },1478      { id: "medium", label: "Default" },1479      { id: "large", label: "Large" },1480    ];1481    return (1482      <div class="peach-settings-body">1483        <div class="peach-settings-group">1484          <p class="peach-settings-sub-label">THEME</p>1485          <div class="peach-settings-row peach-settings-row--meta">1486            <span>Theme</span>1487            <button1488              class="peach-btn peach-btn--subtle peach-btn--sm"1489              onClick={() =>1490                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1491              }1492            >1493              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1494            </button>1495          </div>14961497          <p class="peach-settings-sub-label">ACCENT</p>1498          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1499            <span>Accent</span>1500            <div class="peach-accent-picker">1501              <For each={ACCENT_COLORS}>1502                {(color) => (1503                  <button1504                    class="peach-accent-swatch"1505                    classList={{ "is-active": props.accentColor === color.name }}1506                    style={{ background: color.hex }}1507                    onClick={() => void props.onAccentChange(color.name)}1508                    aria-label={color.name}1509                    title={color.name}1510                  />1511                )}1512              </For>1513            </div>1514          </div>15151516          <p class="peach-settings-sub-label">DISPLAY FONT</p>1517          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1518            <span>Font</span>1519            <div class="peach-font-picker">1520              <For each={fontOptions}>1521                {(opt) => (1522                  <button1523                    class="peach-font-swatch"1524                    classList={{ "is-active": props.displayFont === opt.id }}1525                    data-font-preview={opt.id}1526                    onClick={() => void props.onDisplayFontChange(opt.id)}1527                    aria-label={opt.label}1528                    title={opt.label}1529                  >1530                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1531                    <span class="peach-font-swatch-name">{opt.label}</span>1532                  </button>1533                )}1534              </For>1535            </div>1536          </div>15371538          <p class="peach-settings-sub-label">TEXT SIZE</p>1539          <div class="peach-settings-row peach-settings-row--meta">1540            <span>Size</span>1541            <div class="peach-pill-group">1542              <For each={sizeOptions}>1543                {(opt) => (1544                  <button1545                    class="peach-btn peach-btn--ghost peach-btn--sm"1546                    classList={{ "is-active": props.textSize === opt.id }}1547                    onClick={() => void props.onTextSizeChange(opt.id)}1548                  >1549                    {opt.label}1550                  </button>1551                )}1552              </For>1553            </div>1554          </div>15551556          <p class="peach-settings-sub-label">MOTION</p>1557          <div class="peach-settings-row peach-settings-row--meta">1558            <span>Reduce motion</span>1559            <button1560              class="peach-btn peach-btn--subtle peach-btn--sm"1561              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1562            >1563              {props.reduceMotion ? "On" : "Off"}1564            </button>1565          </div>1566        </div>1567      </div>1568    );1569  }15701571  function renderShortcuts() {1572    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1573    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15741575    function startCapture(action: ShortcutAction) {1576      setConflictMsg(null);1577      setCapturingAction(action);1578    }1579    function cancelCapture() {1580      setCapturingAction(null);1581      setConflictMsg(null);1582    }1583    function commitBinding(action: ShortcutAction, binding: Keybinding) {1584      const conflict = findConflict(props.shortcuts, binding, action);1585      if (conflict) {1586        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1587        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1588        return;1589      }1590      const next = { ...props.shortcuts, [action]: binding };1591      setCapturingAction(null);1592      setConflictMsg(null);1593      void props.onShortcutsChange(next);1594    }1595    function resetAction(action: ShortcutAction) {1596      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1597      void props.onShortcutsChange(next);1598    }1599    function resetAll() {1600      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1601    }16021603    // Capture-mode key handler — wired via ref onto the chip element so it1604    // catches the next non-modifier keypress and ignores other bindings.1605    function handleCaptureKey(e: KeyboardEvent) {1606      e.preventDefault();1607      e.stopPropagation();1608      if (e.key === "Escape") { cancelCapture(); return; }1609      const binding = keybindingFromEvent(e);1610      if (!binding) return; // bare modifier — keep waiting1611      const action = capturingAction();1612      if (action) commitBinding(action, binding);1613    }16141615    return (1616      <div class="peach-settings-body">1617        <div class="peach-settings-group peach-shortcuts-group">1618          <p class="peach-settings-sub-label">ENTRY VIEW</p>1619          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1620            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1621          </For>1622        </div>16231624        <div class="peach-settings-group peach-shortcuts-group">1625          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1626          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1627            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1628          </For>1629        </div>16301631        <Show when={conflictMsg()}>1632          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1633        </Show>16341635        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1636          <div class="peach-settings-row peach-settings-row--meta">1637            <span>Inline autofill on page</span>1638            <a1639              class="peach-settings-link"1640              href="chrome://extensions/shortcuts"1641              onClick={(e) => {1642                e.preventDefault();1643                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1644              }}1645            >1646              ⌘⇧P — customize ↗1647            </a>1648          </div>1649          <button1650            class="peach-btn peach-btn--subtle peach-btn--sm"1651            style="align-self:flex-start"1652            onClick={resetAll}1653          >1654            Reset all to defaults1655          </button>1656        </div>1657      </div>1658    );16591660    function renderRow(1661      desc: typeof SHORTCUT_DESCRIPTORS[number],1662      capturing: () => ShortcutAction | null,1663      onStart: (a: ShortcutAction) => void,1664      onCancel: () => void,1665      onReset: (a: ShortcutAction) => void,1666      onKey: (e: KeyboardEvent) => void,1667    ) {1668      const isActive = () => capturing() === desc.action;1669      const binding = () => props.shortcuts[desc.action];1670      const isCustom = () => {1671        const b = binding();1672        const d = DEFAULT_BINDINGS[desc.action];1673        return b.key !== d.key1674          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1675          || Boolean(b.meta)  !== Boolean(d.meta)1676          || Boolean(b.shift) !== Boolean(d.shift)1677          || Boolean(b.alt)   !== Boolean(d.alt);1678      };1679      return (1680        <div class="peach-shortcut-row">1681          <span class="peach-shortcut-row-label">{desc.label}</span>1682          <button1683            class="peach-shortcut-chip"1684            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1685            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1686            onKeyDown={(e) => { if (isActive()) onKey(e); }}1687            ref={(el) => {1688              // Auto-focus when entering capture mode so the keydown listener catches the next press.1689              if (el) {1690                queueMicrotask(() => { if (isActive()) el.focus(); });1691              }1692            }}1693          >1694            {isActive() ? "Press a key…" : formatKeybinding(binding())}1695          </button>1696          <Show when={isCustom() && !isActive()}>1697            <button1698              class="peach-shortcut-reset"1699              title="Reset to default"1700              onClick={() => onReset(desc.action)}1701            >1702              ↺1703            </button>1704          </Show>1705        </div>1706      );1707    }1708  }17091710  const PRO_FEATURES: Array<{1711    feature: ProFeature;1712    name: string;1713    desc: string;1714  }> = [1715    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1716    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1717    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1718    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1719    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1720  ];17211722  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17231724  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1725  const [deactivating, setDeactivating] = createSignal(false);17261727  async function handleDeactivateLicense() {1728    setDeactivating(true);1729    await sendMessage({ type: "DEACTIVATE_LICENSE" });1730    setLicenseInfo(null);1731    await props.onLicenseChange?.();1732    setDeactivating(false);1733  }17341735  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1736    setCanaryTestingId(entry.id);1737    setCanaryTestResult(null);1738    const res = await sendMessage({1739      type: "FIRE_CANARY_PING",1740      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1741    }) as { ok: boolean };1742    setCanaryTestResult({ id: entry.id, ok: res.ok });1743    setCanaryTestingId(null);1744    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1745  }17461747  function renderLicense() {1748    async function onActivate() {1749      await handleActivateLicense();1750      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1751        await props.onLicenseChange?.();1752      }1753    }17541755    return (1756      <div class="peach-settings-body">1757        <Show1758          when={isPro()}1759          fallback={1760            <Show1761              when={showKeyEntry()}1762              fallback={1763                <div class="peach-pro-upgrade">1764                  <div class="peach-pro-upgrade-hero">1765                    <div class="peach-pro-upgrade-mark-wrap">1766                      <PeachMark class="peach-pro-upgrade-mark" />1767                    </div>1768                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1769                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1770                  </div>17711772                  <div class="peach-pro-features">1773                    <For each={PRO_FEATURES}>1774                      {(item) => (1775                        <div1776                          class="peach-pro-feature"1777                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1778                        >1779                          <span class="peach-pro-feature-mark">✦</span>1780                          <div class="peach-pro-feature-text">1781                            <span class="peach-pro-feature-name">{item.name}</span>1782                            <span class="peach-pro-feature-desc">{item.desc}</span>1783                          </div>1784                        </div>1785                      )}1786                    </For>1787                  </div>17881789                  <div class="peach-pro-upgrade-actions">1790                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1791                    <a1792                      href="https://peachpasswords.com/#pricing"1793                      target="_blank"1794                      rel="noopener noreferrer"1795                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1796                    >1797                      Get Peach Pro1798                    </a>1799                    <div class="peach-pro-key-divider"><span>or</span></div>1800                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1801                      Activate a license key1802                    </button>1803                  </div>1804                </div>1805              }1806            >1807              <div class="peach-pro-key-ceremony">1808                <LockRuneCanvas />1809                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1810                <input1811                  class="peach-underline-input peach-pro-key-input"1812                  type="text"1813                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1814                  value={licenseKeyInput()}1815                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1816                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1817                  autocomplete="off"1818                  spellcheck={false}1819                />1820                <Show when={licenseError()}>1821                  <p class="peach-pro-key-error">That key doesn't look right.</p>1822                </Show>1823                <button1824                  class="peach-btn peach-btn--accent peach-pro-key-activate"1825                  onClick={() => void onActivate()}1826                  disabled={activatingLicense() || !licenseKeyInput().trim()}1827                >1828                  {activatingLicense() ? "activating…" : "Activate"}1829                </button>1830                <button1831                  class="peach-pro-key-cancel"1832                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1833                >1834                  cancel1835                </button>1836              </div>1837            </Show>1838          }1839        >1840          <div class="peach-pro-active">1841            <div class="peach-pro-active-hero">1842              <div class="peach-pro-active-mark-wrap">1843                <PeachMark class="peach-pro-active-mark" />1844              </div>1845              <h2 class="peach-pro-active-title">Peach Pro</h2>1846              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1847            </div>18481849            <Show when={licenseKey()}>1850              <div class="peach-settings-license">1851                <span class="peach-settings-label">LICENSE KEY</span>1852                <div class="peach-settings-license-row">1853                  <span class="peach-settings-license-key">{licenseKey()!}</span>1854                  <button1855                    class="peach-settings-license-copy"1856                    onClick={() => copyLicenseKey(licenseKey()!)}1857                  >1858                    {copied() ? "✓" : "copy"}1859                  </button>1860                </div>1861              </div>1862            </Show>18631864            <div class="peach-pro-active-features">1865              <For each={PRO_FEATURES}>1866                {(item) => (1867                  <div class="peach-pro-active-feature">1868                    <span class="peach-pro-active-feature-mark">✦</span>1869                    <span class="peach-pro-active-feature-name">{item.name}</span>1870                  </div>1871                )}1872              </For>1873            </div>18741875            <button1876              class="peach-pro-deactivate"1877              onClick={() => void handleDeactivateLicense()}1878              disabled={deactivating()}1879            >1880              {deactivating() ? "Deactivating…" : "Deactivate"}1881            </button>1882          </div>1883        </Show>1884      </div>1885    );1886  }18871888  function renderFeedback() {1889    return (1890      <div class="peach-settings-body">1891        <div class="peach-settings-group peach-feedback-group">1892          <p class="peach-feedback-intro">1893            Help us improve Peach. Every report goes directly to our inbox.1894          </p>18951896          <p class="peach-settings-sub-label">TYPE</p>1897          <div class="peach-feedback-chips">1898            <For each={FEEDBACK_CATEGORIES}>1899              {(cat) => (1900                <button1901                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1902                  onClick={() => setFbCategory(cat)}1903                >1904                  {cat}1905                </button>1906              )}1907            </For>1908          </div>19091910          <p class="peach-settings-sub-label">MESSAGE</p>1911          <textarea1912            class="peach-feedback-textarea"1913            placeholder="Describe the issue or idea..."1914            value={fbMessage()}1915            onInput={(e) => setFbMessage(e.currentTarget.value)}1916          />19171918          <p class="peach-feedback-meta">1919            Version {version} · will be included automatically1920          </p>19211922          <Show when={fbResult() === "success"}>1923            <p class="peach-feedback-result peach-feedback-result--ok">1924              Sent — thank you1925            </p>1926          </Show>1927          <Show when={fbResult() === "error"}>1928            <p class="peach-feedback-result peach-feedback-result--err">1929              Failed to send — try again1930            </p>1931          </Show>19321933          <button1934            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1935            onClick={() => void handleSubmitFeedback()}1936            disabled={fbSubmitting() || !fbMessage().trim()}1937          >1938            {fbSubmitting() ? "Sending..." : "Send feedback"}1939          </button>19401941          <p class="peach-feedback-anon">1942            Sent anonymously · no account required1943          </p>1944        </div>1945      </div>1946    );1947  }19481949  function renderMobile() {1950    return (1951      <div class="peach-settings-body">1952        <div class="peach-settings-group">1953          <p class="peach-settings-label">Autofill service</p>1954          <div class="peach-mobile-row">1955            <span class="peach-mobile-status">1956              {autofillEnabled() ? "Enabled" : "Disabled"}1957            </span>1958            <button1959              class="peach-btn peach-btn--subtle peach-btn--sm"1960              onClick={() => void AutofillPlugin?.openAutofillSettings()}1961            >1962              {autofillEnabled() ? "Change" : "Enable"}1963            </button>1964          </div>1965        </div>19661967        <div class="peach-settings-group">1968          <p class="peach-settings-label">Biometric unlock</p>1969          <div class="peach-mobile-row">1970            <span class="peach-mobile-status">1971              {biometricEnabled() ? "Available" : "Not available"}1972            </span>1973          </div>1974        </div>1975      </div>1976    );1977  }19781979  return (1980    <div class="peach-overlay-view ov-enter-right" style="display:flex">1981      <div class="peach-overlay-header">1982        <button1983          class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1984          onClick={handleHeaderBack}1985          aria-label="Back"1986        >1987          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1988            <path d="M6 1L1 6L6 11" />1989          </svg>1990        </button>1991        <span class="peach-overlay-title">{currentTitle()}</span>1992        <div class="peach-overlay-spacer"></div>1993      </div>19941995      <Switch fallback={renderHome()}>1996        <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1997        <Match when={activeSection() === "vault"}>{renderVault()}</Match>1998        <Match when={activeSection() === "sync"}>{renderSync()}</Match>1999        <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2000        <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2001        <Match when={activeSection() === "license"}>{renderLicense()}</Match>2002        <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2003        <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>2004      </Switch>2005    </div>2006  );2007}