1import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2import { sendMessage } from "../App";3import { PeachMark } from "../components/PeachMark";4import { LockRuneCanvas } from "../components/LockRuneCanvas";5import { RecoveryWordDisplayView } from "./RecoveryWordDisplay";6import type { Canary } from "../../shared/types";7import { ACCENT_COLORS } from "../../shared/types";8import type {9 AccentColorName,10 PasskeyProviderState,11 ProFeature,12 VaultSnapshot,13} from "../../shared/types";14import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types";15import {16 DEFAULT_BINDINGS,17 findConflict,18 formatKeybinding,19 keybindingFromEvent,20 SHORTCUT_DESCRIPTORS,21 type Keybinding,22 type ShortcutAction,23} from "../../shared/shortcuts";24import { isNativePlatform, nativePlugin } from "../../shared/native";2526interface LicenseInfo {27 valid: boolean;28 plan: "free" | "pro";29 issued?: string;30}3132interface SettingsProps {33 entriesCount: number;34 breachMonitoring: boolean;35 lockOnClose: boolean;36 autoLockMinutes: number;37 trashRetentionDays: number;38 theme: "dark" | "light";39 accentColor: AccentColorName;40 displayFont: "cormorant-italic" | "cormorant" | "inter" | "system";41 textSize: "small" | "medium" | "large";42 reduceMotion: boolean;43 isPro?: boolean;44 initialSection?: string | null;45 upgradeHighlight?: ProFeature | null;46 onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;47 onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;48 onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;49 onTrashRetentionChange: (days: number) => void | Promise<void>;50 onThemeChange: (theme: "dark" | "light") => void;51 onAccentChange: (name: AccentColorName) => void | Promise<void>;52 onDisplayFontChange: (font: "cormorant-italic" | "cormorant" | "inter" | "system") => void | Promise<void>;53 onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise<void>;54 onReduceMotionChange: (enabled: boolean) => void | Promise<void>;55 shortcuts: Record<ShortcutAction, Keybinding>;56 onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;57 onLicenseChange?: () => void | Promise<void>;58 onBack: () => void;59 onImport: () => void;60 onExport: () => void;61 onBreachCheckDone: () => void;62 onOpenHealth?: () => void;63 onOpenSyncPairing?: () => void;64 onOpenFileImport?: () => void;65 onOpenPanic?: () => void;66}6768type SectionName =69 | "security"70 | "vault"71 | "sync"72 | "appearance"73 | "shortcuts"74 | "license"75 | "feedback";7677const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7879function formatActivatedSince(issued: string | undefined): string {80 if (!issued) return "Active";81 try {82 const d = new Date(issued);83 if (Number.isNaN(d.getTime())) return "Active";84 return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;85 } catch {86 return "Active";87 }88}8990function isSectionName(value: string): value is SectionName {91 return (92 value === "security" ||93 value === "vault" ||94 value === "sync" ||95 value === "appearance" ||96 value === "shortcuts" ||97 value === "license" ||98 value === "feedback"99 );100}101102export function SettingsView(props: SettingsProps) {103 const version = chrome.runtime.getManifest().version;104 const initial = props.initialSection;105 const [activeSection, setActiveSection] = createSignal<SectionName | null>(106 initial && isSectionName(initial) ? (initial as SectionName) : null107 );108109 const [checkingBreaches, setCheckingBreaches] = createSignal(false);110 const [breachResult, setBreachResult] = createSignal<string | null>(null);111 const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);112 const [licenseKeyInput, setLicenseKeyInput] = createSignal("");113 const [licenseError, setLicenseError] = createSignal<string | null>(null);114 const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);115 const [activatingLicense, setActivatingLicense] = createSignal(false);116117 const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);118 const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);119 const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");120 const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);121 122 123 124 const [syncProStatus, setSyncProStatus] = createSignal(false);125 const isPro = () => props.isPro ?? syncProStatus();126 const [syncingNow, setSyncingNow] = createSignal(false);127 const [biometricSupported, setBiometricSupported] = createSignal(false);128 const [biometricStored, setBiometricStored] = createSignal(false);129 const [biometricBusy, setBiometricBusy] = createSignal(false);130 const [biometricError, setBiometricError] = createSignal<string | null>(null);131 const [syncFeedback, setSyncFeedback] = createSignal<132 { kind: "ok" | "err"; text: string } | null133 >(null);134 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);135 const [passkeySaving, setPasskeySaving] = createSignal(false);136137 const [dedupCount, setDedupCount] = createSignal<number | null>(null);138 const [dedupRunning, setDedupRunning] = createSignal(false);139 const [dedupDone, setDedupDone] = createSignal<number | null>(null);140141 const [declinedCardCount, setDeclinedCardCount] = createSignal(0);142143 144 145 146 const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);147 const [historyLoading, setHistoryLoading] = createSignal(false);148 const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);149150 async function loadVaultHistory() {151 setHistoryLoading(true);152 try {153 const res = await sendMessage({ type: "GET_VAULT_HISTORY" });154 const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];155 setHistorySnapshots(list);156 } finally {157 setHistoryLoading(false);158 }159 }160161 async function handleRestoreVaultSnapshot(id: string) {162 if (historyRestoring()) return;163 setHistoryRestoring(id);164 const res = await sendMessage({165 type: "RESTORE_VAULT_SNAPSHOT",166 payload: { snapshotId: id },167 });168 setHistoryRestoring(null);169 if ((res as { ok: boolean }).ok) {170 171 172 props.onBack();173 }174 }175176 function formatSnapshotTime(savedAt: number): string {177 const diff = Date.now() - savedAt;178 if (diff < 60_000) return "Just now";179 if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;180 const d = new Date(savedAt);181 const isToday = new Date().toDateString() === d.toDateString();182 const isYest =183 new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();184 const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });185 if (isToday) return `Today ${t}`;186 if (isYest) return `Yesterday ${t}`;187 return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;188 }189190 function reasonLabel(reason: VaultSnapshot["reason"]): string {191 return (192 {193 local_save: "saved",194 sync_received: "synced",195 import: "imported",196 restore: "restored",197 } as const198 )[reason] ?? reason;199 }200201 const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);202203 const [showChangePw, setShowChangePw] = createSignal(false);204 const [changePwCurrent, setChangePwCurrent] = createSignal("");205 const [changePwNew, setChangePwNew] = createSignal("");206 const [changePwConfirm, setChangePwConfirm] = createSignal("");207 const [changePwSaving, setChangePwSaving] = createSignal(false);208 const [changePwError, setChangePwError] = createSignal<string | null>(null);209 const [changePwSuccess, setChangePwSuccess] = createSignal(false);210 const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);211212 const [duressEnabled, setDuressEnabled] = createSignal(false);213 const [showDuressSetup, setShowDuressSetup] = createSignal(false);214 const [duressPassword, setDuressPassword] = createSignal("");215 const [duressConfirm, setDuressConfirm] = createSignal("");216 const [duressMainPassword, setDuressMainPassword] = createSignal("");217 const [duressSaving, setDuressSaving] = createSignal(false);218 const [duressError, setDuressError] = createSignal<string | null>(null);219 const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);220221 const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);222223 const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);224 const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);225226 const [fbCategory, setFbCategory] = createSignal("Bug report");227 const [fbMessage, setFbMessage] = createSignal("");228 const [fbSubmitting, setFbSubmitting] = createSignal(false);229 const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);230231 onMount(async () => {232 const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([233 sendMessage({ type: "GET_LICENSE" }),234 sendMessage({ type: "GET_SYNC_STATE" }),235 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),236 sendMessage({ type: "IS_DURESS_ENABLED" }),237 sendMessage({ type: "GET_SETTINGS" }),238 sendMessage({ type: "GET_LICENSE_KEY" }),239 ]);240241 242 243 void loadVaultHistory();244245 if ((licenseRes as { ok: boolean }).ok) {246 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);247 }248249 if ((syncRes as { ok: boolean }).ok) {250 const data = syncRes as { ok: boolean; data?: {251 pairedDevices?: PairedDevice[];252 trustedNetworks?: TrustedNetwork[];253 status?: SyncStatus;254 lastSyncAt?: number;255 isPro?: boolean;256 }};257 if (data.data) {258 setPairedDevices(data.data.pairedDevices ?? []);259 setTrustedNetworks(data.data.trustedNetworks ?? []);260 setSyncStatus(data.data.status ?? "offline");261 setLastSyncAt(data.data.lastSyncAt);262 setSyncProStatus(data.data.isPro ?? false);263 }264 }265266 if ((passkeyRes as { ok: boolean }).ok) {267 setPasskeyState(268 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null269 );270 }271272 if ((duressRes as { ok: boolean }).ok) {273 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);274 }275276 if ((settingsRes as { ok: boolean }).ok) {277 const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;278 setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);279 }280281 if ((keyRes as { ok: boolean }).ok) {282 setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);283 }284285 const entriesRes = await sendMessage({ type: "GET_ENTRIES" });286 if ((entriesRes as { ok: boolean }).ok) {287 const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];288 setCanaryEntries(289 entries290 .filter((e) => e.canary?.webhookUrl)291 .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))292 );293 }294295 chrome.runtime.onMessage.addListener((msg) => {296 if (msg.type === "SYNC_STATE_UPDATE") {297 setPairedDevices(msg.pairedDevices ?? []);298 setTrustedNetworks(msg.trustedNetworks ?? []);299 setSyncStatus(msg.status ?? "offline");300 setLastSyncAt(msg.lastSyncAt);301 setSyncProStatus(msg.isPro ?? false);302 }303 });304305 if (isNativePlatform()) {306 const biometric = nativePlugin<{ isAvailable: () => Promise<{ available: boolean }> }>("BiometricPlugin");307 const keystore = nativePlugin<{ hasMasterKey: () => Promise<{ exists: boolean }> }>("KeystorePlugin");308 try {309 if (biometric) {310 const a = await biometric.isAvailable();311 setBiometricSupported(a.available);312 }313 if (keystore) {314 const k = await keystore.hasMasterKey();315 setBiometricStored(k.exists);316 }317 } catch { }318 }319 });320321 async function handleEnableBiometrics() {322 setBiometricBusy(true);323 setBiometricError(null);324 try {325 const keyRes = (await sendMessage({ type: "GET_DERIVED_KEY" })) as {326 ok: boolean;327 data?: { masterKeyHex: string };328 error?: string;329 };330 if (!keyRes.ok || !keyRes.data?.masterKeyHex) {331 setBiometricError(keyRes.error === "vault_locked"332 ? "unlock the vault first"333 : "couldn't read derived key");334 return;335 }336 const keystore = nativePlugin<{ storeMasterKey: (p: { masterKeyHex: string }) => Promise<void> }>("KeystorePlugin");337 if (!keystore) {338 setBiometricError("biometric plugin unavailable");339 return;340 }341 await keystore.storeMasterKey({ masterKeyHex: keyRes.data.masterKeyHex });342 await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: true } });343 setBiometricStored(true);344 } catch (e) {345 setBiometricError(e instanceof Error ? e.message : "failed to enable biometrics");346 } finally {347 setBiometricBusy(false);348 }349 }350351 async function handleDisableBiometrics() {352 setBiometricBusy(true);353 setBiometricError(null);354 try {355 const keystore = nativePlugin<{ clearMasterKey: () => Promise<void> }>("KeystorePlugin");356 if (keystore) await keystore.clearMasterKey();357 await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: false } });358 setBiometricStored(false);359 } catch (e) {360 setBiometricError(e instanceof Error ? e.message : "failed to disable biometrics");361 } finally {362 setBiometricBusy(false);363 }364 }365366 async function handleChangePassword() {367 setChangePwError(null);368 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }369 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }370 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }371 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }372 setChangePwSaving(true);373 const res = await sendMessage({374 type: "CHANGE_PASSWORD",375 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },376 }) as { ok: boolean; error?: string };377 if (res.ok) {378 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");379 setShowChangePw(false);380 setShowNewRecoveryWords(true);381 } else {382 setChangePwError(res.error ?? "failed to change password");383 }384 setChangePwSaving(false);385 }386387 const [copied, setCopied] = createSignal(false);388389 async function copyLicenseKey(key: string) {390 await navigator.clipboard.writeText(key);391 setCopied(true);392 setTimeout(() => setCopied(false), 2000);393 }394395 async function handleActivateLicense() {396 const key = licenseKeyInput().trim();397 if (!key) return;398 setActivatingLicense(true);399 setLicenseError(null);400 setLicenseSuccess(null);401 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });402 setActivatingLicense(false);403 if ((res as { ok: boolean }).ok) {404 const data = (res as { ok: boolean; data?: LicenseInfo }).data;405 if (data?.valid) {406 setLicenseInfo(data);407 setLicenseKey(key);408 setLicenseSuccess("License activated!");409 setLicenseKeyInput("");410 } else {411 setLicenseError("Invalid license key");412 }413 } else {414 setLicenseError("Activation failed");415 }416 }417418 async function handleTogglePasskeys() {419 const current = passkeyState();420 if (!current || !current.supported) return;421 setPasskeySaving(true);422 const res = await sendMessage({423 type: "SET_PASSKEY_PROVIDER_ENABLED",424 payload: { enabled: !current.enabled },425 });426 setPasskeySaving(false);427 if ((res as { ok: boolean }).ok) {428 setPasskeyState(429 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null430 );431 }432 }433434 async function handleCheckAllBreaches() {435 setCheckingBreaches(true);436 setBreachResult(null);437 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });438 setCheckingBreaches(false);439 if ((res as { ok: boolean }).ok) {440 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};441 const breached = Object.values(data).filter((count) => count > 0).length;442 setBreachResult(443 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"444 );445 props.onBreachCheckDone();446 }447 }448449 async function handleSyncNow() {450 if (syncStatus() === "not_trusted") return;451 setSyncingNow(true);452 setSyncFeedback(null);453 const res = await sendMessage({ type: "TRIGGER_SYNC" });454 const ok = (res as { ok: boolean }).ok;455 if (ok) {456 const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;457 const count = data?.syncedCount ?? 0;458 setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });459 setLastSyncAt(Date.now());460 void refreshSyncState();461 } else {462 const error = (res as { error?: string }).error;463 const map: Record<string, string> = {464 no_paired_devices: "No paired devices",465 no_devices_reachable: "No devices found",466 vault_locked: "Vault is locked",467 pro_required: "Sync requires Pro",468 };469 setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });470 }471 setTimeout(() => {472 setSyncingNow(false);473 setSyncFeedback(null);474 }, 3500);475 }476477 async function refreshSyncState() {478 const res = await sendMessage({ type: "GET_SYNC_STATE" });479 const data = (res as { ok: boolean; data?: {480 pairedDevices?: PairedDevice[];481 trustedNetworks?: TrustedNetwork[];482 status?: SyncStatus;483 lastSyncAt?: number;484 }}).data;485 if (!data) return;486 setPairedDevices(data.pairedDevices ?? []);487 setTrustedNetworks(data.trustedNetworks ?? []);488 setSyncStatus(data.status ?? "offline");489 setLastSyncAt(data.lastSyncAt);490 }491492 async function handleRemoveDevice(deviceId: string) {493 setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));494 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });495 void refreshSyncState();496 }497498 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {499 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });500 void refreshSyncState();501 }502503 const [markingNetwork, setMarkingNetwork] = createSignal(false);504505 async function handleMarkNetworkTrusted() {506 if (markingNetwork()) return;507 setMarkingNetwork(true);508 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });509 await refreshSyncState();510 setMarkingNetwork(false);511 }512513 const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);514 const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);515516 async function handleResetSyncIdentity() {517 setResetIdentityRunning(true);518 const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });519 setResetIdentityRunning(false);520 if ((res as { ok: boolean }).ok) {521 setPairedDevices([]);522 setResetIdentityConfirm(false);523 }524 }525526 function formatRelativeTime(timestamp?: number): string {527 if (!timestamp) return "Never";528 const diff = Date.now() - timestamp;529 const minutes = Math.floor(diff / 60000);530 const hours = Math.floor(diff / 3600000);531 const days = Math.floor(diff / 86400000);532 if (minutes < 1) return "Just now";533 if (minutes < 60) return `${minutes}m ago`;534 if (hours < 24) return `${hours}h ago`;535 return `${days}d ago`;536 }537538 function getStatusColor(status: SyncStatus): string {539 switch (status) {540 case "synced": return "var(--peach-sync-ok)";541 case "syncing": return "var(--peach-sync-busy)";542 case "no_peers": return "var(--peach-sync-off)";543 case "not_trusted": return "var(--peach-sync-err)";544 case "offline": return "var(--peach-sync-off)";545 default: return "var(--peach-sync-off)";546 }547 }548549 function getStatusLabel(status: SyncStatus): string {550 switch (status) {551 case "synced": return "Synced";552 case "syncing": return "Syncing";553 case "no_peers": return "No peers visible";554 case "not_trusted": return "Not on a trusted network";555 case "offline": return "Offline";556 default: return "Unknown";557 }558 }559560 function isCurrentNetworkTrusted(): boolean {561 return trustedNetworks().length > 0;562 }563564 async function handleSetupDuress() {565 const mainPw = duressMainPassword().trim();566 const duressPw = duressPassword().trim();567 const confirmPw = duressConfirm().trim();568569 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }570 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }571 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }572 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }573574 setDuressSaving(true);575 setDuressError(null);576577 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };578 if (!verifyRes.ok || !verifyRes.data) {579 setDuressError("incorrect main password");580 setDuressSaving(false);581 return;582 }583584 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };585 setDuressSaving(false);586 if (res.ok) {587 setDuressEnabled(true);588 setShowDuressSetup(false);589 setDuressPassword("");590 setDuressConfirm("");591 setDuressMainPassword("");592 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");593 setTimeout(() => setDuressSuccess(null), 5000);594 } else {595 setDuressError(res.error ?? "failed to set duress password");596 }597 }598599 async function handleClearDeclinedCards() {600 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });601 setDeclinedCardCount(0);602 }603604 async function handleCountDuplicates() {605 setDedupRunning(true);606 setDedupDone(null);607 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };608 setDedupRunning(false);609 if (res.ok) setDedupCount(res.data ?? 0);610 }611612 async function handleDeduplicate() {613 setDedupRunning(true);614 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };615 setDedupRunning(false);616 if (res.ok) {617 setDedupDone(res.data ?? 0);618 setDedupCount(0);619 }620 }621622 async function handleDisableDuress() {623 const mainPw = duressMainPassword().trim();624 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }625626 setDuressSaving(true);627 setDuressError(null);628629 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };630 if (!verifyRes.ok || !verifyRes.data) {631 setDuressError("incorrect main password");632 setDuressSaving(false);633 return;634 }635636 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };637 setDuressSaving(false);638 if (res.ok) {639 setDuressEnabled(false);640 setShowDuressSetup(false);641 setDuressMainPassword("");642 setDuressSuccess("Duress password disabled.");643 setTimeout(() => setDuressSuccess(null), 4000);644 } else {645 setDuressError(res.error ?? "failed to disable duress password");646 }647 }648649 async function handleSubmitFeedback() {650 if (!fbMessage().trim() || fbSubmitting()) return;651 setFbSubmitting(true);652 setFbResult(null);653 const res = await sendMessage({654 type: "SUBMIT_FEEDBACK",655 payload: {656 category: fbCategory(),657 message: fbMessage().trim(),658 version,659 browser: navigator.userAgent,660 },661 }) as { ok: boolean; error?: string };662 setFbSubmitting(false);663 if (res.ok) {664 setFbResult("success");665 setFbMessage("");666 setTimeout(() => setFbResult(null), 4000);667 } else {668 setFbResult("error");669 }670 }671672 function handleHeaderBack() {673 if (activeSection() !== null) {674 setActiveSection(null);675 } else {676 props.onBack();677 }678 }679680 function currentTitle(): string {681 const s = activeSection();682 if (s === null) return "settings";683 if (s === "license") return "account";684 return s;685 }686687 function renderCatRow(opts: {688 section: SectionName;689 name: string;690 desc: string;691 icon: () => any;692 pro?: boolean;693 }) {694 return (695 <button696 class="peach-settings-cat-row"697 onClick={() => setActiveSection(opts.section)}698 >699 <div class="peach-settings-cat-icon">{opts.icon()}</div>700 <div class="peach-settings-cat-content">701 <span class="peach-settings-cat-name">702 {opts.name}703 <Show when={opts.pro && !isPro()}>704 <span class="peach-pro-mark" aria-label="Pro feature">✦</span>705 </Show>706 </span>707 <span class="peach-settings-cat-desc">{opts.desc}</span>708 </div>709 <svg710 class="peach-settings-cat-chevron"711 width="6" height="10" viewBox="0 0 6 10" fill="none"712 stroke="currentColor" stroke-width="1.1"713 stroke-linecap="round" stroke-linejoin="round"714 >715 <path d="M1 1l4 4-4 4" />716 </svg>717 </button>718 );719 }720721 const iconShield = () => (722 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">723 <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>724 </svg>725 );726 const iconKey = () => (727 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">728 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>729 </svg>730 );731 const iconLock = () => (732 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">733 <rect x="3" y="7" width="10" height="7" rx="1.5"/>734 <path d="M6 7V5a3 3 0 016 0v2"/>735 </svg>736 );737 const iconSync = () => (738 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">739 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>740 <path d="M14 6l-2 2-2-2"/>741 <path d="M2 10l2-2 2 2"/>742 </svg>743 );744 const iconSun = () => (745 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">746 <circle cx="8" cy="8" r="3"/>747 <path d="M8 2v1m0 10v1M2 8h1m10 0h1m-2.1-4.1-.7.7M4.8 11.3l-.7.7m0-7.7.7.7m6.5 6.3-.7.7"/>748 </svg>749 );750 const iconKeys = () => (751 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">752 <rect x="2" y="5" width="5" height="4" rx="1"/>753 <rect x="9" y="7" width="5" height="4" rx="1"/>754 <path d="M7 7h2m-2 2h2"/>755 </svg>756 );757 const iconBubble = () => (758 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">759 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>760 </svg>761 );762763 function renderHome() {764 return (765 <div class="peach-settings-body">766 <p class="peach-settings-nav-label">PROTECTION</p>767 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}768 {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}769770 <p class="peach-settings-nav-label">VAULT</p>771 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}772 {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}773774 <p class="peach-settings-nav-label">PREFERENCES</p>775 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}776 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}777778 <p class="peach-settings-nav-label">SUPPORT</p>779 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}780781 <div class="peach-settings-footer">782 <span>Peach {version}</span>783 <span class="peach-divider"></span>784 <span>Browser extension</span>785 </div>786 </div>787 );788 }789790 function renderSecurity() {791 return (792 <div class="peach-settings-body">793 <div class="peach-settings-group">794 <p class="peach-settings-sub-label">LOCKING</p>795 <div class="peach-settings-row">796 <span>Lock on close</span>797 <button798 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}799 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}800 aria-label="Toggle lock on close"801 >802 <span class="peach-toggle-knob"></span>803 </button>804 </div>805 <div class="peach-settings-row">806 <span>Auto-lock after</span>807 <select808 class="peach-settings-select"809 value={String(props.autoLockMinutes)}810 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}811 >812 <option value="1">1 minute</option>813 <option value="5">5 minutes</option>814 <option value="15">15 minutes</option>815 <option value="30">30 minutes</option>816 <option value="0">Never</option>817 </select>818 </div>819820 <Show when={isNativePlatform() && biometricSupported()}>821 <p class="peach-settings-sub-label">BIOMETRICS</p>822 <div class="peach-settings-row">823 <span>Unlock with biometrics</span>824 <button825 class={`peach-toggle ${biometricStored() ? "is-on" : ""}`}826 onClick={() => void (biometricStored() ? handleDisableBiometrics() : handleEnableBiometrics())}827 disabled={biometricBusy()}828 aria-label="Toggle biometric unlock"829 >830 <span class="peach-toggle-knob"></span>831 </button>832 </div>833 <Show when={biometricError()}>834 <div class="peach-settings-row peach-settings-row--meta">835 <span class="peach-hint peach-hint-error">{biometricError()}</span>836 </div>837 </Show>838 </Show>839840 <p class="peach-settings-sub-label">BREACH PROTECTION</p>841 <div class="peach-settings-row">842 <span>Breach monitoring</span>843 <button844 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}845 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}846 aria-label="Toggle breach monitoring"847 >848 <span class="peach-toggle-knob"></span>849 </button>850 </div>851 <div class="peach-settings-row peach-settings-row--meta">852 <span>Check all passwords</span>853 <button854 class="peach-btn peach-btn--subtle peach-btn--sm"855 onClick={() => void handleCheckAllBreaches()}856 disabled={checkingBreaches()}857 >858 {checkingBreaches() ? "checking..." : "run"}859 </button>860 </div>861 <Show when={breachResult()}>862 <div class="peach-settings-row peach-settings-row--meta">863 <span>Last check</span>864 <span class="peach-settings-value">{breachResult()}</span>865 </div>866 </Show>867868 <p class="peach-settings-sub-label">MASTER PASSWORD</p>869 <div class="peach-settings-row peach-settings-row--meta">870 <span>Master password</span>871 <button872 class="peach-btn peach-btn--subtle peach-btn--sm"873 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}874 >875 {showChangePw() ? "cancel" : "change"}876 </button>877 </div>878 <Show when={showChangePw()}>879 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">880 <input881 class="peach-settings-input"882 type="password"883 placeholder="current password"884 value={changePwCurrent()}885 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}886 autocomplete="current-password"887 />888 <input889 class="peach-settings-input"890 type="password"891 placeholder="new password (min 12 chars)"892 value={changePwNew()}893 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}894 autocomplete="new-password"895 />896 <input897 class="peach-settings-input"898 type="password"899 placeholder="confirm new password"900 value={changePwConfirm()}901 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}902 autocomplete="new-password"903 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}904 />905 <Show when={changePwError()}>906 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>907 </Show>908 <Show when={changePwSuccess()}>909 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>910 </Show>911 <button912 class="peach-btn peach-btn--accent peach-btn--sm"913 style="align-self:flex-start"914 onClick={() => void handleChangePassword()}915 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}916 >917 {changePwSaving() ? "saving…" : "save new password"}918 </button>919 </div>920 </Show>921922 <p class="peach-settings-sub-label">DURESS PASSWORD</p>923 <div class="peach-settings-row peach-settings-row--top">924 <div class="peach-settings-row-main">925 <span>Duress password</span>926 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">927 If entered under coercion, shows a decoy empty vault instead of your real one.928 </span>929 </div>930 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>931 {duressEnabled() ? "on" : "off"}932 </div>933 </div>934935 <Show when={duressEnabled() && !showDuressSetup()}>936 <div class="peach-settings-row peach-settings-row--meta">937 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">938 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.939 </span>940 </div>941 </Show>942943 <Show when={duressSuccess()}>944 <div class="peach-settings-row peach-settings-row--meta">945 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>946 </div>947 </Show>948949 <Show when={!showDuressSetup()}>950 <div class="peach-settings-row peach-settings-row--meta">951 <button952 class="peach-btn peach-btn--subtle peach-btn--sm"953 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}954 >955 {duressEnabled() ? "Change / disable" : "Set up"}956 </button>957 </div>958 </Show>959960 <Show when={showDuressSetup()}>961 <div class="peach-duress-form">962 <label class="peach-label">main password (to confirm)</label>963 <input964 class="peach-settings-input"965 type="password"966 placeholder="your current master password"967 autocomplete="current-password"968 value={duressMainPassword()}969 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}970 />971 <Show when={!duressEnabled()}>972 <label class="peach-label">new duress password</label>973 <input974 class="peach-settings-input"975 type="password"976 placeholder="duress password (min 8 chars)"977 autocomplete="new-password"978 value={duressPassword()}979 onInput={(e) => setDuressPassword(e.currentTarget.value)}980 />981 <label class="peach-label">confirm duress password</label>982 <input983 class="peach-settings-input"984 type="password"985 placeholder="confirm duress password"986 autocomplete="new-password"987 value={duressConfirm()}988 onInput={(e) => setDuressConfirm(e.currentTarget.value)}989 />990 </Show>991992 <Show when={duressError()}>993 <p class="peach-hint peach-hint-error">{duressError()}</p>994 </Show>995996 <div class="peach-duress-actions">997 <button998 class="peach-btn peach-btn--subtle peach-btn--sm"999 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}1000 >1001 cancel1002 </button>1003 <Show when={duressEnabled()}>1004 <button1005 class="peach-btn peach-btn--ghost peach-btn--sm"1006 style="color:var(--peach-breach)"1007 onClick={() => void handleDisableDuress()}1008 disabled={duressSaving()}1009 >1010 {duressSaving() ? "disabling..." : "disable duress"}1011 </button>1012 </Show>1013 <Show when={!duressEnabled()}>1014 <button1015 class="peach-btn peach-btn--accent peach-btn--sm"1016 onClick={() => void handleSetupDuress()}1017 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}1018 >1019 {duressSaving() ? "saving..." : "save"}1020 </button>1021 </Show>1022 </div>1023 </div>1024 </Show>10251026 <p class="peach-settings-sub-label">CANARY ENTRIES</p>1027 <div class="peach-settings-row peach-settings-row--top">1028 <div class="peach-settings-row-main">1029 <span>Canary entries</span>1030 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1031 Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.1032 </span>1033 </div>1034 <span class="peach-settings-value" style="flex-shrink:0">1035 {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}1036 </span>1037 </div>1038 <Show when={canaryEntries().length > 0}>1039 <For each={canaryEntries()}>1040 {(entry) => (1041 <div class="peach-settings-row peach-settings-row--meta">1042 <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>1043 <button1044 class="peach-btn peach-btn--subtle peach-btn--sm"1045 onClick={() => void handleTestCanary(entry)}1046 disabled={canaryTestingId() === entry.id}1047 style="flex-shrink:0"1048 >1049 {canaryTestingId() === entry.id1050 ? "sending…"1051 : canaryTestResult()?.id === entry.id1052 ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1053 : "test"}1054 </button>1055 </div>1056 )}1057 </For>1058 </Show>10591060 <p class="peach-settings-sub-label">PASSKEYS</p>1061 <div class="peach-settings-row peach-settings-row--top">1062 <div class="peach-settings-row-main">1063 <span>Intercept passkey requests</span>1064 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1065 Peach can handle passkey sign-in and creation on supported Chromium browsers.1066 </span>1067 </div>1068 <button1069 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1070 onClick={() => void handleTogglePasskeys()}1071 aria-label="Toggle passkey interception"1072 disabled={!passkeyState()?.supported || passkeySaving()}1073 >1074 <span class="peach-toggle-knob"></span>1075 </button>1076 </div>1077 <div class="peach-settings-row peach-settings-row--meta">1078 <span>Status</span>1079 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1080 </div>1081 <div class="peach-settings-row peach-settings-row--meta">1082 <span>Browser</span>1083 <span class="peach-settings-value">1084 {passkeyState()?.browserLabel ?? "Unknown"}1085 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1086 </span>1087 </div>1088 <div class="peach-info-notice" style="margin-top:8px">1089 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1090 </div>1091 <Show when={passkeyState()?.conflict}>1092 <div class="peach-settings-row peach-settings-row--meta">1093 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1094 </div>1095 </Show>1096 </div>1097 </div>1098 );1099 }11001101 function renderVault() {1102 return (1103 <div class="peach-settings-body">1104 <div class="peach-settings-group">1105 <p class="peach-settings-sub-label">ENTRIES</p>1106 <div class="peach-settings-row peach-settings-row--meta">1107 <span>Entries</span>1108 <span class="peach-settings-value">{props.entriesCount}</span>1109 </div>1110 <div class="peach-settings-row peach-settings-row--meta">1111 <span>Duplicates</span>1112 <Show when={dedupDone() !== null} fallback={1113 <button1114 class="peach-btn peach-btn--subtle peach-btn--sm"1115 onClick={() => void handleCountDuplicates()}1116 disabled={dedupRunning()}1117 >1118 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1119 </button>1120 }>1121 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1122 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1123 </span>1124 </Show>1125 </div>1126 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1127 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1128 <div class="peach-restore-warning">1129 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1130 </div>1131 <div style="display:flex;gap:6px">1132 <button1133 class="peach-btn peach-btn--subtle peach-btn--sm"1134 onClick={() => setDedupCount(null)}1135 >1136 cancel1137 </button>1138 <button1139 class="peach-btn peach-btn--accent peach-btn--sm"1140 onClick={() => void handleDeduplicate()}1141 disabled={dedupRunning()}1142 >1143 {dedupRunning() ? "removing..." : "remove duplicates"}1144 </button>1145 </div>1146 </div>1147 </Show>11481149 <p class="peach-settings-sub-label">MAINTENANCE</p>1150 <div class="peach-settings-row peach-settings-row--meta">1151 <span>Trash auto-delete</span>1152 <select1153 class="peach-settings-select"1154 value={String(props.trashRetentionDays)}1155 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1156 >1157 <option value="7">7 days</option>1158 <option value="30">30 days</option>1159 <option value="90">90 days</option>1160 <option value="0">Never</option>1161 </select>1162 </div>1163 <div class="peach-settings-row peach-settings-row--meta">1164 <span>Vault Health</span>1165 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1166 open1167 </button>1168 </div>1169 <Show when={declinedCardCount() > 0}>1170 <div class="peach-settings-row peach-settings-row--meta">1171 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1172 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1173 Clear1174 </button>1175 </div>1176 </Show>11771178 <p class="peach-settings-sub-label">DATA</p>1179 <div class="peach-settings-row peach-settings-row--meta">1180 <span>Import passwords</span>1181 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1182 open1183 </button>1184 </div>1185 <div class="peach-settings-row peach-settings-row--meta">1186 <span>Export backup</span>1187 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1188 open1189 </button>1190 </div>11911192 <p class="peach-settings-sub-label">VAULT HISTORY</p>1193 <Show when={historyLoading()}>1194 <p class="peach-settings-hint">Loading…</p>1195 </Show>1196 <Show when={!historyLoading() && historySnapshots().length === 0}>1197 <p class="peach-settings-hint">1198 No snapshots yet. Peach saves automatically before every change —1199 saves, syncs, and imports.1200 </p>1201 </Show>1202 <Show when={!historyLoading() && historySnapshots().length > 0}>1203 <p class="peach-settings-hint">1204 Restoring locks your vault. Unlock with the password that1205 encrypted that snapshot to confirm.1206 </p>1207 <div class="peach-history-list">1208 <For each={historySnapshots()}>1209 {(snap) => (1210 <div class="peach-history-row">1211 <div class="peach-history-meta">1212 <span class="peach-history-time">1213 {formatSnapshotTime(snap.savedAt)}1214 </span>1215 <span class="peach-history-sub">1216 {[1217 snap.entryCount !== null1218 ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1219 : null,1220 reasonLabel(snap.reason),1221 ]1222 .filter(Boolean)1223 .join(" · ")}1224 </span>1225 </div>1226 <button1227 class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1228 historyRestoring() === snap.id ? " is-busy" : ""1229 }`}1230 onClick={() => void handleRestoreVaultSnapshot(snap.id)}1231 disabled={historyRestoring() !== null}1232 >1233 <Show when={historyRestoring() === snap.id} fallback="Restore">1234 …1235 </Show>1236 </button>1237 </div>1238 )}1239 </For>1240 </div>1241 <p class="peach-history-footer">1242 {historySnapshots().length} of 10 snapshots1243 </p>1244 </Show>12451246 <button1247 class="peach-settings-advanced-toggle"1248 onClick={() => setShowVaultAdvanced((v) => !v)}1249 aria-expanded={showVaultAdvanced()}1250 >1251 <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1252 <span class="peach-settings-advanced-chevron">1253 {showVaultAdvanced() ? "▲" : "▼"}1254 </span>1255 </button>12561257 <Show when={showVaultAdvanced()}>1258 <div class="peach-danger-card">1259 <div class="peach-danger-card-head">1260 <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1261 <h3 class="peach-danger-card-title">Replace vault from backup</h3>1262 </div>1263 <p class="peach-danger-card-copy">1264 Wipes the current vault and restores from a backup file.1265 Recoverable only if your backup is good.1266 </p>1267 <button1268 class="peach-danger-card-button"1269 onClick={() => props.onOpenFileImport?.()}1270 >1271 Choose backup file1272 </button>1273 </div>12741275 <Show when={props.onOpenPanic}>1276 <div class="peach-danger-card peach-danger-card--hazard">1277 <div class="peach-danger-card-head">1278 <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1279 <h3 class="peach-danger-card-title">Destroy vault</h3>1280 </div>1281 <p class="peach-danger-card-copy">1282 Permanently wipes every entry, setting, and license on this1283 device. There is no recovery.1284 </p>1285 <button1286 class="peach-danger-card-button"1287 onClick={() => props.onOpenPanic?.()}1288 >1289 Begin destruction1290 </button>1291 </div>1292 </Show>1293 </Show>1294 </div>1295 </div>1296 );1297 }12981299 function renderSync() {1300 return (1301 <div class="peach-settings-body">1302 <div class="peach-settings-group">1303 {}1304 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1305 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1306 <Show when={pairedDevices().length === 0}>1307 <p class="peach-settings-hint">1308 No paired devices yet. Add a device to start syncing.1309 </p>1310 </Show>1311 <Show when={pairedDevices().length > 0}>1312 <div class="peach-history-list">1313 <For each={pairedDevices()}>1314 {(device) => (1315 <div class="peach-history-row">1316 <div class="peach-history-meta">1317 <span class="peach-history-time">{device.displayName}</span>1318 <span class="peach-history-sub">1319 {device.lastSyncAt1320 ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1321 : "Never synced"}1322 </span>1323 </div>1324 <button1325 class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1326 onClick={() => void handleRemoveDevice(device.deviceId)}1327 >1328 Remove1329 </button>1330 </div>1331 )}1332 </For>1333 </div>1334 </Show>1335 <div class="peach-settings-actions--row">1336 <button1337 class="peach-btn peach-btn--subtle peach-btn--sm"1338 onClick={() => props.onOpenSyncPairing?.()}1339 >1340 Add a device1341 </button>1342 </div>1343 </div>13441345 {}1346 <Show1347 when={!isPro()}1348 fallback={1349 <>1350 <div class="peach-settings-row">1351 <span>Status</span>1352 <span class="peach-settings-value peach-settings-value--status">1353 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1354 {getStatusLabel(syncStatus())}1355 </span>1356 </div>1357 <div class="peach-settings-row peach-settings-row--meta">1358 <span>Last sync</span>1359 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1360 </div>1361 <div class="peach-settings-row peach-settings-row--meta">1362 <button1363 class="peach-btn peach-btn--subtle peach-btn--sm"1364 onClick={() => void handleSyncNow()}1365 disabled={syncStatus() === "not_trusted" || syncingNow()}1366 >1367 <Switch>1368 <Match when={syncingNow()}>Syncing…</Match>1369 <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1370 <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1371 <Match when={true}>Sync now</Match>1372 </Switch>1373 </button>1374 </div>1375 <p class="peach-settings-hint">1376 If you edit on multiple devices before syncing, the most recently1377 changed device wins. Use Vault History above to recover older1378 versions if needed.1379 </p>13801381 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1382 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1383 <For each={trustedNetworks()}>1384 {(network) => (1385 <div class="peach-sync-device-row">1386 <span class="peach-sync-device-name">1387 {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1388 </span>1389 <Show1390 when={!network.isTailscale}1391 fallback={<span class="peach-sync-device-meta">Always trusted</span>}1392 >1393 <button1394 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1395 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1396 >1397 Remove1398 </button>1399 </Show>1400 </div>1401 )}1402 </For>1403 <div class="peach-settings-actions--row">1404 <Show when={!isCurrentNetworkTrusted()}>1405 <button1406 class="peach-btn peach-btn--subtle peach-btn--sm"1407 onClick={() => void handleMarkNetworkTrusted()}1408 disabled={markingNetwork()}1409 >1410 {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1411 </button>1412 </Show>1413 </div>1414 </div>14151416 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1417 <span class="peach-sync-section-label">Advanced</span>1418 <Show1419 when={resetIdentityConfirm()}1420 fallback={1421 <div class="peach-sync-advanced-row">1422 <div class="peach-sync-advanced-info">1423 <span class="peach-sync-advanced-name">Reset sync identity</span>1424 <span class="peach-sync-device-meta">1425 Generates a new keypair and unpairs every device.1426 </span>1427 </div>1428 <button1429 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1430 onClick={() => setResetIdentityConfirm(true)}1431 >1432 Reset1433 </button>1434 </div>1435 }1436 >1437 <div class="peach-sync-reset-confirm">1438 <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1439 <p class="peach-sync-reset-confirm-body">1440 This will unpair all devices. You will need to pair them again from scratch.1441 </p>1442 <div class="peach-settings-actions--row">1443 <button1444 class="peach-btn peach-btn--ghost peach-btn--sm"1445 onClick={() => setResetIdentityConfirm(false)}1446 disabled={resetIdentityRunning()}1447 >1448 Cancel1449 </button>1450 <button1451 class="peach-btn peach-btn--danger peach-btn--sm"1452 onClick={() => void handleResetSyncIdentity()}1453 disabled={resetIdentityRunning()}1454 >1455 {resetIdentityRunning() ? "Resetting..." : "Reset"}1456 </button>1457 </div>1458 </div>1459 </Show>1460 </div>1461 </>1462 }1463 >1464 <Show when={pairedDevices().length > 0}>1465 <div class="peach-sync-pro-hint">1466 <p class="peach-settings-hint">1467 Upgrade to Pro for automatic background sync across all your devices.1468 </p>1469 <button1470 class="peach-btn peach-btn--subtle peach-btn--sm"1471 onClick={() => setActiveSection("license")}1472 >1473 Upgrade to Pro1474 </button>1475 </div>1476 </Show>1477 </Show>1478 </div>1479 </div>1480 );1481 }14821483 function renderAppearance() {1484 const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1485 { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1486 { id: "cormorant", label: "Cormorant", preview: "Aa" },1487 { id: "inter", label: "Inter", preview: "Aa" },1488 { id: "system", label: "System", preview: "Aa" },1489 ];1490 const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1491 { id: "small", label: "Small" },1492 { id: "medium", label: "Default" },1493 { id: "large", label: "Large" },1494 ];1495 return (1496 <div class="peach-settings-body">1497 <div class="peach-settings-group">1498 <p class="peach-settings-sub-label">THEME</p>1499 <div class="peach-settings-row peach-settings-row--meta">1500 <span>Theme</span>1501 <button1502 class="peach-btn peach-btn--subtle peach-btn--sm"1503 onClick={() =>1504 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1505 }1506 >1507 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1508 </button>1509 </div>15101511 <p class="peach-settings-sub-label">ACCENT</p>1512 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1513 <span>Accent</span>1514 <div class="peach-accent-picker">1515 <For each={ACCENT_COLORS}>1516 {(color) => (1517 <button1518 class="peach-accent-swatch"1519 classList={{ "is-active": props.accentColor === color.name }}1520 style={{ background: color.hex }}1521 onClick={() => void props.onAccentChange(color.name)}1522 aria-label={color.name}1523 title={color.name}1524 />1525 )}1526 </For>1527 </div>1528 </div>15291530 <p class="peach-settings-sub-label">DISPLAY FONT</p>1531 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1532 <span>Font</span>1533 <div class="peach-font-picker">1534 <For each={fontOptions}>1535 {(opt) => (1536 <button1537 class="peach-font-swatch"1538 classList={{ "is-active": props.displayFont === opt.id }}1539 data-font-preview={opt.id}1540 onClick={() => void props.onDisplayFontChange(opt.id)}1541 aria-label={opt.label}1542 title={opt.label}1543 >1544 <span class="peach-font-swatch-glyph">{opt.preview}</span>1545 <span class="peach-font-swatch-name">{opt.label}</span>1546 </button>1547 )}1548 </For>1549 </div>1550 </div>15511552 <p class="peach-settings-sub-label">TEXT SIZE</p>1553 <div class="peach-settings-row peach-settings-row--meta">1554 <span>Size</span>1555 <div class="peach-pill-group">1556 <For each={sizeOptions}>1557 {(opt) => (1558 <button1559 class="peach-btn peach-btn--ghost peach-btn--sm"1560 classList={{ "is-active": props.textSize === opt.id }}1561 onClick={() => void props.onTextSizeChange(opt.id)}1562 >1563 {opt.label}1564 </button>1565 )}1566 </For>1567 </div>1568 </div>15691570 <p class="peach-settings-sub-label">MOTION</p>1571 <div class="peach-settings-row peach-settings-row--meta">1572 <span>Reduce motion</span>1573 <button1574 class="peach-btn peach-btn--subtle peach-btn--sm"1575 onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1576 >1577 {props.reduceMotion ? "On" : "Off"}1578 </button>1579 </div>1580 </div>1581 </div>1582 );1583 }15841585 function renderShortcuts() {1586 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1587 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15881589 function startCapture(action: ShortcutAction) {1590 setConflictMsg(null);1591 setCapturingAction(action);1592 }1593 function cancelCapture() {1594 setCapturingAction(null);1595 setConflictMsg(null);1596 }1597 function commitBinding(action: ShortcutAction, binding: Keybinding) {1598 const conflict = findConflict(props.shortcuts, binding, action);1599 if (conflict) {1600 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1601 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1602 return;1603 }1604 const next = { ...props.shortcuts, [action]: binding };1605 setCapturingAction(null);1606 setConflictMsg(null);1607 void props.onShortcutsChange(next);1608 }1609 function resetAction(action: ShortcutAction) {1610 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1611 void props.onShortcutsChange(next);1612 }1613 function resetAll() {1614 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1615 }16161617 1618 1619 function handleCaptureKey(e: KeyboardEvent) {1620 e.preventDefault();1621 e.stopPropagation();1622 if (e.key === "Escape") { cancelCapture(); return; }1623 const binding = keybindingFromEvent(e);1624 if (!binding) return; 1625 const action = capturingAction();1626 if (action) commitBinding(action, binding);1627 }16281629 return (1630 <div class="peach-settings-body">1631 <div class="peach-settings-group peach-shortcuts-group">1632 <p class="peach-settings-sub-label">ENTRY VIEW</p>1633 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1634 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1635 </For>1636 </div>16371638 <div class="peach-settings-group peach-shortcuts-group">1639 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1640 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1641 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1642 </For>1643 </div>16441645 <Show when={conflictMsg()}>1646 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1647 </Show>16481649 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1650 <div class="peach-settings-row peach-settings-row--meta">1651 <span>Inline autofill on page</span>1652 <a1653 class="peach-settings-link"1654 href="chrome://extensions/shortcuts"1655 onClick={(e) => {1656 e.preventDefault();1657 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1658 }}1659 >1660 ⌘⇧P — customize ↗1661 </a>1662 </div>1663 <button1664 class="peach-btn peach-btn--subtle peach-btn--sm"1665 style="align-self:flex-start"1666 onClick={resetAll}1667 >1668 Reset all to defaults1669 </button>1670 </div>1671 </div>1672 );16731674 function renderRow(1675 desc: typeof SHORTCUT_DESCRIPTORS[number],1676 capturing: () => ShortcutAction | null,1677 onStart: (a: ShortcutAction) => void,1678 onCancel: () => void,1679 onReset: (a: ShortcutAction) => void,1680 onKey: (e: KeyboardEvent) => void,1681 ) {1682 const isActive = () => capturing() === desc.action;1683 const binding = () => props.shortcuts[desc.action];1684 const isCustom = () => {1685 const b = binding();1686 const d = DEFAULT_BINDINGS[desc.action];1687 return b.key !== d.key1688 || Boolean(b.ctrl) !== Boolean(d.ctrl)1689 || Boolean(b.meta) !== Boolean(d.meta)1690 || Boolean(b.shift) !== Boolean(d.shift)1691 || Boolean(b.alt) !== Boolean(d.alt);1692 };1693 return (1694 <div class="peach-shortcut-row">1695 <span class="peach-shortcut-row-label">{desc.label}</span>1696 <button1697 class="peach-shortcut-chip"1698 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1699 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1700 onKeyDown={(e) => { if (isActive()) onKey(e); }}1701 ref={(el) => {1702 1703 if (el) {1704 queueMicrotask(() => { if (isActive()) el.focus(); });1705 }1706 }}1707 >1708 {isActive() ? "Press a key…" : formatKeybinding(binding())}1709 </button>1710 <Show when={isCustom() && !isActive()}>1711 <button1712 class="peach-shortcut-reset"1713 title="Reset to default"1714 onClick={() => onReset(desc.action)}1715 >1716 ↺1717 </button>1718 </Show>1719 </div>1720 );1721 }1722 }17231724 const PRO_FEATURES: Array<{1725 feature: ProFeature;1726 name: string;1727 desc: string;1728 }> = [1729 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1730 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1731 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1732 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1733 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1734 ];17351736 const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17371738 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1739 const [deactivating, setDeactivating] = createSignal(false);17401741 async function handleDeactivateLicense() {1742 setDeactivating(true);1743 await sendMessage({ type: "DEACTIVATE_LICENSE" });1744 setLicenseInfo(null);1745 await props.onLicenseChange?.();1746 setDeactivating(false);1747 }17481749 async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1750 setCanaryTestingId(entry.id);1751 setCanaryTestResult(null);1752 const res = await sendMessage({1753 type: "FIRE_CANARY_PING",1754 payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1755 }) as { ok: boolean };1756 setCanaryTestResult({ id: entry.id, ok: res.ok });1757 setCanaryTestingId(null);1758 setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1759 }17601761 function renderLicense() {1762 async function onActivate() {1763 await handleActivateLicense();1764 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1765 await props.onLicenseChange?.();1766 }1767 }17681769 return (1770 <div class="peach-settings-body">1771 <Show1772 when={isPro()}1773 fallback={1774 <Show1775 when={showKeyEntry()}1776 fallback={1777 <div class="peach-pro-upgrade">1778 <div class="peach-pro-upgrade-hero">1779 <div class="peach-pro-upgrade-mark-wrap">1780 <PeachMark class="peach-pro-upgrade-mark" />1781 </div>1782 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1783 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1784 </div>17851786 <div class="peach-pro-features">1787 <For each={PRO_FEATURES}>1788 {(item) => (1789 <div1790 class="peach-pro-feature"1791 classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1792 >1793 <span class="peach-pro-feature-mark">✦</span>1794 <div class="peach-pro-feature-text">1795 <span class="peach-pro-feature-name">{item.name}</span>1796 <span class="peach-pro-feature-desc">{item.desc}</span>1797 </div>1798 </div>1799 )}1800 </For>1801 </div>18021803 <div class="peach-pro-upgrade-actions">1804 <p class="peach-pro-pricing">$39 · once · yours forever</p>1805 <a1806 href="https://peachpasswords.com/#pricing"1807 target="_blank"1808 rel="noopener noreferrer"1809 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1810 >1811 Get Peach Pro1812 </a>1813 <div class="peach-pro-key-divider"><span>or</span></div>1814 <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1815 Activate a license key1816 </button>1817 </div>1818 </div>1819 }1820 >1821 <div class="peach-pro-key-ceremony">1822 <LockRuneCanvas />1823 <p class="peach-pro-key-ceremony-label">Enter your license key</p>1824 <input1825 class="peach-underline-input peach-pro-key-input"1826 type="text"1827 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1828 value={licenseKeyInput()}1829 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1830 onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1831 autocomplete="off"1832 spellcheck={false}1833 />1834 <Show when={licenseError()}>1835 <p class="peach-pro-key-error">That key doesn't look right.</p>1836 </Show>1837 <button1838 class="peach-btn peach-btn--accent peach-pro-key-activate"1839 onClick={() => void onActivate()}1840 disabled={activatingLicense() || !licenseKeyInput().trim()}1841 >1842 {activatingLicense() ? "activating…" : "Activate"}1843 </button>1844 <button1845 class="peach-pro-key-cancel"1846 onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1847 >1848 cancel1849 </button>1850 </div>1851 </Show>1852 }1853 >1854 <div class="peach-pro-active">1855 <div class="peach-pro-active-hero">1856 <div class="peach-pro-active-mark-wrap">1857 <PeachMark class="peach-pro-active-mark" />1858 </div>1859 <h2 class="peach-pro-active-title">Peach Pro</h2>1860 <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1861 </div>18621863 <Show when={licenseKey()}>1864 <div class="peach-settings-license">1865 <span class="peach-settings-label">LICENSE KEY</span>1866 <div class="peach-settings-license-row">1867 <span class="peach-settings-license-key">{licenseKey()!}</span>1868 <button1869 class="peach-settings-license-copy"1870 onClick={() => copyLicenseKey(licenseKey()!)}1871 >1872 {copied() ? "✓" : "copy"}1873 </button>1874 </div>1875 </div>1876 </Show>18771878 <div class="peach-pro-active-features">1879 <For each={PRO_FEATURES}>1880 {(item) => (1881 <div class="peach-pro-active-feature">1882 <span class="peach-pro-active-feature-mark">✦</span>1883 <span class="peach-pro-active-feature-name">{item.name}</span>1884 </div>1885 )}1886 </For>1887 </div>18881889 <button1890 class="peach-pro-deactivate"1891 onClick={() => void handleDeactivateLicense()}1892 disabled={deactivating()}1893 >1894 {deactivating() ? "Deactivating…" : "Deactivate"}1895 </button>1896 </div>1897 </Show>1898 </div>1899 );1900 }19011902 function renderFeedback() {1903 return (1904 <div class="peach-settings-body">1905 <div class="peach-settings-group peach-feedback-group">1906 <p class="peach-feedback-intro">1907 Help us improve Peach. Every report goes directly to our inbox.1908 </p>19091910 <p class="peach-settings-sub-label">TYPE</p>1911 <div class="peach-feedback-chips">1912 <For each={FEEDBACK_CATEGORIES}>1913 {(cat) => (1914 <button1915 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1916 onClick={() => setFbCategory(cat)}1917 >1918 {cat}1919 </button>1920 )}1921 </For>1922 </div>19231924 <p class="peach-settings-sub-label">MESSAGE</p>1925 <textarea1926 class="peach-feedback-textarea"1927 placeholder="Describe the issue or idea..."1928 value={fbMessage()}1929 onInput={(e) => setFbMessage(e.currentTarget.value)}1930 />19311932 <p class="peach-feedback-meta">1933 Version {version} · will be included automatically1934 </p>19351936 <Show when={fbResult() === "success"}>1937 <p class="peach-feedback-result peach-feedback-result--ok">1938 Sent — thank you1939 </p>1940 </Show>1941 <Show when={fbResult() === "error"}>1942 <p class="peach-feedback-result peach-feedback-result--err">1943 Failed to send — try again1944 </p>1945 </Show>19461947 <button1948 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1949 onClick={() => void handleSubmitFeedback()}1950 disabled={fbSubmitting() || !fbMessage().trim()}1951 >1952 {fbSubmitting() ? "Sending..." : "Send feedback"}1953 </button>19541955 <p class="peach-feedback-anon">1956 Sent anonymously · no account required1957 </p>1958 </div>1959 </div>1960 );1961 }19621963 return (1964 <Show1965 when={showNewRecoveryWords()}1966 fallback={1967 <div class="peach-overlay-view ov-enter-right" style="display:flex">1968 <div class="peach-overlay-header">1969 <button1970 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1971 onClick={handleHeaderBack}1972 aria-label="Back"1973 >1974 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1975 <path d="M6 1L1 6L6 11" />1976 </svg>1977 </button>1978 <span class="peach-overlay-title">{currentTitle()}</span>1979 <div class="peach-overlay-spacer"></div>1980 </div>19811982 <Switch fallback={renderHome()}>1983 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1984 <Match when={activeSection() === "vault"}>{renderVault()}</Match>1985 <Match when={activeSection() === "sync"}>{renderSync()}</Match>1986 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1987 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1988 <Match when={activeSection() === "license"}>{renderLicense()}</Match>1989 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1990 </Switch>1991 </div>1992 }1993 >1994 <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />1995 </Show>1996 );1997}