Settings.tsx

78.1 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?: (mode: "initiate" | "accept") => 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 formatHistoryTime(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 [showChangePw, setShowChangePw] = createSignal(false);200  const [changePwCurrent, setChangePwCurrent] = createSignal("");201  const [changePwNew, setChangePwNew] = createSignal("");202  const [changePwConfirm, setChangePwConfirm] = createSignal("");203  const [changePwSaving, setChangePwSaving] = createSignal(false);204  const [changePwError, setChangePwError] = createSignal<string | null>(null);205  const [changePwSuccess, setChangePwSuccess] = createSignal(false);206207  const [duressEnabled, setDuressEnabled] = createSignal(false);208  const [showDuressSetup, setShowDuressSetup] = createSignal(false);209  const [duressPassword, setDuressPassword] = createSignal("");210  const [duressConfirm, setDuressConfirm] = createSignal("");211  const [duressMainPassword, setDuressMainPassword] = createSignal("");212  const [duressSaving, setDuressSaving] = createSignal(false);213  const [duressError, setDuressError] = createSignal<string | null>(null);214  const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);215216  const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);217218  // Mobile-only state219  function getAutofillPlugin() {220    try {221      return registerPlugin<{222        isEnabledAsAutofillService(): Promise<{ enabled: boolean }>;223        openAutofillSettings(): Promise<void>;224      }>("Autofill");225    } catch {226      return null;227    }228  }229  const AutofillPlugin = getAutofillPlugin();230  const [autofillEnabled, setAutofillEnabled] = createSignal(false);231  const [biometricEnabled, setBiometricEnabled] = createSignal(false);232  const isAndroid = Capacitor.getPlatform() === "android";233  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);234  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);235236  const [fbCategory, setFbCategory] = createSignal("Bug report");237  const [fbMessage, setFbMessage] = createSignal("");238  const [fbSubmitting, setFbSubmitting] = createSignal(false);239  const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);240241  onMount(async () => {242    const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([243      sendMessage({ type: "GET_LICENSE" }),244      sendMessage({ type: "GET_SYNC_STATE" }),245      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),246      sendMessage({ type: "IS_DURESS_ENABLED" }),247      sendMessage({ type: "GET_SETTINGS" }),248      sendMessage({ type: "GET_LICENSE_KEY" }),249    ]);250251    // Vault history loads in the background — the Vault sub-view picks it up252    // when the user navigates in. Loading it lazily keeps Settings boot fast.253    void loadVaultHistory();254255    if ((licenseRes as { ok: boolean }).ok) {256      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);257    }258259    if ((syncRes as { ok: boolean }).ok) {260      const data = syncRes as { ok: boolean; data?: {261        pairedDevices?: PairedDevice[];262        trustedNetworks?: TrustedNetwork[];263        status?: SyncStatus;264        lastSyncAt?: number;265        isPro?: boolean;266      }};267      if (data.data) {268        setPairedDevices(data.data.pairedDevices ?? []);269        setTrustedNetworks(data.data.trustedNetworks ?? []);270        setSyncStatus(data.data.status ?? "offline");271        setLastSyncAt(data.data.lastSyncAt);272        setSyncProStatus(data.data.isPro ?? false);273      }274    }275276    if ((passkeyRes as { ok: boolean }).ok) {277      setPasskeyState(278        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null279      );280    }281282    if ((duressRes as { ok: boolean }).ok) {283      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);284    }285286    if ((settingsRes as { ok: boolean }).ok) {287      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;288      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);289    }290291    if ((keyRes as { ok: boolean }).ok) {292      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);293    }294295    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });296    if ((entriesRes as { ok: boolean }).ok) {297      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];298      setCanaryEntries(299        entries300          .filter((e) => e.canary?.webhookUrl)301          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))302      );303    }304305    // Android-only: check autofill and biometric status306    if (isAndroid) {307      if (AutofillPlugin) {308        const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();309        setAutofillEnabled(enabled);310      }311      const bio = await biometricAvailable();312      setBiometricEnabled(bio);313    }314315    chrome.runtime.onMessage.addListener((msg) => {316      if (msg.type === "SYNC_STATE_UPDATE") {317        setPairedDevices(msg.pairedDevices ?? []);318        setTrustedNetworks(msg.trustedNetworks ?? []);319        setSyncStatus(msg.status ?? "offline");320        setLastSyncAt(msg.lastSyncAt);321        setSyncProStatus(msg.isPro ?? false);322      }323    });324  });325326  async function handleChangePassword() {327    setChangePwError(null);328    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }329    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }330    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }331    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }332    setChangePwSaving(true);333    const res = await sendMessage({334      type: "CHANGE_PASSWORD",335      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },336    }) as { ok: boolean; error?: string };337    if (res.ok) {338      setChangePwSuccess(true);339      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");340      setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);341    } else {342      setChangePwError(res.error ?? "failed to change password");343    }344    setChangePwSaving(false);345  }346347  const [copied, setCopied] = createSignal(false);348349  async function copyLicenseKey(key: string) {350    await navigator.clipboard.writeText(key);351    setCopied(true);352    setTimeout(() => setCopied(false), 2000);353  }354355  async function handleActivateLicense() {356    const key = licenseKeyInput().trim();357    if (!key) return;358    setActivatingLicense(true);359    setLicenseError(null);360    setLicenseSuccess(null);361    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });362    setActivatingLicense(false);363    if ((res as { ok: boolean }).ok) {364      const data = (res as { ok: boolean; data?: LicenseInfo }).data;365      if (data?.valid) {366        setLicenseInfo(data);367        setLicenseKey(key);368        setLicenseSuccess("License activated!");369        setLicenseKeyInput("");370      } else {371        setLicenseError("Invalid license key");372      }373    } else {374      setLicenseError("Activation failed");375    }376  }377378  async function handleTogglePasskeys() {379    const current = passkeyState();380    if (!current || !current.supported) return;381    setPasskeySaving(true);382    const res = await sendMessage({383      type: "SET_PASSKEY_PROVIDER_ENABLED",384      payload: { enabled: !current.enabled },385    });386    setPasskeySaving(false);387    if ((res as { ok: boolean }).ok) {388      setPasskeyState(389        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null390      );391    }392  }393394  async function handleCheckAllBreaches() {395    setCheckingBreaches(true);396    setBreachResult(null);397    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });398    setCheckingBreaches(false);399    if ((res as { ok: boolean }).ok) {400      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};401      const breached = Object.values(data).filter((count) => count > 0).length;402      setBreachResult(403        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"404      );405      props.onBreachCheckDone();406    }407  }408409  async function handleSyncNow() {410    if (syncStatus() === "not_trusted") return;411    setSyncingNow(true);412    setSyncFeedback(null);413    const res = await sendMessage({ type: "TRIGGER_SYNC" });414    const ok = (res as { ok: boolean }).ok;415    if (ok) {416      const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;417      const count = data?.syncedCount ?? 0;418      setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });419      setLastSyncAt(Date.now());420    } else {421      const error = (res as { error?: string }).error;422      const map: Record<string, string> = {423        no_paired_devices: "No paired devices",424        no_devices_reachable: "No devices found",425        vault_locked: "Vault is locked",426        pro_required: "Sync requires Pro",427      };428      setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });429    }430    setTimeout(() => {431      setSyncingNow(false);432      setSyncFeedback(null);433    }, 3500);434  }435436  async function handleRemoveDevice(deviceId: string) {437    await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });438  }439440  async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {441    await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });442  }443444  async function handleMarkNetworkTrusted() {445    await sendMessage({ type: "MARK_NETWORK_TRUSTED" });446  }447448  const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);449  const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);450451  async function handleResetSyncIdentity() {452    setResetIdentityRunning(true);453    const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });454    setResetIdentityRunning(false);455    if ((res as { ok: boolean }).ok) {456      setPairedDevices([]);457      setResetIdentityConfirm(false);458    }459  }460461  function formatRelativeTime(timestamp?: number): string {462    if (!timestamp) return "Never";463    const diff = Date.now() - timestamp;464    const minutes = Math.floor(diff / 60000);465    const hours = Math.floor(diff / 3600000);466    const days = Math.floor(diff / 86400000);467    if (minutes < 1) return "Just now";468    if (minutes < 60) return `${minutes}m ago`;469    if (hours < 24) return `${hours}h ago`;470    return `${days}d ago`;471  }472473  function getStatusColor(status: SyncStatus): string {474    switch (status) {475      case "synced": return "var(--peach-sync-ok)";476      case "syncing": return "var(--peach-sync-busy)";477      case "no_peers": return "var(--peach-sync-off)";478      case "not_trusted": return "var(--peach-sync-err)";479      case "offline": return "var(--peach-sync-off)";480      default: return "var(--peach-sync-off)";481    }482  }483484  function getStatusLabel(status: SyncStatus): string {485    switch (status) {486      case "synced": return "Synced";487      case "syncing": return "Syncing";488      case "no_peers": return "No peers visible";489      case "not_trusted": return "Not on a trusted network";490      case "offline": return "Offline";491      default: return "Unknown";492    }493  }494495  function isCurrentNetworkTrusted(): boolean {496    return trustedNetworks().length > 0;497  }498499  async function handleSetupDuress() {500    const mainPw = duressMainPassword().trim();501    const duressPw = duressPassword().trim();502    const confirmPw = duressConfirm().trim();503504    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }505    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }506    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }507    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }508509    setDuressSaving(true);510    setDuressError(null);511512    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };513    if (!verifyRes.ok || !verifyRes.data) {514      setDuressError("incorrect main password");515      setDuressSaving(false);516      return;517    }518519    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };520    setDuressSaving(false);521    if (res.ok) {522      setDuressEnabled(true);523      setShowDuressSetup(false);524      setDuressPassword("");525      setDuressConfirm("");526      setDuressMainPassword("");527      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");528      setTimeout(() => setDuressSuccess(null), 5000);529    } else {530      setDuressError(res.error ?? "failed to set duress password");531    }532  }533534  async function handleClearDeclinedCards() {535    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });536    setDeclinedCardCount(0);537  }538539  async function handleCountDuplicates() {540    setDedupRunning(true);541    setDedupDone(null);542    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };543    setDedupRunning(false);544    if (res.ok) setDedupCount(res.data ?? 0);545  }546547  async function handleDeduplicate() {548    setDedupRunning(true);549    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };550    setDedupRunning(false);551    if (res.ok) {552      setDedupDone(res.data ?? 0);553      setDedupCount(0);554    }555  }556557  async function handleDisableDuress() {558    const mainPw = duressMainPassword().trim();559    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }560561    setDuressSaving(true);562    setDuressError(null);563564    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };565    if (!verifyRes.ok || !verifyRes.data) {566      setDuressError("incorrect main password");567      setDuressSaving(false);568      return;569    }570571    const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };572    setDuressSaving(false);573    if (res.ok) {574      setDuressEnabled(false);575      setShowDuressSetup(false);576      setDuressMainPassword("");577      setDuressSuccess("Duress password disabled.");578      setTimeout(() => setDuressSuccess(null), 4000);579    } else {580      setDuressError(res.error ?? "failed to disable duress password");581    }582  }583584  async function handleSubmitFeedback() {585    if (!fbMessage().trim() || fbSubmitting()) return;586    setFbSubmitting(true);587    setFbResult(null);588    const res = await sendMessage({589      type: "SUBMIT_FEEDBACK",590      payload: {591        category: fbCategory(),592        message: fbMessage().trim(),593        version,594        browser: navigator.userAgent,595      },596    }) as { ok: boolean; error?: string };597    setFbSubmitting(false);598    if (res.ok) {599      setFbResult("success");600      setFbMessage("");601      setTimeout(() => setFbResult(null), 4000);602    } else {603      setFbResult("error");604    }605  }606607  function handleHeaderBack() {608    if (activeSection() !== null) {609      setActiveSection(null);610    } else {611      props.onBack();612    }613  }614615  function currentTitle(): string {616    const s = activeSection();617    if (s === null) return "settings";618    if (s === "license") return "account";619    return s;620  }621622  function renderCatRow(opts: {623    section: SectionName;624    name: string;625    desc: string;626    icon: () => any;627    pro?: boolean;628  }) {629    return (630      <button631        class="peach-settings-cat-row"632        onClick={() => setActiveSection(opts.section)}633      >634        <div class="peach-settings-cat-icon">{opts.icon()}</div>635        <div class="peach-settings-cat-content">636          <span class="peach-settings-cat-name">637            {opts.name}638            <Show when={opts.pro && !isPro()}>639              <span class="peach-pro-mark" aria-label="Pro feature">✦</span>640            </Show>641          </span>642          <span class="peach-settings-cat-desc">{opts.desc}</span>643        </div>644        <svg645          class="peach-settings-cat-chevron"646          width="6" height="10" viewBox="0 0 6 10" fill="none"647          stroke="currentColor" stroke-width="1.1"648          stroke-linecap="round" stroke-linejoin="round"649        >650          <path d="M1 1l4 4-4 4" />651        </svg>652      </button>653    );654  }655656  const iconShield = () => (657    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">658      <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>659    </svg>660  );661  const iconKey = () => (662    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">663      <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>664    </svg>665  );666  const iconLock = () => (667    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">668      <rect x="3" y="7" width="10" height="7" rx="1.5"/>669      <path d="M6 7V5a3 3 0 016 0v2"/>670    </svg>671  );672  const iconSync = () => (673    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">674      <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>675      <path d="M14 6l-2 2-2-2"/>676      <path d="M2 10l2-2 2 2"/>677    </svg>678  );679  const iconSun = () => (680    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">681      <circle cx="8" cy="8" r="3"/>682      <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"/>683    </svg>684  );685  const iconKeys = () => (686    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">687      <rect x="2" y="5" width="5" height="4" rx="1"/>688      <rect x="9" y="7" width="5" height="4" rx="1"/>689      <path d="M7 7h2m-2 2h2"/>690    </svg>691  );692  const iconBubble = () => (693    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">694      <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>695    </svg>696  );697698  function renderHome() {699    return (700      <div class="peach-settings-body">701        <p class="peach-settings-nav-label">PROTECTION</p>702        {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}703        {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}704705        <p class="peach-settings-nav-label">VAULT</p>706        {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}707        {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}708709        <p class="peach-settings-nav-label">PREFERENCES</p>710        {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}711        {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}712        <Show when={isAndroid}>713          <p class="peach-settings-nav-label">MOBILE</p>714          {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}715        </Show>716717        <p class="peach-settings-nav-label">SUPPORT</p>718        {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}719720        <div class="peach-settings-footer">721          <span>Peach {version}</span>722          <span class="peach-divider"></span>723          <span>Browser extension</span>724        </div>725      </div>726    );727  }728729  function renderSecurity() {730    return (731      <div class="peach-settings-body">732        <div class="peach-settings-group">733          <p class="peach-settings-sub-label">LOCKING</p>734          <div class="peach-settings-row">735            <span>Lock on close</span>736            <button737              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}738              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}739              aria-label="Toggle lock on close"740            >741              <span class="peach-toggle-knob"></span>742            </button>743          </div>744          <div class="peach-settings-row">745            <span>Auto-lock after</span>746            <select747              class="peach-settings-select"748              value={String(props.autoLockMinutes)}749              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}750            >751              <option value="1">1 minute</option>752              <option value="5">5 minutes</option>753              <option value="15">15 minutes</option>754              <option value="30">30 minutes</option>755              <option value="0">Never</option>756            </select>757          </div>758759          <p class="peach-settings-sub-label">BREACH PROTECTION</p>760          <div class="peach-settings-row">761            <span>Breach monitoring</span>762            <button763              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}764              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}765              aria-label="Toggle breach monitoring"766            >767              <span class="peach-toggle-knob"></span>768            </button>769          </div>770          <div class="peach-settings-row peach-settings-row--meta">771            <span>Check all passwords</span>772            <button773              class="peach-btn peach-btn--subtle peach-btn--sm"774              onClick={() => void handleCheckAllBreaches()}775              disabled={checkingBreaches()}776            >777              {checkingBreaches() ? "checking..." : "run"}778            </button>779          </div>780          <Show when={breachResult()}>781            <div class="peach-settings-row peach-settings-row--meta">782              <span>Last check</span>783              <span class="peach-settings-value">{breachResult()}</span>784            </div>785          </Show>786787          <p class="peach-settings-sub-label">MASTER PASSWORD</p>788          <div class="peach-settings-row peach-settings-row--meta">789            <span>Master password</span>790            <button791              class="peach-btn peach-btn--subtle peach-btn--sm"792              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}793            >794              {showChangePw() ? "cancel" : "change"}795            </button>796          </div>797          <Show when={showChangePw()}>798            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">799              <input800                class="peach-settings-input"801                type="password"802                placeholder="current password"803                value={changePwCurrent()}804                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}805                autocomplete="current-password"806              />807              <input808                class="peach-settings-input"809                type="password"810                placeholder="new password (min 12 chars)"811                value={changePwNew()}812                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}813                autocomplete="new-password"814              />815              <input816                class="peach-settings-input"817                type="password"818                placeholder="confirm new password"819                value={changePwConfirm()}820                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}821                autocomplete="new-password"822                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}823              />824              <Show when={changePwError()}>825                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>826              </Show>827              <Show when={changePwSuccess()}>828                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>829              </Show>830              <button831                class="peach-btn peach-btn--accent peach-btn--sm"832                style="align-self:flex-start"833                onClick={() => void handleChangePassword()}834                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}835              >836                {changePwSaving() ? "saving…" : "save new password"}837              </button>838            </div>839          </Show>840841          <p class="peach-settings-sub-label">DURESS PASSWORD</p>842          <div class="peach-settings-row peach-settings-row--top">843            <div class="peach-settings-row-main">844              <span>Duress password</span>845              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">846                If entered under coercion, shows a decoy empty vault instead of your real one.847              </span>848            </div>849            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>850              {duressEnabled() ? "on" : "off"}851            </div>852          </div>853854          <Show when={duressEnabled() && !showDuressSetup()}>855            <div class="peach-settings-row peach-settings-row--meta">856              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">857                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.858              </span>859            </div>860          </Show>861862          <Show when={duressSuccess()}>863            <div class="peach-settings-row peach-settings-row--meta">864              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>865            </div>866          </Show>867868          <Show when={!showDuressSetup()}>869            <div class="peach-settings-row peach-settings-row--meta">870              <button871                class="peach-btn peach-btn--subtle peach-btn--sm"872                onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}873              >874                {duressEnabled() ? "Change / disable" : "Set up"}875              </button>876            </div>877          </Show>878879          <Show when={showDuressSetup()}>880            <div class="peach-duress-form">881              <label class="peach-label">main password (to confirm)</label>882              <input883                class="peach-settings-input"884                type="password"885                placeholder="your current master password"886                autocomplete="current-password"887                value={duressMainPassword()}888                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}889              />890              <Show when={!duressEnabled()}>891                <label class="peach-label">new duress password</label>892                <input893                  class="peach-settings-input"894                  type="password"895                  placeholder="duress password (min 8 chars)"896                  autocomplete="new-password"897                  value={duressPassword()}898                  onInput={(e) => setDuressPassword(e.currentTarget.value)}899                />900                <label class="peach-label">confirm duress password</label>901                <input902                  class="peach-settings-input"903                  type="password"904                  placeholder="confirm duress password"905                  autocomplete="new-password"906                  value={duressConfirm()}907                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}908                />909              </Show>910911              <Show when={duressError()}>912                <p class="peach-hint peach-hint-error">{duressError()}</p>913              </Show>914915              <div class="peach-duress-actions">916                <button917                  class="peach-btn peach-btn--subtle peach-btn--sm"918                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}919                >920                  cancel921                </button>922                <Show when={duressEnabled()}>923                  <button924                    class="peach-btn peach-btn--ghost peach-btn--sm"925                    style="color:var(--peach-breach)"926                    onClick={() => void handleDisableDuress()}927                    disabled={duressSaving()}928                  >929                    {duressSaving() ? "disabling..." : "disable duress"}930                  </button>931                </Show>932                <Show when={!duressEnabled()}>933                  <button934                    class="peach-btn peach-btn--accent peach-btn--sm"935                    onClick={() => void handleSetupDuress()}936                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}937                  >938                    {duressSaving() ? "saving..." : "save"}939                  </button>940                </Show>941              </div>942            </div>943          </Show>944945          <p class="peach-settings-sub-label">CANARY ENTRIES</p>946          <div class="peach-settings-row peach-settings-row--top">947            <div class="peach-settings-row-main">948              <span>Canary entries</span>949              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">950                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.951              </span>952            </div>953            <span class="peach-settings-value" style="flex-shrink:0">954              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}955            </span>956          </div>957          <Show when={canaryEntries().length > 0}>958            <For each={canaryEntries()}>959              {(entry) => (960                <div class="peach-settings-row peach-settings-row--meta">961                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>962                  <button963                    class="peach-btn peach-btn--subtle peach-btn--sm"964                    onClick={() => void handleTestCanary(entry)}965                    disabled={canaryTestingId() === entry.id}966                    style="flex-shrink:0"967                  >968                    {canaryTestingId() === entry.id969                      ? "sending…"970                      : canaryTestResult()?.id === entry.id971                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")972                        : "test"}973                  </button>974                </div>975              )}976            </For>977          </Show>978979          <p class="peach-settings-sub-label">PASSKEYS</p>980          <div class="peach-settings-row peach-settings-row--top">981            <div class="peach-settings-row-main">982              <span>Intercept passkey requests</span>983              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">984                Peach can handle passkey sign-in and creation on supported Chromium browsers.985              </span>986            </div>987            <button988              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}989              onClick={() => void handleTogglePasskeys()}990              aria-label="Toggle passkey interception"991              disabled={!passkeyState()?.supported || passkeySaving()}992            >993              <span class="peach-toggle-knob"></span>994            </button>995          </div>996          <div class="peach-settings-row peach-settings-row--meta">997            <span>Status</span>998            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>999          </div>1000          <div class="peach-settings-row peach-settings-row--meta">1001            <span>Browser</span>1002            <span class="peach-settings-value">1003              {passkeyState()?.browserLabel ?? "Unknown"}1004              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1005            </span>1006          </div>1007          <div class="peach-info-notice" style="margin-top:8px">1008            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1009          </div>1010          <Show when={passkeyState()?.conflict}>1011            <div class="peach-settings-row peach-settings-row--meta">1012              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1013            </div>1014          </Show>1015        </div>1016      </div>1017    );1018  }10191020  function renderVault() {1021    return (1022      <div class="peach-settings-body">1023        <div class="peach-settings-group">1024          <p class="peach-settings-sub-label">ENTRIES</p>1025          <div class="peach-settings-row peach-settings-row--meta">1026            <span>Entries</span>1027            <span class="peach-settings-value">{props.entriesCount}</span>1028          </div>1029          <div class="peach-settings-row peach-settings-row--meta">1030            <span>Duplicates</span>1031            <Show when={dedupDone() !== null} fallback={1032              <button1033                class="peach-btn peach-btn--subtle peach-btn--sm"1034                onClick={() => void handleCountDuplicates()}1035                disabled={dedupRunning()}1036              >1037                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1038              </button>1039            }>1040              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1041                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1042              </span>1043            </Show>1044          </div>1045          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1046            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1047              <div class="peach-restore-warning">1048                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1049              </div>1050              <div style="display:flex;gap:6px">1051                <button1052                  class="peach-btn peach-btn--subtle peach-btn--sm"1053                  onClick={() => setDedupCount(null)}1054                >1055                  cancel1056                </button>1057                <button1058                  class="peach-btn peach-btn--accent peach-btn--sm"1059                  onClick={() => void handleDeduplicate()}1060                  disabled={dedupRunning()}1061                >1062                  {dedupRunning() ? "removing..." : "remove duplicates"}1063                </button>1064              </div>1065            </div>1066          </Show>10671068          <p class="peach-settings-sub-label">MAINTENANCE</p>1069          <div class="peach-settings-row peach-settings-row--meta">1070            <span>Trash auto-delete</span>1071            <select1072              class="peach-settings-select"1073              value={String(props.trashRetentionDays)}1074              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1075            >1076              <option value="7">7 days</option>1077              <option value="30">30 days</option>1078              <option value="90">90 days</option>1079              <option value="0">Never</option>1080            </select>1081          </div>1082          <div class="peach-settings-row peach-settings-row--meta">1083            <span>Vault Health</span>1084            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1085              open1086            </button>1087          </div>1088          <Show when={declinedCardCount() > 0}>1089            <div class="peach-settings-row peach-settings-row--meta">1090              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1091              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1092                Clear1093              </button>1094            </div>1095          </Show>10961097          <p class="peach-settings-sub-label">DATA</p>1098          <div class="peach-settings-row peach-settings-row--meta">1099            <span>Import passwords</span>1100            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1101              open1102            </button>1103          </div>1104          <div class="peach-settings-row peach-settings-row--meta">1105            <span>Export backup</span>1106            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1107              open1108            </button>1109          </div>11101111          <p class="peach-settings-sub-label">VAULT HISTORY</p>1112          <Show when={historyLoading()}>1113            <p class="peach-settings-hint">Loading…</p>1114          </Show>1115          <Show when={!historyLoading() && historySnapshots().length === 0}>1116            <p class="peach-settings-hint">1117              No snapshots yet. Peach saves automatically before every change —1118              saves, syncs, and imports.1119            </p>1120          </Show>1121          <Show when={!historyLoading() && historySnapshots().length > 0}>1122            <p class="peach-settings-hint">1123              Restoring locks your vault. Unlock with the password that1124              encrypted that snapshot to confirm.1125            </p>1126            <div class="peach-history-list">1127              <For each={historySnapshots()}>1128                {(snap) => (1129                  <div class="peach-history-row">1130                    <div class="peach-history-meta">1131                      <span class="peach-history-time">1132                        {formatHistoryTime(snap.savedAt)}1133                      </span>1134                      <span class="peach-history-sub">1135                        {[1136                          snap.entryCount !== null1137                            ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1138                            : null,1139                          reasonLabel(snap.reason),1140                        ]1141                          .filter(Boolean)1142                          .join(" · ")}1143                      </span>1144                    </div>1145                    <button1146                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1147                        historyRestoring() === snap.id ? " is-busy" : ""1148                      }`}1149                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1150                      disabled={historyRestoring() !== null}1151                    >1152                      <Show when={historyRestoring() === snap.id} fallback="Restore">1153                        …1154                      </Show>1155                    </button>1156                  </div>1157                )}1158              </For>1159            </div>1160            <p class="peach-history-footer">1161              {historySnapshots().length} of 10 snapshots1162            </p>1163          </Show>11641165          <div class="peach-danger-card">1166            <div class="peach-danger-card-head">1167              <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1168              <h3 class="peach-danger-card-title">Replace vault from backup</h3>1169            </div>1170            <p class="peach-danger-card-copy">1171              Wipes the current vault and restores from a backup file.1172              Recoverable only if your backup is good.1173            </p>1174            <button1175              class="peach-danger-card-button"1176              onClick={() => props.onOpenFileImport?.()}1177            >1178              Choose backup file1179            </button>1180          </div>11811182          <Show when={props.onOpenPanic}>1183            <div class="peach-danger-card peach-danger-card--hazard">1184              <div class="peach-danger-card-head">1185                <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1186                <h3 class="peach-danger-card-title">Destroy vault</h3>1187              </div>1188              <p class="peach-danger-card-copy">1189                Permanently wipes every entry, setting, and license on this1190                device. There is no recovery.1191              </p>1192              <button1193                class="peach-danger-card-button"1194                onClick={() => props.onOpenPanic?.()}1195              >1196                Begin destruction1197              </button>1198            </div>1199          </Show>1200        </div>1201      </div>1202    );1203  }12041205  function renderSync() {1206    if (!isPro()) {1207      return (1208        <div class="peach-settings-body">1209          <div class="peach-pro-upgrade">1210            <div class="peach-pro-upgrade-hero">1211              <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1212                <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1213                <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1214                <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"/>1215                <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"/>1216                <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1217                <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1218                <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1219              </svg>1220              <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1221              <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1222            </div>12231224            <div class="peach-pro-features">1225              <div class="peach-pro-feature">1226                <span class="peach-pro-feature-mark">✦</span>1227                <div class="peach-pro-feature-text">1228                  <span class="peach-pro-feature-name">Always in sync</span>1229                  <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1230                </div>1231              </div>1232              <div class="peach-pro-feature">1233                <span class="peach-pro-feature-mark">✦</span>1234                <div class="peach-pro-feature-text">1235                  <span class="peach-pro-feature-name">No cloud. No relay.</span>1236                  <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1237                </div>1238              </div>1239              <div class="peach-pro-feature">1240                <span class="peach-pro-feature-mark">✦</span>1241                <div class="peach-pro-feature-text">1242                  <span class="peach-pro-feature-name">Tailscale supported</span>1243                  <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1244                </div>1245              </div>1246              <div class="peach-pro-feature">1247                <span class="peach-pro-feature-mark">✦</span>1248                <div class="peach-pro-feature-text">1249                  <span class="peach-pro-feature-name">Zero friction after setup</span>1250                  <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1251                </div>1252              </div>1253            </div>12541255            <button1256              class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1257              onClick={() => setActiveSection("license")}1258            >1259              Upgrade to Pro1260            </button>1261            <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1262              Already have a key? Enter it →1263            </button>1264          </div>1265        </div>1266      );1267    }12681269    return (1270      <div class="peach-settings-body">1271        <div class="peach-settings-group">1272          <div class="peach-settings-row">1273            <span>Status</span>1274            <span class="peach-settings-value peach-settings-value--status">1275              <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1276              {getStatusLabel(syncStatus())}1277            </span>1278          </div>1279          <div class="peach-settings-row peach-settings-row--meta">1280            <span>Last sync</span>1281            <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1282          </div>1283          <div class="peach-settings-row peach-settings-row--meta">1284            <button1285              class="peach-btn peach-btn--subtle peach-btn--sm"1286              onClick={() => void handleSyncNow()}1287              disabled={syncStatus() === "not_trusted" || syncingNow()}1288            >1289              <Switch>1290                <Match when={syncingNow()}>Syncing…</Match>1291                <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1292                <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1293                <Match when={true}>Sync now</Match>1294              </Switch>1295            </button>1296          </div>1297          <p class="peach-settings-hint">1298            If you edit on multiple devices before syncing, the most recently1299            changed device wins. Use Vault History above to recover older1300            versions if needed.1301          </p>13021303          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1304            <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1305            {pairedDevices().map((device) => (1306              <div class="peach-sync-device-row">1307                <div class="peach-sync-device-info">1308                  <span class="peach-sync-device-name">{device.displayName}</span>1309                  <span class="peach-sync-device-meta">1310                    Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}1311                  </span>1312                </div>1313                <button1314                  class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1315                  onClick={() => void handleRemoveDevice(device.deviceId)}1316                >1317                  Remove1318                </button>1319              </div>1320            ))}1321            <div class="peach-settings-actions--row">1322              <button1323                class="peach-btn peach-btn--subtle peach-btn--sm"1324                onClick={() => props.onOpenSyncPairing?.("initiate")}1325              >1326                Pair a new device1327              </button>1328              <button1329                class="peach-btn peach-btn--subtle peach-btn--sm"1330                onClick={() => props.onOpenSyncPairing?.("accept")}1331              >1332                Pair with another device1333              </button>1334            </div>1335          </div>13361337          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1338            <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1339            {trustedNetworks().filter(n => !n.isTailscale).map((network) => (1340              <div class="peach-sync-device-row">1341                <span class="peach-sync-device-name">{network.ssid ?? "Unknown network"}</span>1342                <button1343                  class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1344                  onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1345                >1346                  Remove1347                </button>1348              </div>1349            ))}1350            <div class="peach-settings-actions--row">1351              <Show when={!isCurrentNetworkTrusted()}>1352                <button1353                  class="peach-btn peach-btn--subtle peach-btn--sm"1354                  onClick={() => void handleMarkNetworkTrusted()}1355                >1356                  Mark current network as trusted1357                </button>1358              </Show>1359            </div>1360            <span class="peach-sync-device-meta">1361              Tailscale is always trusted1362            </span>1363          </div>13641365          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1366            <span class="peach-sync-section-label">Advanced</span>1367            <Show1368              when={resetIdentityConfirm()}1369              fallback={1370                <div class="peach-sync-advanced-row">1371                  <div class="peach-sync-advanced-info">1372                    <span class="peach-sync-advanced-name">Reset sync identity</span>1373                    <span class="peach-sync-device-meta">1374                      Generates a new keypair and unpairs every device.1375                    </span>1376                  </div>1377                  <button1378                    class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1379                    onClick={() => setResetIdentityConfirm(true)}1380                  >1381                    Reset1382                  </button>1383                </div>1384              }1385            >1386              <div class="peach-sync-reset-confirm">1387                <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1388                <p class="peach-sync-reset-confirm-body">1389                  This will unpair all devices. You will need to pair them again from scratch.1390                </p>1391                <div class="peach-settings-actions--row">1392                  <button1393                    class="peach-btn peach-btn--ghost peach-btn--sm"1394                    onClick={() => setResetIdentityConfirm(false)}1395                    disabled={resetIdentityRunning()}1396                  >1397                    Cancel1398                  </button>1399                  <button1400                    class="peach-btn peach-btn--danger peach-btn--sm"1401                    onClick={() => void handleResetSyncIdentity()}1402                    disabled={resetIdentityRunning()}1403                  >1404                    {resetIdentityRunning() ? "Resetting..." : "Reset"}1405                  </button>1406                </div>1407              </div>1408            </Show>1409          </div>1410        </div>1411      </div>1412    );1413  }14141415  function renderAppearance() {1416    const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1417      { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1418      { id: "cormorant", label: "Cormorant", preview: "Aa" },1419      { id: "inter", label: "Inter", preview: "Aa" },1420      { id: "system", label: "System", preview: "Aa" },1421    ];1422    const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1423      { id: "small", label: "Small" },1424      { id: "medium", label: "Default" },1425      { id: "large", label: "Large" },1426    ];1427    return (1428      <div class="peach-settings-body">1429        <div class="peach-settings-group">1430          <p class="peach-settings-sub-label">THEME</p>1431          <div class="peach-settings-row peach-settings-row--meta">1432            <span>Theme</span>1433            <button1434              class="peach-btn peach-btn--subtle peach-btn--sm"1435              onClick={() =>1436                props.onThemeChange(props.theme === "dark" ? "light" : "dark")1437              }1438            >1439              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1440            </button>1441          </div>14421443          <p class="peach-settings-sub-label">ACCENT</p>1444          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1445            <span>Accent</span>1446            <div class="peach-accent-picker">1447              <For each={ACCENT_COLORS}>1448                {(color) => (1449                  <button1450                    class="peach-accent-swatch"1451                    classList={{ "is-active": props.accentColor === color.name }}1452                    style={{ background: color.hex }}1453                    onClick={() => void props.onAccentChange(color.name)}1454                    aria-label={color.name}1455                    title={color.name}1456                  />1457                )}1458              </For>1459            </div>1460          </div>14611462          <p class="peach-settings-sub-label">DISPLAY FONT</p>1463          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1464            <span>Font</span>1465            <div class="peach-font-picker">1466              <For each={fontOptions}>1467                {(opt) => (1468                  <button1469                    class="peach-font-swatch"1470                    classList={{ "is-active": props.displayFont === opt.id }}1471                    data-font-preview={opt.id}1472                    onClick={() => void props.onDisplayFontChange(opt.id)}1473                    aria-label={opt.label}1474                    title={opt.label}1475                  >1476                    <span class="peach-font-swatch-glyph">{opt.preview}</span>1477                    <span class="peach-font-swatch-name">{opt.label}</span>1478                  </button>1479                )}1480              </For>1481            </div>1482          </div>14831484          <p class="peach-settings-sub-label">TEXT SIZE</p>1485          <div class="peach-settings-row peach-settings-row--meta">1486            <span>Size</span>1487            <div class="peach-pill-group">1488              <For each={sizeOptions}>1489                {(opt) => (1490                  <button1491                    class="peach-btn peach-btn--ghost peach-btn--sm"1492                    classList={{ "is-active": props.textSize === opt.id }}1493                    onClick={() => void props.onTextSizeChange(opt.id)}1494                  >1495                    {opt.label}1496                  </button>1497                )}1498              </For>1499            </div>1500          </div>15011502          <p class="peach-settings-sub-label">MOTION</p>1503          <div class="peach-settings-row peach-settings-row--meta">1504            <span>Reduce motion</span>1505            <button1506              class="peach-btn peach-btn--subtle peach-btn--sm"1507              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1508            >1509              {props.reduceMotion ? "On" : "Off"}1510            </button>1511          </div>1512        </div>1513      </div>1514    );1515  }15161517  function renderShortcuts() {1518    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1519    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15201521    function startCapture(action: ShortcutAction) {1522      setConflictMsg(null);1523      setCapturingAction(action);1524    }1525    function cancelCapture() {1526      setCapturingAction(null);1527      setConflictMsg(null);1528    }1529    function commitBinding(action: ShortcutAction, binding: Keybinding) {1530      const conflict = findConflict(props.shortcuts, binding, action);1531      if (conflict) {1532        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1533        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1534        return;1535      }1536      const next = { ...props.shortcuts, [action]: binding };1537      setCapturingAction(null);1538      setConflictMsg(null);1539      void props.onShortcutsChange(next);1540    }1541    function resetAction(action: ShortcutAction) {1542      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1543      void props.onShortcutsChange(next);1544    }1545    function resetAll() {1546      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1547    }15481549    // Capture-mode key handler — wired via ref onto the chip element so it1550    // catches the next non-modifier keypress and ignores other bindings.1551    function handleCaptureKey(e: KeyboardEvent) {1552      e.preventDefault();1553      e.stopPropagation();1554      if (e.key === "Escape") { cancelCapture(); return; }1555      const binding = keybindingFromEvent(e);1556      if (!binding) return; // bare modifier — keep waiting1557      const action = capturingAction();1558      if (action) commitBinding(action, binding);1559    }15601561    return (1562      <div class="peach-settings-body">1563        <div class="peach-settings-group peach-shortcuts-group">1564          <p class="peach-settings-sub-label">ENTRY VIEW</p>1565          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1566            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1567          </For>1568        </div>15691570        <div class="peach-settings-group peach-shortcuts-group">1571          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1572          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1573            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1574          </For>1575        </div>15761577        <Show when={conflictMsg()}>1578          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1579        </Show>15801581        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1582          <div class="peach-settings-row peach-settings-row--meta">1583            <span>Inline autofill on page</span>1584            <a1585              class="peach-settings-link"1586              href="chrome://extensions/shortcuts"1587              onClick={(e) => {1588                e.preventDefault();1589                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1590              }}1591            >1592              ⌘⇧P — customize ↗1593            </a>1594          </div>1595          <button1596            class="peach-btn peach-btn--subtle peach-btn--sm"1597            style="align-self:flex-start"1598            onClick={resetAll}1599          >1600            Reset all to defaults1601          </button>1602        </div>1603      </div>1604    );16051606    function renderRow(1607      desc: typeof SHORTCUT_DESCRIPTORS[number],1608      capturing: () => ShortcutAction | null,1609      onStart: (a: ShortcutAction) => void,1610      onCancel: () => void,1611      onReset: (a: ShortcutAction) => void,1612      onKey: (e: KeyboardEvent) => void,1613    ) {1614      const isActive = () => capturing() === desc.action;1615      const binding = () => props.shortcuts[desc.action];1616      const isCustom = () => {1617        const b = binding();1618        const d = DEFAULT_BINDINGS[desc.action];1619        return b.key !== d.key1620          || Boolean(b.ctrl)  !== Boolean(d.ctrl)1621          || Boolean(b.meta)  !== Boolean(d.meta)1622          || Boolean(b.shift) !== Boolean(d.shift)1623          || Boolean(b.alt)   !== Boolean(d.alt);1624      };1625      return (1626        <div class="peach-shortcut-row">1627          <span class="peach-shortcut-row-label">{desc.label}</span>1628          <button1629            class="peach-shortcut-chip"1630            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1631            onClick={() => isActive() ? onCancel() : onStart(desc.action)}1632            onKeyDown={(e) => { if (isActive()) onKey(e); }}1633            ref={(el) => {1634              // Auto-focus when entering capture mode so the keydown listener catches the next press.1635              if (el) {1636                queueMicrotask(() => { if (isActive()) el.focus(); });1637              }1638            }}1639          >1640            {isActive() ? "Press a key…" : formatKeybinding(binding())}1641          </button>1642          <Show when={isCustom() && !isActive()}>1643            <button1644              class="peach-shortcut-reset"1645              title="Reset to default"1646              onClick={() => onReset(desc.action)}1647            >1648              ↺1649            </button>1650          </Show>1651        </div>1652      );1653    }1654  }16551656  const PRO_FEATURES: Array<{1657    feature: ProFeature;1658    name: string;1659    desc: string;1660  }> = [1661    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1662    { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1663    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1664    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1665    { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1666  ];16671668  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);16691670  const [showKeyEntry, setShowKeyEntry] = createSignal(false);1671  const [deactivating, setDeactivating] = createSignal(false);16721673  async function handleDeactivateLicense() {1674    setDeactivating(true);1675    await sendMessage({ type: "DEACTIVATE_LICENSE" });1676    setLicenseInfo(null);1677    await props.onLicenseChange?.();1678    setDeactivating(false);1679  }16801681  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1682    setCanaryTestingId(entry.id);1683    setCanaryTestResult(null);1684    const res = await sendMessage({1685      type: "FIRE_CANARY_PING",1686      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1687    }) as { ok: boolean };1688    setCanaryTestResult({ id: entry.id, ok: res.ok });1689    setCanaryTestingId(null);1690    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1691  }16921693  function renderLicense() {1694    async function onActivate() {1695      await handleActivateLicense();1696      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1697        await props.onLicenseChange?.();1698      }1699    }17001701    return (1702      <div class="peach-settings-body">1703        <Show1704          when={isPro()}1705          fallback={1706            <Show1707              when={showKeyEntry()}1708              fallback={1709                <div class="peach-pro-upgrade">1710                  <div class="peach-pro-upgrade-hero">1711                    <div class="peach-pro-upgrade-mark-wrap">1712                      <PeachMark class="peach-pro-upgrade-mark" />1713                    </div>1714                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1715                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1716                  </div>17171718                  <div class="peach-pro-features">1719                    <For each={PRO_FEATURES}>1720                      {(item) => (1721                        <div1722                          class="peach-pro-feature"1723                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1724                        >1725                          <span class="peach-pro-feature-mark">✦</span>1726                          <div class="peach-pro-feature-text">1727                            <span class="peach-pro-feature-name">{item.name}</span>1728                            <span class="peach-pro-feature-desc">{item.desc}</span>1729                          </div>1730                        </div>1731                      )}1732                    </For>1733                  </div>17341735                  <div class="peach-pro-upgrade-actions">1736                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1737                    <a1738                      href="https://peachpasswords.com/#pricing"1739                      target="_blank"1740                      rel="noopener noreferrer"1741                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1742                    >1743                      Get Peach Pro1744                    </a>1745                    <div class="peach-pro-key-divider"><span>or</span></div>1746                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1747                      Activate a license key1748                    </button>1749                  </div>1750                </div>1751              }1752            >1753              <div class="peach-pro-key-ceremony">1754                <LockRuneCanvas />1755                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1756                <input1757                  class="peach-underline-input peach-pro-key-input"1758                  type="text"1759                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1760                  value={licenseKeyInput()}1761                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1762                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1763                  autocomplete="off"1764                  spellcheck={false}1765                />1766                <Show when={licenseError()}>1767                  <p class="peach-pro-key-error">That key doesn't look right.</p>1768                </Show>1769                <button1770                  class="peach-btn peach-btn--accent peach-pro-key-activate"1771                  onClick={() => void onActivate()}1772                  disabled={activatingLicense() || !licenseKeyInput().trim()}1773                >1774                  {activatingLicense() ? "activating…" : "Activate"}1775                </button>1776                <button1777                  class="peach-pro-key-cancel"1778                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1779                >1780                  cancel1781                </button>1782              </div>1783            </Show>1784          }1785        >1786          <div class="peach-pro-active">1787            <div class="peach-pro-active-hero">1788              <div class="peach-pro-active-mark-wrap">1789                <PeachMark class="peach-pro-active-mark" />1790              </div>1791              <h2 class="peach-pro-active-title">Peach Pro</h2>1792              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1793            </div>17941795            <Show when={licenseKey()}>1796              <div class="peach-settings-license">1797                <span class="peach-settings-label">LICENSE KEY</span>1798                <div class="peach-settings-license-row">1799                  <span class="peach-settings-license-key">{licenseKey()!}</span>1800                  <button1801                    class="peach-settings-license-copy"1802                    onClick={() => copyLicenseKey(licenseKey()!)}1803                  >1804                    {copied() ? "✓" : "copy"}1805                  </button>1806                </div>1807              </div>1808            </Show>18091810            <div class="peach-pro-active-features">1811              <For each={PRO_FEATURES}>1812                {(item) => (1813                  <div class="peach-pro-active-feature">1814                    <span class="peach-pro-active-feature-mark">✦</span>1815                    <span class="peach-pro-active-feature-name">{item.name}</span>1816                  </div>1817                )}1818              </For>1819            </div>18201821            <button1822              class="peach-pro-deactivate"1823              onClick={() => void handleDeactivateLicense()}1824              disabled={deactivating()}1825            >1826              {deactivating() ? "Deactivating…" : "Deactivate"}1827            </button>1828          </div>1829        </Show>1830      </div>1831    );1832  }18331834  function renderFeedback() {1835    return (1836      <div class="peach-settings-body">1837        <div class="peach-settings-group peach-feedback-group">1838          <p class="peach-feedback-intro">1839            Help us improve Peach. Every report goes directly to our inbox.1840          </p>18411842          <p class="peach-settings-sub-label">TYPE</p>1843          <div class="peach-feedback-chips">1844            <For each={FEEDBACK_CATEGORIES}>1845              {(cat) => (1846                <button1847                  class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1848                  onClick={() => setFbCategory(cat)}1849                >1850                  {cat}1851                </button>1852              )}1853            </For>1854          </div>18551856          <p class="peach-settings-sub-label">MESSAGE</p>1857          <textarea1858            class="peach-feedback-textarea"1859            placeholder="Describe the issue or idea..."1860            value={fbMessage()}1861            onInput={(e) => setFbMessage(e.currentTarget.value)}1862          />18631864          <p class="peach-feedback-meta">1865            Version {version} · will be included automatically1866          </p>18671868          <Show when={fbResult() === "success"}>1869            <p class="peach-feedback-result peach-feedback-result--ok">1870              Sent — thank you1871            </p>1872          </Show>1873          <Show when={fbResult() === "error"}>1874            <p class="peach-feedback-result peach-feedback-result--err">1875              Failed to send — try again1876            </p>1877          </Show>18781879          <button1880            class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1881            onClick={() => void handleSubmitFeedback()}1882            disabled={fbSubmitting() || !fbMessage().trim()}1883          >1884            {fbSubmitting() ? "Sending..." : "Send feedback"}1885          </button>18861887          <p class="peach-feedback-anon">1888            Sent anonymously · no account required1889          </p>1890        </div>1891      </div>1892    );1893  }18941895  function renderMobile() {1896    return (1897      <div class="peach-settings-body">1898        <div class="peach-settings-group">1899          <p class="peach-settings-label">Autofill service</p>1900          <div class="peach-mobile-row">1901            <span class="peach-mobile-status">1902              {autofillEnabled() ? "Enabled" : "Disabled"}1903            </span>1904            <button1905              class="peach-btn peach-btn--subtle peach-btn--sm"1906              onClick={() => void AutofillPlugin?.openAutofillSettings()}1907            >1908              {autofillEnabled() ? "Change" : "Enable"}1909            </button>1910          </div>1911        </div>19121913        <div class="peach-settings-group">1914          <p class="peach-settings-label">Biometric unlock</p>1915          <div class="peach-mobile-row">1916            <span class="peach-mobile-status">1917              {biometricEnabled() ? "Available" : "Not available"}1918            </span>1919          </div>1920        </div>1921      </div>1922    );1923  }19241925  return (1926    <div class="peach-overlay-view ov-enter-right" style="display:flex">1927      <div class="peach-overlay-header">1928        <button1929          class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1930          onClick={handleHeaderBack}1931          aria-label="Back"1932        >1933          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1934            <path d="M6 1L1 6L6 11" />1935          </svg>1936        </button>1937        <span class="peach-overlay-title">{currentTitle()}</span>1938        <div class="peach-overlay-spacer"></div>1939      </div>19401941      <Switch fallback={renderHome()}>1942        <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1943        <Match when={activeSection() === "vault"}>{renderVault()}</Match>1944        <Match when={activeSection() === "sync"}>{renderSync()}</Match>1945        <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1946        <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1947        <Match when={activeSection() === "license"}>{renderLicense()}</Match>1948        <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1949        <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>1950      </Switch>1951    </div>1952  );1953}