Settings.tsx

104.7 KB · raw

1import { createEffect, createMemo, createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js";2import { sendMessage } from "../App";3import { PeachMark } from "../components/PeachMark";4import { SyncModeSelector } from "../components/SyncModeSelector";5import { StackDeck } from "../components/settings/StackDeck";6import type { CardId, StackBodies, StackCardSpec } from "../components/settings/types";7import { RecoveryWordDisplayView } from "./RecoveryWordDisplay";8import { WhatsNewView } from "./WhatsNew";9import type { Canary } from "../../shared/types";10import { ACCENT_COLORS } from "../../shared/types";11import { PEACH_VERSION, hasReleaseNotes } from "../../shared/release-notes";12import type {13  AccentColorName,14  PasskeyProviderState,15  ProFeature,16  VaultSnapshot,17} from "../../shared/types";18import type { PairedDevice } from "../../shared/sync-types";19import {20  describeSyncOutcome,21  syncIndicator,22  syncIndicatorLabel,23  type SyncOutcomeSummary,24} from "../../shared/sync-outcome";25import type { SyncGeneration } from "../../background/sync-generation";26import {27  DEFAULT_BINDINGS,28  findConflict,29  formatKeybinding,30  keybindingFromEvent,31  SHORTCUT_DESCRIPTORS,32  type Keybinding,33  type ShortcutAction,34} from "../../shared/shortcuts";35import { WEB3FORMS_KEY } from "../../shared/config";3637interface LicenseInfo {38  valid: boolean;39  plan: "free" | "pro";40  issued?: string;41}4243interface SettingsProps {44  entriesCount: number;45  breachMonitoring: boolean;46  lockOnClose: boolean;47  autoLockMinutes: number;48  trashRetentionDays: number;49  theme: "dark" | "light";50  accentColor: AccentColorName;51  textSize: "small" | "medium" | "large";52  reduceMotion: boolean;53  isPro?: boolean;54  initialSection?: string | null;55  upgradeHighlight?: ProFeature | null;56  onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;57  onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;58  onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;59  onTrashRetentionChange: (days: number) => void | Promise<void>;60  onThemeChange: (theme: "dark" | "light") => void;61  onAccentChange: (name: AccentColorName) => void | Promise<void>;62  onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise<void>;63  onReduceMotionChange: (enabled: boolean) => void | Promise<void>;64  shortcuts: Record<ShortcutAction, Keybinding>;65  onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;66  onLicenseChange?: () => void | Promise<void>;67  isExiting?: boolean;68  onBack: () => void;69  onImport: () => void;70  onExport: () => void;71  onBreachCheckDone: () => void;72  onOpenHealth?: () => void;73  onOpenSyncPairing?: (method?: "code") => void;74  onOpenFileImport?: () => void;75  onOpenPanic?: () => void;76  onOpenCodexGenerate?: () => void;77  onOpenCodexRestore?: () => void;78  /** Invoked from within Settings when a Pro-gated control is tapped on79   *  free tier. App owns the upgrade navigation + highlight state, so this80   *  callback is the single way for Settings to request that navigation. */81  onProGate?: (feature: ProFeature) => void;82  onCardChange?: (id: CardId) => void;83}8485const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];86const MAX_FEEDBACK_MESSAGE_LENGTH = 4000;87const PRO_CELEBRATION_CARD_STEP_MS = 150;88const PRO_CELEBRATION_THANK_YOU_MS = 3200;8990function sanitizeFeedbackMessage(value: string): string {91  return value92    .trim()93    .slice(0, MAX_FEEDBACK_MESSAGE_LENGTH)94    // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — strips control characters from feedback text95    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")96    .replace(/&/g, "&amp;")97    .replace(/</g, "&lt;")98    .replace(/>/g, "&gt;")99    .replace(/"/g, "&quot;")100    .replace(/'/g, "&#39;");101}102103function formatActivatedSince(issued: string | undefined): string {104  if (!issued) return "Active";105  try {106    const d = new Date(issued);107    if (Number.isNaN(d.getTime())) return "Active";108    return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;109  } catch {110    return "Active";111  }112}113114function initialSectionToCard(value: string | null | undefined): CardId | null {115  if (!value) return null;116  if (value === "security") return "protection";117  if (value === "license") return "pro";118  if (119    value === "protection" ||120    value === "vault" ||121    value === "sync" ||122    value === "appearance" ||123    value === "shortcuts" ||124    value === "pro" ||125    value === "feedback"126  ) {127    return value;128  }129  return null;130}131132/**133 * §11 migration copy. It must never imply that old peers keep working: the134 * whole point of the reset is that they cannot.135 */136const SECURE_RESET_DISCLOSURE =137  "Peach will archive this device's old Sync trust and start the new Sync generation. " +138  "Every other device must be updated first, then re-paired with this one — they will " +139  "not continue on their own. Your vault and your recovery words are not changed.";140141function secureResetHeadline(state: string | undefined): string {142  switch (state) {143    case "LEGACY_ACTIVE":144      return "Legacy Sync generation";145    case "RESET_REQUIRED":146      return "Reset required";147    case "RESET_CONFIRMED":148      return "Reset not completed";149    case "V2_ANCHOR":150    case "REPAIRING":151      return "Not re-paired";152    case "V2_ACTIVE":153      return "New Sync generation";154    case "UNKNOWN":155      return "Sync generation unreadable";156    default:157      return "Sync generation";158  }159}160161function secureResetDetail(state: string | undefined): string {162  switch (state) {163    case "LEGACY_ACTIVE":164      return "This device still holds the old Sync trust. Resetting retires it for good: update Peach on every other device, then re-pair each one.";165    case "RESET_REQUIRED":166      return "Sync is stopped until the reset completes. Update Peach on every other device, then reset this one and re-pair each device.";167    case "RESET_CONFIRMED":168      return "The reset did not finish. Nothing was lost — the old trust is archived. Try again.";169    case "V2_ANCHOR":170    case "REPAIRING":171      return "This device anchors the new Sync generation and is not re-paired yet. Pair each device again; until then it keeps its own vault and cannot sync.";172    case "V2_ACTIVE":173      return "This device is on the new Sync generation. Each device is re-paired individually.";174    case "UNKNOWN":175      return "This device's Sync generation can't be read, so sync is stopped. Reinstall or contact support.";176    default:177      return "Sync state is unavailable.";178  }179}180181export function SettingsView(props: SettingsProps) {182  const manifest = chrome.runtime.getManifest();183  const version = manifest.version;184  const initial = props.initialSection;185  const [activeCard, setActiveCard] = createSignal<CardId>(186    props.upgradeHighlight ? "pro" : initialSectionToCard(initial) ?? "pro"187  );188189  createEffect(() => { props.onCardChange?.(activeCard()); });190191  const [justActivated, setJustActivated] = createSignal(false);192  const [isFanning, setIsFanning] = createSignal(false);193194  function triggerCelebration() {195    if (isFanning()) return;196    setJustActivated(false);197    setIsFanning(true);198    setActiveCard("pro");199    const fanOrder: CardId[] = ["protection", "vault", "sync", "appearance", "shortcuts", "feedback"];200    fanOrder.forEach((id, i) => {201      window.setTimeout(() => setActiveCard(id), (i + 1) * PRO_CELEBRATION_CARD_STEP_MS);202    });203    window.setTimeout(() => {204      setActiveCard("pro");205      setIsFanning(false);206      setJustActivated(true);207      window.setTimeout(() => setJustActivated(false), PRO_CELEBRATION_THANK_YOU_MS);208    }, (fanOrder.length + 1) * PRO_CELEBRATION_CARD_STEP_MS);209  }210211  function handleProCelebrationClick(e: MouseEvent) {212    e.stopPropagation();213    triggerCelebration();214  }215216  const [checkingBreaches, setCheckingBreaches] = createSignal(false);217  const [breachResult, setBreachResult] = createSignal<string | null>(null);218  const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);219  const [licenseKeyInput, setLicenseKeyInput] = createSignal("");220  const [licenseError, setLicenseError] = createSignal<string | null>(null);221  const [, setLicenseSuccess] = createSignal<string | null>(null);222  const [activatingLicense, setActivatingLicense] = createSignal(false);223224  const isPro = () => props.isPro ?? false;225  const cards = createMemo<StackCardSpec[]>(() => [226    {227      id: "pro",228      num: "I · FOREVER",229      title: "Peach Pro",230      lede: isPro() ? "active · paid once, yours forever" : "$39 once · yours forever",231      bgVar: "--stack-pro-bg",232      fgVar: "--stack-pro-fg",233      icon: "spark",234      onIconClick: isPro() ? triggerCelebration : undefined,235    },236    {237      id: "protection",238      num: "II · PROTECT",239      title: "Protection",240      lede: `${props.breachMonitoring ? "breaches watched" : "breach watch off"} · ${props.lockOnClose ? "locks on close" : "manual lock"}`,241      bgVar: "--stack-protect-bg",242      fgVar: "--stack-protect-fg",243      icon: "shield",244    },245    {246      id: "vault",247      num: "III · KEEP",248      title: "Vault",249      lede: `${props.entriesCount} ${props.entriesCount === 1 ? "entry" : "entries"} · backups, history, cleanup`,250      bgVar: "--stack-vault-bg",251      fgVar: "--stack-vault-fg",252      icon: "lock",253    },254    {255      id: "sync",256      num: "IV · ECHO",257      title: "Sync",258      lede: isPro() ? "your devices, one vault" : "Pro · encrypted device sync",259      bgVar: "--stack-sync-bg",260      fgVar: "--stack-sync-fg",261      icon: "sync",262    },263    {264      id: "appearance",265      num: "V · DRESS",266      title: "Appearance",267      lede: `${props.theme} · ${props.accentColor} · ${props.textSize}`,268      bgVar: "--stack-appear-bg",269      fgVar: "--stack-appear-fg",270      icon: "sun",271    },272    {273      id: "shortcuts",274      num: "VI · KEYS",275      title: "Shortcuts",276      lede: `${SHORTCUT_DESCRIPTORS.length} bindings · what your fingers know`,277      bgVar: "--stack-shortcut-bg",278      fgVar: "--stack-shortcut-fg",279      icon: "keys",280    },281    {282      id: "feedback",283      num: "VII · ECHO",284      title: "Feedback",285      lede: `version ${version} · tell us what would make it better`,286      bgVar: "--stack-feedback-bg",287      fgVar: "--stack-feedback-fg",288      icon: "bubble",289    },290  ]);291  const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);292  const [passkeySaving, setPasskeySaving] = createSignal(false);293294  // UI-3 / MV-7: devices vouched by another of the user's devices, awaiting295  // explicit confirmation before they become sync targets.296  interface PendingVouched {297    deviceId: string;298    deviceName: string;299    longTermPublicKeyHex: string;300    receivedAt: number;301    vouchedByDeviceId: string;302  }303  const [pendingVouched, setPendingVouched] = createSignal<PendingVouched[]>([]);304  const [vouchedBusy, setVouchedBusy] = createSignal<string | null>(null);305  // Paired-device management lives here — Settings is the sync hub.306  const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);307  const [confirmUnpair, setConfirmUnpair] = createSignal<string | null>(null);308  const [renameDeviceId, setRenameDeviceId] = createSignal<string | null>(null);309  const [renameDraft, setRenameDraft] = createSignal("");310  const [syncStatus, setSyncStatus] = createSignal<"idle" | "syncing" | "error">("idle");311  const [syncFlash, setSyncFlash] = createSignal<string | null>(null);312  // The last sync ATTEMPT, kept on screen rather than flashed for 3 seconds —313  // a transient toast is no use for diagnosing a phone that isn't syncing.314  const [syncSummary, setSyncSummary] = createSignal<SyncOutcomeSummary>({});315  // §11: the Sync generation this installation is on. Not secret — a version316  // and a state name — so it renders the same in any session.317  const [generation, setGeneration] = createSignal<SyncGeneration | null>(null);318  const [resetOpen, setResetOpen] = createSignal(false);319  const [resetPassword, setResetPassword] = createSignal("");320  const [resetError, setResetError] = createSignal<string | null>(null);321  const [resetting, setResetting] = createSignal(false);322  const syncLight = () =>323    syncStatus() === "syncing"324      ? "busy"325      : syncIndicator(syncSummary(), pairedDevices().length);326  const syncLightLabel = () => syncIndicatorLabel(syncLight(), syncSummary());327  const lastSyncLine = () => {328    const s = syncSummary();329    if (!s.outcome) return "No sync attempted yet";330    return `${syncRelativeTime(s.finishedAt)} · ${describeSyncOutcome(s)}`;331  };332  async function loadSyncState() {333    const r = (await sendMessage({ type: "GET_SYNC_STATE" })) as {334      ok: boolean;335      data?: SyncOutcomeSummary;336    };337    if (r.ok && r.data) {338      setSyncSummary(r.data);339      setSyncStatus(r.data.status ?? "idle");340    }341  }342343  async function loadGeneration() {344    const r = (await sendMessage({ type: "GET_SYNC_GENERATION" })) as {345      ok: boolean;346      data?: SyncGeneration | null;347    };348    if (r.ok) setGeneration(r.data ?? null);349  }350351  async function runSecureReset() {352    setResetting(true);353    const r = (await sendMessage({354      type: "SECURE_RESET",355      payload: { password: resetPassword() },356    })) as { ok: boolean; data?: SyncGeneration | null; error?: string };357    setResetting(false);358    setResetPassword("");359    if (!r.ok) {360      setResetError(r.error ?? "The reset did not complete — nothing was lost, try again");361      await loadGeneration();362      return;363    }364    setResetError(null);365    setResetOpen(false);366    if (r.data) setGeneration(r.data);367  }368369  const STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;370  function syncRelativeTime(when: number | undefined): string {371    if (!when) return "Not synced yet";372    const elapsed = Date.now() - when;373    if (elapsed < 60_000) return "Just now";374    const minutes = Math.floor(elapsed / 60_000);375    if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;376    const hours = Math.floor(minutes / 60);377    if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;378    const days = Math.floor(hours / 24);379    return days < 7 ? `${days} day${days === 1 ? "" : "s"} ago` : "Not seen recently";380  }381  function isSyncStale(d: PairedDevice): boolean {382    return d.lastSyncedAt !== undefined && Date.now() - d.lastSyncedAt > STALE_THRESHOLD_MS;383  }384  function deviceTypeLabel(t: PairedDevice["deviceType"]): string {385    switch (t) {386      case "android": return "Android";387      case "ios": return "iPhone";388      case "extension": return "Browser";389      default: return "Device";390    }391  }392393  async function loadPairedDevices() {394    if (!isPro()) return;395    const r = (await sendMessage({ type: "LIST_PAIRED_DEVICES" })) as {396      ok: boolean;397      data?: { devices: PairedDevice[] };398    };399    if (r.ok) setPairedDevices(r.data?.devices ?? []);400  }401  async function unpairDevice(deviceId: string) {402    await sendMessage({ type: "UNPAIR_DEVICE", payload: { deviceId } });403    setConfirmUnpair(null);404    await loadPairedDevices();405  }406  async function renameDevice(deviceId: string) {407    const name = renameDraft().trim();408    if (!name) return;409    const result = (await sendMessage({410      type: "RENAME_PAIRED_DEVICE",411      payload: { deviceId, name },412    })) as { ok: boolean };413    if (!result.ok) return;414    setRenameDeviceId(null);415    await loadPairedDevices();416  }417  async function syncNow() {418    setSyncStatus("syncing");419    setSyncFlash(null);420    const res = (await sendMessage({421      type: "TRIGGER_SYNC",422      payload: { reason: "manual", force: true },423    })) as { ok: boolean; error?: string };424    if (!res.ok) {425      setSyncStatus("error");426      setSyncSummary({ status: "error", outcome: "run_failed", detail: res.error });427      return;428    }429    // TRIGGER_SYNC returns as soon as the run is queued, so poll for the430    // outcome. A single delayed read used to land while the run was still431    // "syncing" and leave the button disabled forever.432    let s: SyncOutcomeSummary = { status: "syncing" };433    for (let i = 0; i < 40 && s.status === "syncing"; i++) {434      await new Promise((r) => setTimeout(r, 250));435      const state = (await sendMessage({ type: "GET_SYNC_STATE" })) as {436        ok: boolean;437        data?: SyncOutcomeSummary;438      };439      s = state.ok ? state.data ?? { status: "idle" } : { status: "error", outcome: "run_failed" };440    }441    setSyncSummary(s);442    setSyncStatus(s.status ?? "idle");443    // The persistent line below carries the outcome; the flash is only the444    // "that click did something" acknowledgement.445    setSyncFlash(describeSyncOutcome(s));446    setTimeout(() => setSyncFlash(null), 4000);447    await loadPairedDevices();448  }449  // UI-4 / MV-8: rotate sync identity confirmation dialog.450  const [rotateConfirmOpen, setRotateConfirmOpen] = createSignal(false);451  const [rotating, setRotating] = createSignal(false);452  const [rotateError, setRotateError] = createSignal<string | null>(null);453  const [showSyncConnection, setShowSyncConnection] = createSignal(false);454  const [showSyncRecovery, setShowSyncRecovery] = createSignal(false);455456  async function loadPendingVouched() {457    const r = (await sendMessage({ type: "GET_PENDING_VOUCHED_DEVICES" })) as {458      ok: boolean;459      data?: PendingVouched[];460    };461    if (r.ok) setPendingVouched(r.data ?? []);462  }463  async function confirmVouched(deviceId: string) {464    setVouchedBusy(deviceId);465    try {466      await sendMessage({ type: "CONFIRM_VOUCHED_DEVICE", payload: { deviceId } });467      await loadPendingVouched();468    } finally {469      setVouchedBusy(null);470    }471  }472  async function rejectVouched(deviceId: string) {473    setVouchedBusy(deviceId);474    try {475      await sendMessage({ type: "REJECT_VOUCHED_DEVICE", payload: { deviceId } });476      await loadPendingVouched();477    } finally {478      setVouchedBusy(null);479    }480  }481  async function doRotateSyncIdentity() {482    setRotateError(null);483    setRotating(true);484    try {485      const r = (await sendMessage({ type: "ROTATE_SYNC_IDENTITY" })) as {486        ok: boolean;487        error?: string;488      };489      if (!r.ok) {490        setRotateError(r.error ?? "Rotation failed.");491        return;492      }493      setRotateConfirmOpen(false);494      // Backend set peach_recovery_pending; reuse the existing recovery-words495      // display (its GET_RECOVERY_WORDS fetch is exempted during pending).496      setShowNewRecoveryWords(true);497    } finally {498      setRotating(false);499    }500  }501502  const [dedupCount, setDedupCount] = createSignal<number | null>(null);503  const [dedupRunning, setDedupRunning] = createSignal(false);504  const [dedupDone, setDedupDone] = createSignal<number | null>(null);505506  const [declinedCardCount, setDeclinedCardCount] = createSignal(0);507508  // Vault history (peach_vault_history) — last 10 snapshots, snapshotted509  // BEFORE every real-mode vault write so the user can roll back if a sync510  // or import overwrites the wrong state.511  const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);512  const [historyLoading, setHistoryLoading] = createSignal(false);513  const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);514  const [historyShowAll, setHistoryShowAll] = createSignal(false);515516  async function loadVaultHistory() {517    setHistoryLoading(true);518    try {519      const res = await sendMessage({ type: "GET_VAULT_HISTORY" });520      const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];521      setHistorySnapshots(list);522    } finally {523      setHistoryLoading(false);524    }525  }526527  async function handleRestoreVaultSnapshot(id: string) {528    if (historyRestoring()) return;529    setHistoryRestoring(id);530    const res = await sendMessage({531      type: "RESTORE_VAULT_SNAPSHOT",532      payload: { snapshotId: id },533    });534    setHistoryRestoring(null);535    if ((res as { ok: boolean }).ok) {536      // Vault is now locked — let the parent close Settings; App.tsx's537      // VAULT_LOCKED listener will route back to the lock screen.538      props.onBack();539    }540  }541542  function formatSnapshotTime(savedAt: number): string {543    const diff = Date.now() - savedAt;544    if (diff < 60_000) return "Just now";545    if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;546    const d = new Date(savedAt);547    const isToday = new Date().toDateString() === d.toDateString();548    const isYest =549      new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();550    const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });551    if (isToday) return `Today ${t}`;552    if (isYest) return `Yesterday ${t}`;553    return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;554  }555556  function reasonLabel(reason: VaultSnapshot["reason"]): string {557    return (558      {559        local_save: "saved",560        sync_received: "synced",561        import: "imported",562        restore: "restored",563      } as const564    )[reason] ?? reason;565  }566567  const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);568  const [showWhatsNew, setShowWhatsNew] = createSignal(false);569570  const [showChangePw, setShowChangePw] = createSignal(false);571  const [changePwCurrent, setChangePwCurrent] = createSignal("");572  const [changePwNew, setChangePwNew] = createSignal("");573  const [changePwConfirm, setChangePwConfirm] = createSignal("");574  const [changePwSaving, setChangePwSaving] = createSignal(false);575  const [changePwError, setChangePwError] = createSignal<string | null>(null);576  const [changePwSuccess] = createSignal(false);577  const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);578579  const [pinEnabled, setPinEnabled] = createSignal(false);580  const [pinFlow, setPinFlow] = createSignal<"setup" | "disable" | null>(null);581  const [pinCurrentPassword, setPinCurrentPassword] = createSignal("");582  const [pinValue, setPinValue] = createSignal("");583  const [pinConfirm, setPinConfirm] = createSignal("");584  const [pinSaving, setPinSaving] = createSignal(false);585  const [pinError, setPinError] = createSignal<string | null>(null);586587  // UI-5 / LV-4: view existing recovery words behind a fresh password proof.588  const [showViewWords, setShowViewWords] = createSignal(false);589  const [viewWordsPassword, setViewWordsPassword] = createSignal("");590  const [viewWordsError, setViewWordsError] = createSignal<string | null>(null);591  const [viewedWords, setViewedWords] = createSignal<string[] | null>(null);592  const [viewWordsBusy, setViewWordsBusy] = createSignal(false);593594  async function handleViewRecoveryWords() {595    setViewWordsError(null);596    if (!viewWordsPassword()) {597      setViewWordsError("Enter your master password.");598      return;599    }600    setViewWordsBusy(true);601    try {602      const r = (await sendMessage({603        type: "GET_RECOVERY_WORDS",604        payload: { password: viewWordsPassword() },605      })) as { ok: boolean; data?: string[]; error?: string };606      if (r.ok) {607        setViewedWords(r.data ?? []);608        setViewWordsPassword("");609        return;610      }611      switch (r.error) {612        case "wrong_password":613          setViewWordsError("Incorrect password");614          break;615        case "password_required":616          setViewWordsError("Enter your master password.");617          break;618        case "recovery_not_setup":619          setViewWordsError("Recovery words aren't set up for this vault yet.");620          break;621        default:622          setViewWordsError("Couldn't retrieve recovery words.");623      }624    } finally {625      setViewWordsBusy(false);626    }627  }628  function closeViewWords() {629    setShowViewWords(false);630    setViewWordsPassword("");631    setViewWordsError(null);632    setViewedWords(null);633  }634635  const [duressEnabled, setDuressEnabled] = createSignal(false);636  const [showDuressSetup, setShowDuressSetup] = createSignal(false);637  const [duressPassword, setDuressPassword] = createSignal("");638  const [duressConfirm, setDuressConfirm] = createSignal("");639  const [duressMainPassword, setDuressMainPassword] = createSignal("");640  const [duressSaving, setDuressSaving] = createSignal(false);641  const [duressError, setDuressError] = createSignal<string | null>(null);642  const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);643644  const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);645646  const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);647  const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);648649  const [fbCategory, setFbCategory] = createSignal("Bug report");650  const [fbMessage, setFbMessage] = createSignal("");651  type FeedbackState = 'idle' | 'sending' | 'sent' | 'error';652  const [feedbackState, setFeedbackState] = createSignal<FeedbackState>('idle');653654  createEffect(() => {655    if (props.upgradeHighlight) {656      setActiveCard("pro");657    }658  });659660  onMount(async () => {661    const [licenseRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([662      sendMessage({ type: "GET_LICENSE" }),663      sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),664      sendMessage({ type: "IS_DURESS_ENABLED" }),665      sendMessage({ type: "GET_SETTINGS" }),666      sendMessage({ type: "GET_LICENSE_KEY" }),667    ]);668669    // Vault history loads in the background — the Vault sub-view picks it up670    // when the user navigates in. Loading it lazily keeps Settings boot fast.671    void loadVaultHistory();672673    // UI-3 / MV-7: surface devices a paired peer vouched for, and refresh the674    // list live if a VOUCHED_DEVICE_PENDING broadcast arrives while open.675    void loadPendingVouched();676    void loadPairedDevices();677    void loadSyncState();678    void loadGeneration();679    const onBroadcast = (msg: unknown) => {680      const m = msg as { type?: string; payload?: unknown } | null;681      if (m?.type === "VOUCHED_DEVICE_PENDING") {682        void loadPendingVouched();683      }684      if (m?.type === "SYNC_STATE_CHANGED") {685        const payload = (m.payload ?? {}) as SyncOutcomeSummary;686        setSyncSummary(payload);687        setSyncStatus(payload.status ?? "idle");688      }689    };690    chrome.runtime?.onMessage?.addListener(onBroadcast);691    onCleanup(() => {692      try { chrome.runtime?.onMessage?.removeListener(onBroadcast); } catch { /* ignore */ }693    });694695    if ((licenseRes as { ok: boolean }).ok) {696      setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);697    }698699    if ((passkeyRes as { ok: boolean }).ok) {700      setPasskeyState(701        (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null702      );703    }704705    if ((duressRes as { ok: boolean }).ok) {706      setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);707    }708709    if ((settingsRes as { ok: boolean }).ok) {710      const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[]; pinEnabled?: boolean } }).data;711      setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);712      setPinEnabled(sData?.pinEnabled === true);713    }714715    if ((keyRes as { ok: boolean }).ok) {716      setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);717    }718719    const entriesRes = await sendMessage({ type: "GET_ENTRIES" });720    if ((entriesRes as { ok: boolean }).ok) {721      const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];722      setCanaryEntries(723        entries724          .filter((e) => e.canary?.webhookUrl)725          .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))726      );727    }728729  });730731  function resetPinFlow() {732    setPinFlow(null);733    setPinCurrentPassword("");734    setPinValue("");735    setPinConfirm("");736    setPinError(null);737  }738739  async function handleSetupPin() {740    setPinError(null);741    if (!pinCurrentPassword()) { setPinError("enter your current password"); return; }742    if (!/^\d{4,12}$/.test(pinValue())) { setPinError("PIN must be 4–12 digits"); return; }743    if (pinValue() !== pinConfirm()) { setPinError("PINs do not match"); return; }744    setPinSaving(true);745    const res = await sendMessage({746      type: "SETUP_PIN",747      payload: { currentPassword: pinCurrentPassword(), pin: pinValue() },748    }) as { ok: boolean; error?: string };749    if (res.ok) {750      setPinEnabled(true);751      resetPinFlow();752    } else {753      setPinError(res.error === "incorrect_current_password" ? "incorrect current password" : "couldn't set up PIN");754    }755    setPinSaving(false);756  }757758  async function handleDisablePin() {759    setPinError(null);760    if (!pinCurrentPassword()) { setPinError("enter your current password"); return; }761    setPinSaving(true);762    const res = await sendMessage({763      type: "DISABLE_PIN",764      payload: { currentPassword: pinCurrentPassword() },765    }) as { ok: boolean; error?: string };766    if (res.ok) {767      setPinEnabled(false);768      resetPinFlow();769    } else {770      setPinError(res.error === "incorrect_current_password" ? "incorrect current password" : "couldn't remove PIN");771    }772    setPinSaving(false);773  }774775  async function handleChangePassword() {776    setChangePwError(null);777    if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }778    if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }779    if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }780    if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }781    setChangePwSaving(true);782    const res = await sendMessage({783      type: "CHANGE_PASSWORD",784      payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },785    }) as { ok: boolean; error?: string };786    if (res.ok) {787      setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");788      setShowChangePw(false);789      // LV-4: the user just authenticated (current password), so the recovery790      // words reveal that follows is exempt from the re-auth gate. The791      // background owns this session-scoped exemption; the display clears it.792      await sendMessage({ type: "SET_RECOVERY_CONFIRM_PENDING" });793      setShowNewRecoveryWords(true);794    } else {795      setChangePwError(res.error ?? "failed to change password");796    }797    setChangePwSaving(false);798  }799800  const [copied, setCopied] = createSignal(false);801802  async function copyLicenseKey(key: string) {803    await navigator.clipboard.writeText(key);804    setCopied(true);805    setTimeout(() => setCopied(false), 2000);806  }807808  async function handleActivateLicense() {809    const key = licenseKeyInput().trim();810    if (!key) return;811    setActivatingLicense(true);812    setLicenseError(null);813    setLicenseSuccess(null);814    const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });815    setActivatingLicense(false);816    if ((res as { ok: boolean }).ok) {817      const data = (res as { ok: boolean; data?: LicenseInfo }).data;818      if (data?.valid) {819        setLicenseInfo(data);820        setLicenseKey(key);821        setLicenseSuccess("License activated!");822        setLicenseKeyInput("");823      } else {824        setLicenseError("Invalid license key");825      }826    } else {827      setLicenseError("Activation failed");828    }829  }830831  async function handleTogglePasskeys() {832    const current = passkeyState();833    if (!current || !current.supported) return;834    setPasskeySaving(true);835    const res = await sendMessage({836      type: "SET_PASSKEY_PROVIDER_ENABLED",837      payload: { enabled: !current.enabled },838    });839    setPasskeySaving(false);840    if ((res as { ok: boolean }).ok) {841      setPasskeyState(842        (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null843      );844    }845  }846847  async function handleCheckAllBreaches() {848    setCheckingBreaches(true);849    setBreachResult(null);850    const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });851    setCheckingBreaches(false);852    if ((res as { ok: boolean }).ok) {853      const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};854      const breached = Object.values(data).filter((count) => count > 0).length;855      setBreachResult(856        breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"857      );858      props.onBreachCheckDone();859    }860  }861862863  async function handleSetupDuress() {864    const mainPw = duressMainPassword().trim();865    const duressPw = duressPassword().trim();866    const confirmPw = duressConfirm().trim();867868    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }869    if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }870    if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }871    if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }872873    setDuressSaving(true);874    setDuressError(null);875876    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };877    if (!verifyRes.ok || !verifyRes.data) {878      setDuressError("incorrect main password");879      setDuressSaving(false);880      return;881    }882883    const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };884    setDuressSaving(false);885    if (res.ok) {886      setDuressEnabled(true);887      setShowDuressSetup(false);888      setDuressPassword("");889      setDuressConfirm("");890      setDuressMainPassword("");891      setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");892      setTimeout(() => setDuressSuccess(null), 5000);893    } else {894      setDuressError(895        res.error === "session_expired"896          ? "Vault session expired — lock and unlock Peach, then try again"897          : res.error ?? "failed to set duress password"898      );899    }900  }901902  async function handleClearDeclinedCards() {903    await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });904    setDeclinedCardCount(0);905  }906907  async function handleCountDuplicates() {908    setDedupRunning(true);909    setDedupDone(null);910    const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };911    setDedupRunning(false);912    if (res.ok) setDedupCount(res.data ?? 0);913  }914915  async function handleDeduplicate() {916    setDedupRunning(true);917    const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };918    setDedupRunning(false);919    if (res.ok) {920      setDedupDone(res.data ?? 0);921      setDedupCount(0);922    }923  }924925  async function handleDisableDuress() {926    const mainPw = duressMainPassword().trim();927    if (!mainPw) { setDuressError("enter your main password to confirm"); return; }928929    setDuressSaving(true);930    setDuressError(null);931932    const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };933    if (!verifyRes.ok || !verifyRes.data) {934      setDuressError("incorrect main password");935      setDuressSaving(false);936      return;937    }938939    const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };940    setDuressSaving(false);941    if (res.ok) {942      setDuressEnabled(false);943      setShowDuressSetup(false);944      setDuressMainPassword("");945      setDuressSuccess("Duress password disabled.");946      setTimeout(() => setDuressSuccess(null), 4000);947    } else {948      setDuressError(res.error ?? "failed to disable duress password");949    }950  }951952  async function handleSubmitFeedback() {953    const message = sanitizeFeedbackMessage(fbMessage());954    if (!message) return;955956    setFeedbackState('sending');957958    try {959      const category = FEEDBACK_CATEGORIES.includes(fbCategory())960        ? fbCategory()961        : FEEDBACK_CATEGORIES[0];962      const res = await fetch('https://api.web3forms.com/submit', {963        method: 'POST',964        headers: { 'Content-Type': 'application/json' },965        body: JSON.stringify({966          access_key: WEB3FORMS_KEY,967          subject: `Peach Feedback - ${category}`,968          message,969          from_name: 'Peach User',970          // Honeypot — leave blank971          botcheck: '',972        }),973      });974975      if (!res.ok) throw new Error(`HTTP ${res.status}`);976      const data = await res.json() as { success: boolean; message?: string };977      if (!data.success) throw new Error(data.message ?? 'unknown error');978979      setFeedbackState('sent');980      setFbMessage('');981      setFbCategory(FEEDBACK_CATEGORIES[0]);982    } catch (err) {983      console.error('[feedback]', err);984      setFeedbackState('error');985      setTimeout(() => setFeedbackState('idle'), 4000);986    }987  }988989  function renderSecurity() {990    return (991      <div class="peach-settings-body">992        <div class="peach-settings-group">993          <p class="peach-settings-sub-label">LOCKING</p>994          <div class="peach-settings-row">995            <span>Lock on close</span>996            <button997              class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}998              onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}999              aria-label="Toggle lock on close"1000            >1001              <span class="peach-toggle-knob"></span>1002            </button>1003          </div>1004          <div class="peach-settings-row">1005            <span>Auto-lock after</span>1006            <select1007              class="peach-settings-select"1008              value={String(props.autoLockMinutes)}1009              onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}1010            >1011              <option value="1">1 minute</option>1012              <option value="5">5 minutes</option>1013              <option value="15">15 minutes</option>1014              <option value="30">30 minutes</option>1015              <option value="0">Never</option>1016            </select>1017          </div>10181019          <p class="peach-settings-sub-label">QUICK UNLOCK</p>1020          <div class="peach-settings-row peach-settings-row--meta">1021            <span>PIN unlock</span>1022            <button1023              class="peach-btn peach-btn--subtle peach-btn--sm"1024              onClick={() => { setPinFlow(pinEnabled() ? "disable" : "setup"); setPinError(null); }}1025            >1026              {pinEnabled() ? "remove" : "set up"}1027            </button>1028          </div>1029          <Show when={pinFlow() === "setup"}>1030            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">1031              <input1032                class="peach-settings-input"1033                type="password"1034                placeholder="current password"1035                value={pinCurrentPassword()}1036                onInput={(e) => setPinCurrentPassword((e.target as HTMLInputElement).value)}1037                autocomplete="current-password"1038              />1039              <input1040                class="peach-settings-input"1041                type="password"1042                inputmode="numeric"1043                maxlength="12"1044                placeholder="new 4–12 digit PIN"1045                value={pinValue()}1046                onInput={(e) => setPinValue((e.target as HTMLInputElement).value.replace(/\D/g, "").slice(0, 12))}1047                autocomplete="off"1048              />1049              <input1050                class="peach-settings-input"1051                type="password"1052                inputmode="numeric"1053                maxlength="12"1054                placeholder="confirm PIN"1055                value={pinConfirm()}1056                onInput={(e) => setPinConfirm((e.target as HTMLInputElement).value.replace(/\D/g, "").slice(0, 12))}1057                autocomplete="off"1058                onKeyDown={(e) => { if (e.key === "Enter") void handleSetupPin(); }}1059              />1060              <Show when={pinError()}>1061                <p class="peach-hint peach-hint-error" style="margin:0">{pinError()}</p>1062              </Show>1063              <button1064                class="peach-btn peach-btn--accent peach-btn--sm"1065                style="align-self:flex-start"1066                onClick={() => void handleSetupPin()}1067                disabled={pinSaving() || !pinCurrentPassword() || !/^\d{4,12}$/.test(pinValue()) || !/^\d{4,12}$/.test(pinConfirm())}1068              >1069                {pinSaving() ? "saving…" : "save PIN"}1070              </button>1071            </div>1072          </Show>1073          <Show when={pinFlow() === "disable"}>1074            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">1075              <input1076                class="peach-settings-input"1077                type="password"1078                placeholder="current password"1079                value={pinCurrentPassword()}1080                onInput={(e) => setPinCurrentPassword((e.target as HTMLInputElement).value)}1081                autocomplete="current-password"1082                onKeyDown={(e) => { if (e.key === "Enter") void handleDisablePin(); }}1083              />1084              <Show when={pinError()}>1085                <p class="peach-hint peach-hint-error" style="margin:0">{pinError()}</p>1086              </Show>1087              <button1088                class="peach-btn peach-btn--subtle peach-btn--sm"1089                style="align-self:flex-start"1090                onClick={() => void handleDisablePin()}1091                disabled={pinSaving() || !pinCurrentPassword()}1092              >1093                {pinSaving() ? "removing…" : "remove PIN"}1094              </button>1095            </div>1096          </Show>10971098          <p class="peach-settings-sub-label">BREACH PROTECTION</p>1099          <div class="peach-settings-row">1100            <span>Breach monitoring</span>1101            <button1102              class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}1103              onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}1104              aria-label="Toggle breach monitoring"1105            >1106              <span class="peach-toggle-knob"></span>1107            </button>1108          </div>1109          <div class="peach-settings-row peach-settings-row--meta">1110            <span>Check all passwords</span>1111            <button1112              class="peach-btn peach-btn--subtle peach-btn--sm"1113              onClick={() => void handleCheckAllBreaches()}1114              disabled={checkingBreaches()}1115            >1116              {checkingBreaches() ? "checking..." : "run"}1117            </button>1118          </div>1119          <Show when={breachResult()}>1120            <div class="peach-settings-row peach-settings-row--meta">1121              <span>Last check</span>1122              <span class="peach-settings-value">{breachResult()}</span>1123            </div>1124          </Show>11251126          <p class="peach-settings-sub-label">MASTER PASSWORD</p>1127          <div class="peach-settings-row peach-settings-row--meta">1128            <span>Master password</span>1129            <button1130              class="peach-btn peach-btn--subtle peach-btn--sm"1131              onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}1132            >1133              {showChangePw() ? "cancel" : "change"}1134            </button>1135          </div>1136          <Show when={showChangePw()}>1137            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">1138              <input1139                class="peach-settings-input"1140                type="password"1141                placeholder="current password"1142                value={changePwCurrent()}1143                onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}1144                autocomplete="current-password"1145              />1146              <input1147                class="peach-settings-input"1148                type="password"1149                placeholder="new password (min 12 chars)"1150                value={changePwNew()}1151                onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}1152                autocomplete="new-password"1153              />1154              <input1155                class="peach-settings-input"1156                type="password"1157                placeholder="confirm new password"1158                value={changePwConfirm()}1159                onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}1160                autocomplete="new-password"1161                onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}1162              />1163              <Show when={changePwError()}>1164                <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>1165              </Show>1166              <Show when={changePwSuccess()}>1167                <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>1168              </Show>1169              <button1170                class="peach-btn peach-btn--accent peach-btn--sm"1171                style="align-self:flex-start"1172                onClick={() => void handleChangePassword()}1173                disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}1174              >1175                {changePwSaving() ? "saving…" : "save new password"}1176              </button>1177            </div>1178          </Show>11791180          {/* UI-5 / LV-4: view existing recovery words behind a password proof. */}1181          <div class="peach-settings-row peach-settings-row--meta">1182            <span>Recovery words</span>1183            <button1184              class="peach-btn peach-btn--subtle peach-btn--sm"1185              onClick={() => {1186                if (showViewWords()) { closeViewWords(); } else { setShowViewWords(true); }1187              }}1188            >1189              {showViewWords() ? "cancel" : "view"}1190            </button>1191          </div>1192          <Show when={showViewWords()}>1193            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">1194              <Show1195                when={viewedWords()}1196                fallback={1197                  <>1198                    <p class="peach-hint" style="margin:0;color:var(--peach-t2)">1199                      Enter your master password to reveal your recovery words.1200                    </p>1201                    <input1202                      class="peach-settings-input"1203                      type="password"1204                      placeholder="master password"1205                      value={viewWordsPassword()}1206                      onInput={(e) => setViewWordsPassword((e.target as HTMLInputElement).value)}1207                      autocomplete="current-password"1208                      onKeyDown={(e) => { if (e.key === "Enter") void handleViewRecoveryWords(); }}1209                    />1210                    <Show when={viewWordsError()}>1211                      <p class="peach-hint peach-hint-error" style="margin:0">{viewWordsError()}</p>1212                    </Show>1213                    <button1214                      class="peach-btn peach-btn--accent peach-btn--sm"1215                      style="align-self:flex-start"1216                      onClick={() => void handleViewRecoveryWords()}1217                      disabled={viewWordsBusy() || !viewWordsPassword()}1218                    >1219                      {viewWordsBusy() ? "checking…" : "reveal recovery words"}1220                    </button>1221                  </>1222                }1223              >1224                <div class="peach-recovery-words-grid">1225                  <For each={viewedWords()!}>1226                    {(word, i) => (1227                      <span class="peach-recovery-word"><b>{i() + 1}</b> {word}</span>1228                    )}1229                  </For>1230                </div>1231                <button1232                  class="peach-btn peach-btn--subtle peach-btn--sm"1233                  style="align-self:flex-start"1234                  onClick={closeViewWords}1235                >done</button>1236              </Show>1237            </div>1238          </Show>12391240          <p class="peach-settings-sub-label">1241            DURESS PASSWORD1242            <Show when={!isPro()}>1243              <span class="peach-pro-mark" aria-label="Pro feature" style="margin-left:6px">✦</span>1244            </Show>1245          </p>1246          <div class="peach-settings-row peach-settings-row--top">1247            <div class="peach-settings-row-main">1248              <span>1249                Duress password1250                <Show when={!isPro()}>1251                  <span class="peach-pro-badge">Pro</span>1252                </Show>1253              </span>1254              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1255                If entered under coercion, shows a decoy empty vault instead of your real one.1256              </span>1257            </div>1258            <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>1259              {duressEnabled() ? "on" : "off"}1260            </div>1261          </div>12621263          <Show when={duressEnabled() && !showDuressSetup()}>1264            <div class="peach-settings-row peach-settings-row--meta">1265              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1266                If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden. The encrypted storage is identical in size whether a hidden vault exists or not, so the hidden vault cannot be proven or disproven without both passwords. This protects against a coercer who demands your password — not against device malware or someone who already has your device unlocked.1267              </span>1268            </div>1269          </Show>12701271          <Show when={duressSuccess()}>1272            <div class="peach-settings-row peach-settings-row--meta">1273              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>1274            </div>1275          </Show>12761277          <Show when={!showDuressSetup()}>1278            <div class="peach-settings-row peach-settings-row--meta">1279              <button1280                class="peach-btn peach-btn--subtle peach-btn--sm"1281                onClick={() => {1282                  // Pro gate entry-point: free users are redirected to the1283                  // upgrade screen before the setup form opens. The server-1284                  // side SETUP_DURESS handler is the single enforcement1285                  // point; this is UI redirection so the form never appears1286                  // for a user who can't complete it.1287                  if (!isPro() && !duressEnabled()) {1288                    props.onProGate?.("duress_vault");1289                    setActiveCard("pro");1290                    return;1291                  }1292                  setShowDuressSetup(true);1293                  setDuressError(null);1294                  setDuressSuccess(null);1295                  setDuressPassword("");1296                  setDuressConfirm("");1297                  setDuressMainPassword("");1298                }}1299              >1300                {duressEnabled() ? "Change / disable" : "Set up"}1301              </button>1302            </div>1303          </Show>13041305          <Show when={showDuressSetup()}>1306            <div class="peach-duress-form">1307              <label class="peach-label">main password (to confirm)</label>1308              <input1309                class="peach-settings-input"1310                type="password"1311                placeholder="your current master password"1312                autocomplete="current-password"1313                value={duressMainPassword()}1314                onInput={(e) => setDuressMainPassword(e.currentTarget.value)}1315              />1316              <Show when={!duressEnabled()}>1317                <label class="peach-label">new duress password</label>1318                <input1319                  class="peach-settings-input"1320                  type="password"1321                  placeholder="duress password (min 8 chars)"1322                  autocomplete="new-password"1323                  value={duressPassword()}1324                  onInput={(e) => setDuressPassword(e.currentTarget.value)}1325                />1326                <label class="peach-label">confirm duress password</label>1327                <input1328                  class="peach-settings-input"1329                  type="password"1330                  placeholder="confirm duress password"1331                  autocomplete="new-password"1332                  value={duressConfirm()}1333                  onInput={(e) => setDuressConfirm(e.currentTarget.value)}1334                />1335              </Show>13361337              <Show when={duressError()}>1338                <p class="peach-hint peach-hint-error">{duressError()}</p>1339              </Show>13401341              <div class="peach-duress-actions">1342                <button1343                  class="peach-btn peach-btn--subtle peach-btn--sm"1344                  onClick={() => { setShowDuressSetup(false); setDuressError(null); }}1345                >1346                  cancel1347                </button>1348                <Show when={duressEnabled()}>1349                  <button1350                    class="peach-btn peach-btn--ghost peach-btn--sm"1351                    style="color:var(--peach-breach)"1352                    onClick={() => void handleDisableDuress()}1353                    disabled={duressSaving()}1354                  >1355                    {duressSaving() ? "disabling..." : "disable duress"}1356                  </button>1357                </Show>1358                <Show when={!duressEnabled()}>1359                  <button1360                    class="peach-btn peach-btn--accent peach-btn--sm"1361                    onClick={() => void handleSetupDuress()}1362                    disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}1363                  >1364                    {duressSaving() ? "saving..." : "save"}1365                  </button>1366                </Show>1367              </div>1368            </div>1369          </Show>13701371          <p class="peach-settings-sub-label">1372            CANARY ENTRIES1373            <Show when={!isPro()}>1374              <span class="peach-pro-mark" aria-label="Pro feature" style="margin-left:6px">✦</span>1375            </Show>1376          </p>1377          <div class="peach-settings-row peach-settings-row--top">1378            <div class="peach-settings-row-main">1379              <span>1380                Canary entries1381                <Show when={!isPro()}>1382                  <span class="peach-pro-badge">Pro</span>1383                </Show>1384              </span>1385              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1386                Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.1387              </span>1388            </div>1389            <span class="peach-settings-value" style="flex-shrink:0">1390              {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}1391            </span>1392          </div>1393          <Show when={canaryEntries().length > 0}>1394            <For each={canaryEntries()}>1395              {(entry) => (1396                <div class="peach-settings-row peach-settings-row--meta">1397                  <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>1398                  <button1399                    class="peach-btn peach-btn--subtle peach-btn--sm"1400                    onClick={() => void handleTestCanary(entry)}1401                    disabled={canaryTestingId() === entry.id}1402                    style="flex-shrink:0"1403                  >1404                    {canaryTestingId() === entry.id1405                      ? "sending…"1406                      : canaryTestResult()?.id === entry.id1407                        ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1408                        : "test"}1409                  </button>1410                </div>1411              )}1412            </For>1413          </Show>14141415          <p class="peach-settings-sub-label">PASSKEYS</p>1416          <div class="peach-settings-row peach-settings-row--top">1417            <div class="peach-settings-row-main">1418              <span>Intercept passkey requests</span>1419              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1420                Peach can handle passkey sign-in and creation on supported Chromium browsers.1421              </span>1422            </div>1423            <button1424              class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1425              onClick={() => void handleTogglePasskeys()}1426              aria-label="Toggle passkey interception"1427              disabled={!passkeyState()?.supported || passkeySaving()}1428            >1429              <span class="peach-toggle-knob"></span>1430            </button>1431          </div>1432          <div class="peach-settings-row peach-settings-row--meta">1433            <span>Status</span>1434            <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1435          </div>1436          <div class="peach-settings-row peach-settings-row--meta">1437            <span>Browser</span>1438            <span class="peach-settings-value">1439              {passkeyState()?.browserLabel ?? "Unknown"}1440              {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1441            </span>1442          </div>1443          <div class="peach-info-notice" style="margin-top:8px">1444            Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1445          </div>1446          <Show when={passkeyState()?.conflict}>1447            <div class="peach-settings-row peach-settings-row--meta">1448              <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1449            </div>1450          </Show>1451        </div>1452      </div>1453    );1454  }14551456  function renderVault() {1457    return (1458      <div class="peach-settings-body">1459        <div class="peach-settings-group">1460          <p class="peach-settings-sub-label">ENTRIES</p>1461          <div class="peach-settings-row peach-settings-row--meta">1462            <span>Entries</span>1463            <span class="peach-settings-value">{props.entriesCount}</span>1464          </div>1465          <div class="peach-settings-row peach-settings-row--meta">1466            <span>Duplicates</span>1467            <Show when={dedupDone() !== null} fallback={1468              <button1469                class="peach-btn peach-btn--subtle peach-btn--sm"1470                onClick={() => void handleCountDuplicates()}1471                disabled={dedupRunning()}1472              >1473                {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1474              </button>1475            }>1476              <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1477                {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1478              </span>1479            </Show>1480          </div>1481          <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1482            <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1483              <div class="peach-restore-warning">1484                {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1485              </div>1486              <div style="display:flex;gap:6px">1487                <button1488                  class="peach-btn peach-btn--subtle peach-btn--sm"1489                  onClick={() => setDedupCount(null)}1490                >1491                  cancel1492                </button>1493                <button1494                  class="peach-btn peach-btn--accent peach-btn--sm"1495                  onClick={() => void handleDeduplicate()}1496                  disabled={dedupRunning()}1497                >1498                  {dedupRunning() ? "removing..." : "remove duplicates"}1499                </button>1500              </div>1501            </div>1502          </Show>15031504          <p class="peach-settings-sub-label">MAINTENANCE</p>1505          <div class="peach-settings-row peach-settings-row--meta">1506            <span>Trash auto-delete</span>1507            <select1508              class="peach-settings-select"1509              value={String(props.trashRetentionDays)}1510              onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1511            >1512              <option value="7">7 days</option>1513              <option value="30">30 days</option>1514              <option value="90">90 days</option>1515              <option value="0">Never</option>1516            </select>1517          </div>1518          <div class="peach-settings-row peach-settings-row--meta">1519            <span>Advanced Vault Health</span>1520            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1521              open1522            </button>1523          </div>1524          <Show when={declinedCardCount() > 0}>1525            <div class="peach-settings-row peach-settings-row--meta">1526              <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1527              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1528                Clear1529              </button>1530            </div>1531          </Show>15321533          <p class="peach-settings-sub-label">DATA</p>1534          <div class="peach-settings-row peach-settings-row--meta">1535            <span>Import passwords</span>1536            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1537              open1538            </button>1539          </div>1540          <div class="peach-settings-row peach-settings-row--meta">1541            <span>Export backup</span>1542            <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1543              open1544            </button>1545          </div>1546          <div class="peach-settings-row peach-settings-row--meta">1547            <span>1548              Peach Codex1549              <Show when={!isPro()}>1550                <span class="peach-pro-badge">Pro</span>1551              </Show>1552            </span>1553            <button1554              class="peach-btn peach-btn--subtle peach-btn--sm"1555              onClick={() => props.onOpenCodexGenerate?.()}1556            >1557              open1558            </button>1559          </div>1560          {/* Restore from Codex: intentionally never gated — a printed1561              Codex must remain restorable regardless of license state. */}1562          <div class="peach-settings-row peach-settings-row--meta">1563            <span>Restore from Codex</span>1564            <button1565              class="peach-btn peach-btn--subtle peach-btn--sm"1566              onClick={() => props.onOpenCodexRestore?.()}1567            >1568              open1569            </button>1570          </div>15711572          <Show when={hasReleaseNotes()}>1573            <p class="peach-settings-sub-label">RELEASE NOTES</p>1574            <div class="peach-settings-row peach-settings-row--meta">1575              <span>v{PEACH_VERSION}</span>1576              <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => setShowWhatsNew(true)}>1577                view1578              </button>1579            </div>1580          </Show>15811582          <span class="peach-settings-sub-label">VAULT HISTORY</span>1583          <Show when={historyLoading()}>1584            <p class="peach-settings-hint">Loading…</p>1585          </Show>1586          <Show when={!historyLoading() && historySnapshots().length === 0}>1587            <p class="peach-settings-hint">1588              No snapshots yet. Peach saves automatically before every change —1589              saves, syncs, and imports.1590            </p>1591          </Show>1592          <Show when={!historyLoading() && historySnapshots().length > 0}>1593            <p class="peach-settings-hint">1594              Restoring locks your vault. Unlock with the password that1595              encrypted that snapshot to confirm.1596            </p>1597            <div class="peach-history-list">1598              <For each={historySnapshots().slice(0, historyShowAll() ? undefined : 3)}>1599                {(snap) => (1600                  <div class="peach-history-row">1601                    <div class="peach-history-meta">1602                      <span class="peach-history-time">1603                        {formatSnapshotTime(snap.savedAt)}1604                      </span>1605                      <span class="peach-history-sub">1606                        {[1607                          snap.entryCount !== null1608                            ? `up to ${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1609                            : null,1610                          reasonLabel(snap.reason),1611                        ]1612                          .filter(Boolean)1613                          .join(" · ")}1614                      </span>1615                    </div>1616                    <button1617                      class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1618                        historyRestoring() === snap.id ? " is-busy" : ""1619                      }`}1620                      onClick={() => void handleRestoreVaultSnapshot(snap.id)}1621                      disabled={historyRestoring() !== null}1622                    >1623                      <Show when={historyRestoring() === snap.id} fallback="Restore">1624                        …1625                      </Show>1626                    </button>1627                  </div>1628                )}1629              </For>1630            </div>1631            <Show when={historySnapshots().length > 3}>1632              <button1633                class="peach-settings-advanced-toggle"1634                onClick={() => setHistoryShowAll((v) => !v)}1635                aria-expanded={historyShowAll()}1636              >1637                <span class="peach-settings-hint" style="margin:0">1638                  {historyShowAll() ? "show less" : `${historySnapshots().length - 3} older`}1639                </span>1640                <span class="peach-settings-advanced-chevron">1641                  {historyShowAll() ? "▲" : "▼"}1642                </span>1643              </button>1644            </Show>1645            <p class="peach-history-footer">1646              {historySnapshots().length} of 10 snapshots1647            </p>1648          </Show>16491650          <button1651            class="peach-settings-advanced-toggle"1652            onClick={() => setShowVaultAdvanced((v) => !v)}1653            aria-expanded={showVaultAdvanced()}1654          >1655            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1656            <span class="peach-settings-advanced-chevron">1657              {showVaultAdvanced() ? "▲" : "▼"}1658            </span>1659          </button>16601661          <Show when={showVaultAdvanced()}>1662            <div class="peach-danger-card">1663              <div class="peach-danger-card-head">1664                <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1665                <h3 class="peach-danger-card-title">Replace vault from backup</h3>1666              </div>1667              <p class="peach-danger-card-copy">1668                Wipes the current vault and restores from a backup file.1669                Recoverable only if your backup is good.1670              </p>1671              <button1672                class="peach-danger-card-button"1673                onClick={() => props.onOpenFileImport?.()}1674              >1675                Choose backup file1676              </button>1677            </div>16781679            <Show when={props.onOpenPanic}>1680              <div class="peach-danger-card peach-danger-card--hazard">1681                <div class="peach-danger-card-head">1682                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1683                  <h3 class="peach-danger-card-title">Destroy vault</h3>1684                </div>1685                <p class="peach-danger-card-copy">1686                  Permanently wipes every entry, setting, and license on this1687                  device. There is no recovery.1688                </p>1689                <button1690                  class="peach-danger-card-button"1691                  onClick={() => props.onOpenPanic?.()}1692                >1693                  Begin destruction1694                </button>1695              </div>1696            </Show>1697          </Show>1698        </div>1699      </div>1700    );1701  }17021703  function renderSync() {1704    return (1705      <div class="peach-settings-body">1706        <div class="peach-settings-group">1707          <p class="peach-settings-sub-label" style="margin-top:0">SYNC STATUS</p>1708          <div1709            class="peach-settings-row peach-settings-row--meta"1710            style="align-items:center;gap:8px"1711            role="status"1712            aria-label={syncLightLabel()}1713            title={syncLightLabel()}1714          >1715            <span class={`stk-pip stk-pip--sync is-${syncLight()}`} aria-hidden="true" />1716            <span class="peach-settings-value" style="color:var(--peach-t2);line-height:1.5;flex:1;min-width:0;overflow-wrap:anywhere">1717              {syncFlash() ?? lastSyncLine()}1718            </span>1719          </div>1720          <Show when={pairedDevices().length > 0}>1721            <div class="peach-settings-row peach-settings-row--meta">1722              <span class="peach-settings-value" style="color:var(--peach-t2)">1723                {(syncSummary().entriesSent ?? 0) + (syncSummary().entriesReceived ?? 0) > 01724                  ? `${syncSummary().entriesSent ?? 0} sent · ${syncSummary().entriesReceived ?? 0} received`1725                  : ""}1726              </span>1727              <button1728                class="peach-btn peach-btn--subtle peach-btn--sm"1729                aria-label="Sync this vault now"1730                disabled={syncStatus() === "syncing"}1731                onClick={() => void syncNow()}1732              >{syncStatus() === "syncing" ? "syncing…" : "sync now"}</button>1733            </div>1734          </Show>17351736          <p class="peach-settings-sub-label">SECURE SYNC RESET</p>1737          <div class="peach-settings-row peach-settings-row--top">1738            <div class="peach-settings-row-main">1739              <span class="peach-settings-value" style="color:var(--peach-t1)">1740                {secureResetHeadline(generation()?.state)}1741              </span>1742              <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1743                {secureResetDetail(generation()?.state)}1744              </span>1745            </div>1746            <Show1747              when={1748                generation()?.state === "LEGACY_ACTIVE" ||1749                generation()?.state === "RESET_REQUIRED" ||1750                generation()?.state === "RESET_CONFIRMED"1751              }1752            >1753              <button1754                class="peach-btn peach-btn--subtle peach-btn--sm"1755                onClick={() => {1756                  setResetError(null);1757                  setResetPassword("");1758                  setResetOpen(true);1759                }}1760              >reset sync</button>1761            </Show>1762          </div>1763          <Show when={resetError()}>1764            <p1765              class="peach-settings-value"1766              role="alert"1767              style="color:var(--peach-danger,#e5805c);font-size:11px;line-height:1.5"1768            >{resetError()}</p>1769          </Show>1770          <Show when={resetOpen()}>1771            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">1772              <p class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1773                {SECURE_RESET_DISCLOSURE}1774              </p>1775              <input1776                class="peach-settings-input"1777                type="password"1778                placeholder="master password"1779                value={resetPassword()}1780                onInput={(e) => setResetPassword((e.target as HTMLInputElement).value)}1781                autocomplete="current-password"1782              />1783              <div style="display:flex;gap:4px">1784                <button1785                  class="peach-btn peach-btn--subtle peach-btn--sm"1786                  disabled={resetting()}1787                  onClick={() => void runSecureReset()}1788                >{resetting() ? "resetting…" : "reset"}</button>1789                <button1790                  class="peach-btn peach-btn--subtle peach-btn--sm"1791                  disabled={resetting()}1792                  onClick={() => setResetOpen(false)}1793                >cancel</button>1794              </div>1795            </div>1796          </Show>17971798          <p class="peach-settings-sub-label">ADD A DEVICE</p>1799          <button1800            class="peach-sync-pair-option"1801            onClick={() => props.onOpenSyncPairing?.("code")}1802          >1803            <span class="peach-sync-pair-option-icon" aria-hidden="true">1804              <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">1805                <rect x="6" y="2.5" width="8" height="15" rx="1.6" />1806                <path d="M9 15 L11 15" />1807              </svg>1808            </span>1809            <span class="peach-sync-pair-option-main">1810              <span class="peach-sync-pair-option-title">Show a code</span>1811              <span class="peach-sync-pair-option-sub">Scan it with Peach on your phone, then confirm one 8-digit code</span>1812            </span>1813            <span class="peach-sync-pair-option-chevron" aria-hidden="true">›</span>1814          </button>1815          <Show when={!isPro()}>1816            <p class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2);margin:8px 2px 0">1817              At least one device needs Peach Pro — pair with a Pro device and it carries over to here.1818            </p>1819          </Show>18201821          <Show when={pairedDevices().length > 0}>1822            <p class="peach-settings-sub-label">PAIRED DEVICES</p>1823            <For each={pairedDevices()}>1824              {(d) => (1825                <div class="peach-settings-row peach-settings-row--top">1826                  <div class="peach-settings-row-main">1827                    <Show1828                      when={renameDeviceId() === d.deviceId}1829                      fallback={<span>{d.localAlias ?? d.deviceName ?? "Unnamed device"}</span>}1830                    >1831                      <input1832                        class="peach-settings-input"1833                        aria-label={`Rename ${d.localAlias ?? d.deviceName ?? "device"}`}1834                        maxlength={64}1835                        value={renameDraft()}1836                        onInput={(event) => setRenameDraft(event.currentTarget.value)}1837                        onKeyDown={(event) => {1838                          if (event.key === "Enter") void renameDevice(d.deviceId);1839                          if (event.key === "Escape") setRenameDeviceId(null);1840                        }}1841                      />1842                    </Show>1843                    <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1844                      {deviceTypeLabel(d.deviceType)} · {syncRelativeTime(d.lastSyncedAt)}{isSyncStale(d) ? " · not seen recently" : ""}1845                    </span>1846                  </div>1847                  <Show1848                    when={confirmUnpair() === d.deviceId}1849                    fallback={1850                      <div style="display:flex;gap:4px;flex-shrink:0">1851                        <Show1852                          when={renameDeviceId() === d.deviceId}1853                          fallback={1854                            <button1855                              class="peach-btn peach-btn--subtle peach-btn--sm"1856                              onClick={() => {1857                                setRenameDraft(d.localAlias ?? d.deviceName ?? "");1858                                setRenameDeviceId(d.deviceId);1859                              }}1860                            >rename</button>1861                          }1862                        >1863                          <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => setRenameDeviceId(null)}>cancel</button>1864                          <button class="peach-btn peach-btn--subtle peach-btn--sm" disabled={!renameDraft().trim()} onClick={() => void renameDevice(d.deviceId)}>save</button>1865                        </Show>1866                        <button1867                          class="peach-btn peach-btn--subtle peach-btn--sm"1868                          onClick={() => setConfirmUnpair(d.deviceId)}1869                        >remove</button>1870                      </div>1871                    }1872                  >1873                    {/* HOLE 4: revocation stops this device receiving future1874                        changes. It does NOT secure the vault against it — that1875                        device may already hold the recovery words, which are1876                        master-password-equivalent and restore the whole vault1877                        anywhere, forever, with no sync involved. Say so here,1878                        rather than leaving the user falsely reassured. */}1879                    <div class="peach-unpair-confirm">1880                      <p class="peach-unpair-confirm-note">1881                        Stops this device receiving future changes. It doesn't1882                        lock it out of what it already has — if it's lost or1883                        stolen, also change your master password and generate1884                        new recovery words.1885                      </p>1886                      <div style="display:flex;gap:6px;justify-content:flex-end">1887                        <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => setConfirmUnpair(null)}>cancel</button>1888                        <button1889                          class="peach-btn peach-btn--sm"1890                          style="background:var(--peach-breach);border-color:var(--peach-breach)"1891                          onClick={() => void unpairDevice(d.deviceId)}1892                        >remove</button>1893                      </div>1894                    </div>1895                  </Show>1896                </div>1897              )}1898            </For>1899          </Show>19001901          <Show when={pendingVouched().length > 0}>1902            <p class="peach-settings-sub-label">1903              PENDING DEVICES1904              <span class="peach-pro-badge" style="background:#C8A840;color:#1a1a1a">{pendingVouched().length}</span>1905            </p>1906            <For each={pendingVouched()}>1907              {(dev) => (1908                <div class="peach-settings-row peach-settings-row--top">1909                  <div class="peach-settings-row-main">1910                    <span>{dev.deviceName}</span>1911                    <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1912                      Key {dev.longTermPublicKeyHex.slice(0, 12).match(/.{4}/g)?.join(" ")} ·1913                      requested {new Date(dev.receivedAt).toISOString().slice(0, 16).replace("T", " ")} UTC ·1914                      vouched by device {dev.vouchedByDeviceId}. Allowing it can read1915                      existing vault contents and all future changes.1916                    </span>1917                  </div>1918                  <div style="display:flex;gap:6px;flex-shrink:0">1919                    <button1920                      class="peach-btn peach-btn--sm"1921                      disabled={vouchedBusy() === dev.deviceId}1922                      onClick={() => void confirmVouched(dev.deviceId)}1923                    >Allow</button>1924                    <button1925                      class="peach-btn peach-btn--subtle peach-btn--sm"1926                      disabled={vouchedBusy() === dev.deviceId}1927                      onClick={() => void rejectVouched(dev.deviceId)}1928                    >Reject</button>1929                  </div>1930                </div>1931              )}1932            </For>1933          </Show>19341935          <button1936            class="peach-settings-advanced-toggle peach-sync-disclosure-toggle"1937            onClick={() => setShowSyncConnection((value) => !value)}1938            aria-expanded={showSyncConnection()}1939            aria-controls="peach-sync-connection"1940          >1941            <span>1942              <span class="peach-settings-sub-label" style="margin:0">CONNECTION</span>1943              <span class="peach-sync-disclosure-summary">Relay and privacy choices</span>1944            </span>1945            <span class="peach-settings-advanced-chevron">1946              {showSyncConnection() ? "▲" : "▼"}1947            </span>1948          </button>1949          <Show when={showSyncConnection()}>1950            <div id="peach-sync-connection" class="peach-sync-disclosure-content">1951              <SyncModeSelector />1952            </div>1953          </Show>19541955          <button1956            class="peach-settings-advanced-toggle peach-sync-disclosure-toggle"1957            onClick={() => setShowSyncRecovery((value) => !value)}1958            aria-expanded={showSyncRecovery()}1959            aria-controls="peach-sync-recovery"1960          >1961            <span>1962              <span class="peach-settings-sub-label" style="margin:0">RECOVERY &amp; SECURITY</span>1963              <span class="peach-sync-disclosure-summary">For a lost or stolen device</span>1964            </span>1965            <span class="peach-settings-advanced-chevron">1966              {showSyncRecovery() ? "▲" : "▼"}1967            </span>1968          </button>1969          <Show when={showSyncRecovery()}>1970            <div id="peach-sync-recovery" class="peach-sync-disclosure-content">1971              <Show1972                when={rotateConfirmOpen()}1973                fallback={1974                  <div class="peach-settings-row peach-settings-row--top">1975                    <div class="peach-settings-row-main">1976                      <span>Rotate sync identity</span>1977                      <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1978                        Disconnects every device and replaces the keys used for sync.1979                      </span>1980                    </div>1981                    <button1982                      class="peach-btn peach-btn--subtle peach-btn--sm"1983                      style="color:var(--peach-breach);border-color:color-mix(in srgb,var(--peach-breach) 40%,transparent)"1984                      onClick={() => { setRotateError(null); setRotateConfirmOpen(true); }}1985                    >rotate</button>1986                  </div>1987                }1988              >1989                <div class="peach-info-notice" style="margin-top:8px;border-color:color-mix(in srgb,var(--peach-breach) 40%,transparent)">1990                  This disconnects every paired device and changes your 12 recovery words. You'll be shown the new words next.1991                  <Show when={rotateError()}>1992                    <div style="color:var(--peach-breach);margin-top:6px">{rotateError()}</div>1993                  </Show>1994                  <div style="display:flex;gap:6px;margin-top:10px">1995                    <button1996                      class="peach-btn peach-btn--subtle peach-btn--sm"1997                      disabled={rotating()}1998                      onClick={() => setRotateConfirmOpen(false)}1999                    >Cancel</button>2000                    <button2001                      class="peach-btn peach-btn--sm"2002                      style="background:var(--peach-breach);border-color:var(--peach-breach)"2003                      disabled={rotating()}2004                      onClick={() => void doRotateSyncIdentity()}2005                    >{rotating() ? "Rotating…" : "Rotate sync identity"}</button>2006                  </div>2007                </div>2008              </Show>2009            </div>2010          </Show>2011        </div>2012      </div>2013    );2014  }20152016  function renderAppearance() {2017    const sizeOptions: Array<{ id: "medium" | "large"; label: string }> = [2018      { id: "medium", label: "Default" },2019      { id: "large", label: "Large" },2020    ];2021    return (2022      <div class="peach-settings-body">2023        <div class="peach-settings-group">2024          <p class="peach-settings-sub-label">THEME</p>2025          <div class="peach-settings-row peach-settings-row--meta">2026            <span>Theme</span>2027            <button2028              class="peach-btn peach-btn--subtle peach-btn--sm"2029              onClick={() =>2030                props.onThemeChange(props.theme === "dark" ? "light" : "dark")2031              }2032            >2033              {props.theme === "dark" ? "Switch to light" : "Switch to dark"}2034            </button>2035          </div>20362037          <p class="peach-settings-sub-label">ACCENT</p>2038          <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">2039            <span>Accent</span>2040            <div class="peach-accent-picker">2041              <For each={ACCENT_COLORS}>2042                {(color) => (2043                  <button2044                    class="peach-accent-swatch peach-swatch"2045                    classList={{ "is-active": props.accentColor === color.name }}2046                    style={{ background: color.hex }}2047                    onClick={() => void props.onAccentChange(color.name)}2048                    aria-label={color.name}2049                    title={color.name}2050                  />2051                )}2052              </For>2053            </div>2054          </div>20552056          <p class="peach-settings-sub-label">TEXT SIZE</p>2057          <div class="peach-settings-row peach-settings-row--meta">2058            <span>Size</span>2059            <div class="peach-pill-group">2060              <For each={sizeOptions}>2061                {(opt) => (2062                  <button2063                    class="peach-btn peach-btn--ghost peach-btn--sm"2064                    classList={{ "is-active": props.textSize === opt.id }}2065                    onClick={() => void props.onTextSizeChange(opt.id)}2066                  >2067                    {opt.label}2068                  </button>2069                )}2070              </For>2071            </div>2072          </div>20732074          <p class="peach-settings-sub-label">MOTION</p>2075          <div class="peach-settings-row peach-settings-row--meta">2076            <span>Reduce motion</span>2077            <button2078              class="peach-btn peach-btn--subtle peach-btn--sm"2079              onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}2080            >2081              {props.reduceMotion ? "On" : "Off"}2082            </button>2083          </div>2084        </div>2085      </div>2086    );2087  }20882089  function renderShortcuts() {2090    const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);2091    const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);20922093    function startCapture(action: ShortcutAction) {2094      setConflictMsg(null);2095      setCapturingAction(action);2096    }2097    function cancelCapture() {2098      setCapturingAction(null);2099      setConflictMsg(null);2100    }2101    function commitBinding(action: ShortcutAction, binding: Keybinding) {2102      const conflict = findConflict(props.shortcuts, binding, action);2103      if (conflict) {2104        const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);2105        setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);2106        return;2107      }2108      const next = { ...props.shortcuts, [action]: binding };2109      setCapturingAction(null);2110      setConflictMsg(null);2111      void props.onShortcutsChange(next);2112    }2113    function resetAction(action: ShortcutAction) {2114      const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };2115      void props.onShortcutsChange(next);2116    }2117    function resetAll() {2118      void props.onShortcutsChange({ ...DEFAULT_BINDINGS });2119    }21202121    // Capture-mode key handler — wired via ref onto the chip element so it2122    // catches the next non-modifier keypress and ignores other bindings.2123    function handleCaptureKey(e: KeyboardEvent) {2124      e.preventDefault();2125      e.stopPropagation();2126      if (e.key === "Escape") { cancelCapture(); return; }2127      const binding = keybindingFromEvent(e);2128      if (!binding) return; // bare modifier — keep waiting2129      const action = capturingAction();2130      if (action) commitBinding(action, binding);2131    }21322133    return (2134      <div class="peach-settings-body">2135        <div class="peach-settings-group peach-shortcuts-group">2136          <p class="peach-settings-sub-label">ENTRY VIEW</p>2137          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>2138            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}2139          </For>2140        </div>21412142        <div class="peach-settings-group peach-shortcuts-group">2143          <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>2144          <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>2145            {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}2146          </For>2147        </div>21482149        <Show when={conflictMsg()}>2150          <div class="peach-shortcuts-conflict">{conflictMsg()}</div>2151        </Show>21522153        <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">2154          <div class="peach-settings-row peach-settings-row--meta">2155            <span>Inline autofill on page</span>2156            <a2157              class="peach-settings-link"2158              href="chrome://extensions/shortcuts"2159              onClick={(e) => {2160                e.preventDefault();2161                chrome.tabs.create({ url: "chrome://extensions/shortcuts" });2162              }}2163            >2164              ⌘⇧P — customize ↗2165            </a>2166          </div>2167          <button2168            class="peach-btn peach-btn--subtle peach-btn--sm"2169            style="align-self:flex-start"2170            onClick={resetAll}2171          >2172            Reset all to defaults2173          </button>2174        </div>2175      </div>2176    );21772178    function renderRow(2179      desc: typeof SHORTCUT_DESCRIPTORS[number],2180      capturing: () => ShortcutAction | null,2181      onStart: (a: ShortcutAction) => void,2182      onCancel: () => void,2183      onReset: (a: ShortcutAction) => void,2184      onKey: (e: KeyboardEvent) => void,2185    ) {2186      const isActive = () => capturing() === desc.action;2187      const binding = () => props.shortcuts[desc.action];2188      const isCustom = () => {2189        const b = binding();2190        const d = DEFAULT_BINDINGS[desc.action];2191        return b.key !== d.key2192          || Boolean(b.ctrl)  !== Boolean(d.ctrl)2193          || Boolean(b.meta)  !== Boolean(d.meta)2194          || Boolean(b.shift) !== Boolean(d.shift)2195          || Boolean(b.alt)   !== Boolean(d.alt);2196      };2197      return (2198        <div class="peach-shortcut-row">2199          <span class="peach-shortcut-row-label">{desc.label}</span>2200          <button2201            class="peach-shortcut-chip"2202            classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}2203            onClick={() => isActive() ? onCancel() : onStart(desc.action)}2204            onKeyDown={(e) => { if (isActive()) onKey(e); }}2205            ref={(el) => {2206              // Auto-focus when entering capture mode so the keydown listener catches the next press.2207              if (el) {2208                queueMicrotask(() => { if (isActive()) el.focus(); });2209              }2210            }}2211          >2212            {isActive() ? "Press a key…" : formatKeybinding(binding())}2213          </button>2214          <Show when={isCustom() && !isActive()}>2215            <button2216              class="peach-shortcut-reset"2217              title="Reset to default"2218              onClick={() => onReset(desc.action)}2219            >2220              ↺2221            </button>2222          </Show>2223        </div>2224      );2225    }2226  }22272228  const PRO_FEATURES: Array<{2229    feature: ProFeature;2230    name: string;2231    desc: string;2232  }> = [2233    { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer" },2234    { feature: "codex", name: "Peach Codex", desc: "Restore from paper" },2235    { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },2236    { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure" },2237  ];22382239  const PRO_SECURITY_FEATURES: Array<{2240    feature: ProFeature;2241    name: string;2242    desc: string;2243  }> = [2244    { feature: "canary_entry", name: "Canary entries", desc: "Detects vault compromise" },2245    { feature: "duress_vault", name: "Duress vault", desc: "Decoy under coercion" },2246  ];22472248  const [licenseKey, setLicenseKey] = createSignal<string | null>(null);2249  const [showKey, setShowKey] = createSignal(false);22502251  const [deactivating, setDeactivating] = createSignal(false);22522253  async function handleDeactivateLicense() {2254    setDeactivating(true);2255    await sendMessage({ type: "DEACTIVATE_LICENSE" });2256    setLicenseInfo(null);2257    await props.onLicenseChange?.();2258    setDeactivating(false);2259  }22602261  async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {2262    setCanaryTestingId(entry.id);2263    setCanaryTestResult(null);2264    const res = await sendMessage({2265      type: "FIRE_CANARY_PING",2266      payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },2267    }) as { ok: boolean };2268    setCanaryTestResult({ id: entry.id, ok: res.ok });2269    setCanaryTestingId(null);2270    setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);2271  }22722273  function renderLicense() {2274    async function onActivate() {2275      await handleActivateLicense();2276      if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {2277        await props.onLicenseChange?.();2278      }2279    }22802281    return (2282      <div class="peach-settings-body">2283        <Show2284          when={isPro()}2285          fallback={2286                <div class="peach-pro-upgrade">2287                  <p class="peach-pro-eyebrow">PEACH PRO · ONE-TIME</p>22882289                  <h2 class="peach-pro-headline">2290                    Every<em>thing,</em><br />every<em>where.</em>2291                  </h2>22922293                  <p class="peach-pro-subtitle">2294                    One key unlocks every Pro feature — for the lifetime of the app.2295                  </p>22962297                  <div class="peach-pro-grid">2298                    <For each={PRO_FEATURES}>2299                      {(item, i) => (2300                        <div2301                          class="peach-pro-grid-item"2302                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}2303                        >2304                          <span class="peach-pro-grid-num">{String(i() + 1).padStart(2, "0")}</span>2305                          <span class="peach-pro-grid-name">{item.name}</span>2306                          <span class="peach-pro-grid-desc">{item.desc}</span>2307                        </div>2308                      )}2309                    </For>2310                  </div>23112312                  <p class="peach-pro-section-label">ADVANCED SECURITY</p>2313                  <div class="peach-pro-grid">2314                    <For each={PRO_SECURITY_FEATURES}>2315                      {(item, i) => (2316                        <div2317                          class="peach-pro-grid-item"2318                          classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}2319                        >2320                          <span class="peach-pro-grid-num">{String(PRO_FEATURES.length + i() + 1).padStart(2, "0")}</span>2321                          <span class="peach-pro-grid-name">{item.name}</span>2322                          <span class="peach-pro-grid-desc">{item.desc}</span>2323                        </div>2324                      )}2325                    </For>2326                  </div>23272328                  <div class="peach-pro-upgrade-actions">2329                    <p class="peach-pro-pricing">2330                      $39 <span class="peach-pro-pricing-sub">once · yours forever</span>2331                    </p>2332                    <a2333                      href="https://buy.polar.sh/polar_cl_IXSFIPojx67rcOTksn8LkH5mnU3osPHtYqBIl1eyuIz"2334                      target="_blank"2335                      rel="noopener noreferrer"2336                      class="peach-pro-upgrade-cta"2337                    >2338                      Get Peach Pro2339                    </a>2340                    <div class="peach-pro-key-divider"><span>ALREADY HAVE A KEY</span></div>2341                    <div class="peach-pro-key-inline">2342                      <input2343                        class="peach-pro-key-inline-input"2344                        type="text"2345                        placeholder="PEACH-XXXX-XXXX-XXXX"2346                        value={licenseKeyInput()}2347                        onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}2348                        onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}2349                        autocomplete="off"2350                        spellcheck={false}2351                      />2352                      <button2353                        class="peach-pro-key-apply"2354                        onClick={() => void onActivate()}2355                        disabled={activatingLicense() || !licenseKeyInput().trim()}2356                      >2357                        {activatingLicense() ? "…" : "APPLY"}2358                      </button>2359                    </div>2360                    <Show when={licenseError()}>2361                      <p class="peach-pro-key-error">{licenseError()}</p>2362                    </Show>2363                  </div>2364                </div>2365          }2366        >2367          <div class="peach-pro-active">2368            <div class="peach-pro-active-hero">2369              <button2370                class="peach-pro-active-mark-wrap"2371                type="button"2372                aria-label="Celebrate"2373                onClick={handleProCelebrationClick}2374              >2375                <PeachMark class="peach-pro-active-mark" />2376                <Show when={justActivated()}>2377                  <div class="peach-pro-sparks" aria-hidden="true">2378                    <span class="peach-pro-spark" style="--spark-x:0px;--spark-y:-54px;--spark-delay:0ms">✦</span>2379                    <span class="peach-pro-spark" style="--spark-x:38px;--spark-y:-38px;--spark-delay:50ms">✦</span>2380                    <span class="peach-pro-spark" style="--spark-x:54px;--spark-y:0px;--spark-delay:110ms">·</span>2381                    <span class="peach-pro-spark" style="--spark-x:38px;--spark-y:38px;--spark-delay:30ms">✦</span>2382                    <span class="peach-pro-spark" style="--spark-x:0px;--spark-y:54px;--spark-delay:80ms">·</span>2383                    <span class="peach-pro-spark" style="--spark-x:-38px;--spark-y:38px;--spark-delay:130ms">✦</span>2384                    <span class="peach-pro-spark" style="--spark-x:-54px;--spark-y:0px;--spark-delay:55ms">·</span>2385                    <span class="peach-pro-spark" style="--spark-x:-38px;--spark-y:-38px;--spark-delay:95ms">✦</span>2386                  </div>2387                </Show>2388              </button>2389              <h2 class="peach-pro-active-title">Peach Pro</h2>2390              <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>2391              <Show when={justActivated()}>2392                <p class="peach-pro-thankyou-text" aria-live="polite">thank you ✦</p>2393              </Show>2394            </div>23952396            <Show when={licenseKey()}>2397              <div class="peach-settings-license">2398                <span class="peach-settings-label">LICENSE KEY</span>2399                <div class="peach-settings-license-row">2400                  <Show2401                    when={showKey()}2402                    fallback={2403                      <span class="peach-settings-license-key peach-settings-license-key--masked" aria-label="License key hidden">2404                        ••••••••••••••••••••••••••••2405                      </span>2406                    }2407                  >2408                    <span class="peach-settings-license-key">{licenseKey()!}</span>2409                  </Show>2410                  <div class="peach-settings-license-actions">2411                    <button2412                      class="peach-settings-license-copy"2413                      title="Copy license key"2414                      onClick={() => copyLicenseKey(licenseKey()!)}2415                    >2416                      {copied() ? "✓" : "copy"}2417                    </button>2418                    <button2419                      class="peach-settings-license-toggle"2420                      aria-label={showKey() ? "Hide license key" : "Reveal license key"}2421                      title={showKey() ? "Hide" : "Reveal"}2422                      onClick={() => setShowKey(v => !v)}2423                    >2424                      <Show2425                        when={showKey()}2426                        fallback={2427                          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">2428                            <ellipse cx="7" cy="7" rx="5.5" ry="3.5" />2429                            <circle cx="7" cy="7" r="1.5" fill="currentColor" stroke="none" />2430                          </svg>2431                        }2432                      >2433                        <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">2434                          <ellipse cx="7" cy="7" rx="5.5" ry="3.5" />2435                          <circle cx="7" cy="7" r="1.5" fill="currentColor" stroke="none" />2436                          <line x1="2" y1="2" x2="12" y2="12" />2437                        </svg>2438                      </Show>2439                    </button>2440                  </div>2441                </div>2442              </div>2443            </Show>24442445            <div class="peach-pro-active-features">2446              <For each={[...PRO_FEATURES, ...PRO_SECURITY_FEATURES]}>2447                {(item) => (2448                  <div class="peach-pro-active-feature">2449                    <span class="peach-pro-active-feature-mark">✦</span>2450                    <span class="peach-pro-active-feature-name">{item.name}</span>2451                  </div>2452                )}2453              </For>2454            </div>24552456            <button2457              class="peach-pro-deactivate"2458              onClick={() => void handleDeactivateLicense()}2459              disabled={deactivating()}2460            >2461              {deactivating() ? "Deactivating…" : "Deactivate"}2462            </button>2463          </div>2464        </Show>2465      </div>2466    );2467  }24682469  function renderFeedback() {2470    return (2471      <div class="peach-settings-body">2472        <div class="peach-settings-group peach-feedback-group">2473          <Show2474            when={feedbackState() === 'sent'}2475            fallback={2476              <>2477                <p class="peach-feedback-intro">2478                  Help us improve Peach. Every report goes directly to our inbox.2479                </p>24802481                <p class="peach-settings-sub-label">TYPE</p>2482                <div class="peach-feedback-chips">2483                  <For each={FEEDBACK_CATEGORIES}>2484                    {(cat) => {2485                      const selected = () => fbCategory() === cat;2486                      return (2487                        <button2488                          type="button"2489                          class={`peach-feedback-chip ${selected() ? "is-active" : ""}`}2490                          aria-pressed={selected()}2491                          onClick={() => setFbCategory(cat)}2492                        >2493                          {cat}2494                        </button>2495                      );2496                    }}2497                  </For>2498                </div>24992500                <p class="peach-settings-sub-label">MESSAGE</p>2501                <textarea2502                  class="peach-feedback-textarea"2503                  placeholder="Describe the issue or idea..."2504                  maxLength={MAX_FEEDBACK_MESSAGE_LENGTH}2505                  value={fbMessage()}2506                  onInput={(e) => setFbMessage(e.currentTarget.value)}2507                />25082509                <p class="peach-feedback-meta">2510                  Version {version} · will be included automatically2511                </p>25122513                <button2514                  type="button"2515                  class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"2516                  disabled={!fbMessage().trim() || feedbackState() === 'sending'}2517                  onClick={() => void handleSubmitFeedback()}2518                >2519                  <Switch>2520                    <Match when={feedbackState() === 'idle'}>Send</Match>2521                    <Match when={feedbackState() === 'sending'}>Sending...</Match>2522                    <Match when={feedbackState() === 'error'}>Failed - try again</Match>2523                  </Switch>2524                </button>25252526                <p class="peach-feedback-anon">2527                  Sent anonymously · no account required2528                </p>2529              </>2530            }2531          >2532            <div class="peach-feedback-thanks" role="status" aria-live="polite">2533              <p class="peach-feedback-thanks-title">Thank you</p>2534              <p>2535                Your note is in the queue. Someone will process it soon.2536              </p>2537            </div>2538          </Show>2539        </div>2540      </div>2541    );2542  }25432544  const bodies: StackBodies = {2545    protection: renderSecurity(),2546    vault: renderVault(),2547    sync: renderSync(),2548    appearance: renderAppearance(),2549    shortcuts: renderShortcuts(),2550    pro: renderLicense(),2551    feedback: renderFeedback(),2552  };25532554  return (2555    <Show2556      when={showWhatsNew()}2557      fallback={2558    <Show2559      when={showNewRecoveryWords()}2560      fallback={2561        <div2562          class="peach-overlay-view peach-settings-overlay"2563          classList={{ "ov-enter-right": !props.isExiting, "ov-exit-right": !!props.isExiting }}2564          style="display:flex;flex-direction:column"2565        >2566          <StackDeck2567            active={activeCard()}2568            onActiveChange={setActiveCard}2569            onBack={props.onBack}2570            cards={cards()}2571            bodies={bodies}2572            version={version}2573            isFanning={isFanning()}2574            celebratingCardId={justActivated() ? "pro" : null}2575            isPro={isPro()}2576          />2577        </div>2578      }2579    >2580      <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />2581    </Show>2582      }2583    >2584      <div class="peach-overlay-view peach-settings-overlay">2585        <WhatsNewView mode="history" onContinue={() => setShowWhatsNew(false)} />2586      </div>2587    </Show>2588  );2589}