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: "Pair devices · sync is free", icon: iconSync })}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 <p class="peach-sync-network-note">1304 Sync works on your home and trusted networks. Some public or1305 corporate networks block device connections.1306 </p>13071308 {}1309 <button1310 class="peach-settings-row peach-settings-row--tappable"1311 onClick={() => props.onOpenSyncPairing?.()}1312 >1313 <span>Add a device</span>1314 <svg width="6" height="10" viewBox="0 0 6 10" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1315 <path d="M1 1l4 4-4 4" />1316 </svg>1317 </button>13181319 {}1320 <Show when={pairedDevices().length > 0}>1321 <div class="peach-history-list">1322 <For each={pairedDevices()}>1323 {(device) => (1324 <div class="peach-history-row">1325 <div class="peach-history-meta">1326 <span class="peach-history-time">{device.displayName}</span>1327 <span class="peach-history-sub">1328 {device.lastSyncAt1329 ? `Last synced ${formatRelativeTime(device.lastSyncAt)}`1330 : "Never synced"}1331 </span>1332 </div>1333 <button1334 class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1335 onClick={() => void handleRemoveDevice(device.deviceId)}1336 >1337 Remove1338 </button>1339 </div>1340 )}1341 </For>1342 </div>1343 </Show>13441345 {}1346 <Show1347 when={isPro()}1348 fallback={1349 <Show when={pairedDevices().length > 0}>1350 <div class="peach-sync-pro-hint">1351 <p class="peach-settings-hint">1352 Upgrade to Pro for automatic background sync across all your devices.1353 </p>1354 <button1355 class="peach-btn peach-btn--subtle peach-btn--sm"1356 onClick={() => setActiveSection("license")}1357 >1358 Upgrade to Pro1359 </button>1360 </div>1361 </Show>1362 }1363 >1364 <div class="peach-settings-row">1365 <span>Status</span>1366 <span class="peach-settings-value peach-settings-value--status">1367 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1368 {getStatusLabel(syncStatus())}1369 </span>1370 </div>1371 <div class="peach-settings-row peach-settings-row--meta">1372 <span>Last sync</span>1373 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1374 </div>1375 <div class="peach-settings-row peach-settings-row--meta">1376 <button1377 class="peach-btn peach-btn--subtle peach-btn--sm"1378 onClick={() => void handleSyncNow()}1379 disabled={syncStatus() === "not_trusted" || syncingNow()}1380 >1381 <Switch>1382 <Match when={syncingNow()}>Syncing…</Match>1383 <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1384 <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1385 <Match when={true}>Sync now</Match>1386 </Switch>1387 </button>1388 </div>1389 <p class="peach-settings-hint">1390 If you edit on multiple devices before syncing, the most recently1391 changed device wins. Use Vault History to recover older versions.1392 </p>13931394 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1395 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1396 <For each={trustedNetworks()}>1397 {(network) => (1398 <div class="peach-sync-device-row">1399 <span class="peach-sync-device-name">1400 {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1401 </span>1402 <Show1403 when={!network.isTailscale}1404 fallback={<span class="peach-sync-device-meta">Always trusted</span>}1405 >1406 <button1407 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1408 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1409 >1410 Remove1411 </button>1412 </Show>1413 </div>1414 )}1415 </For>1416 <div class="peach-settings-actions--row">1417 <Show when={!isCurrentNetworkTrusted()}>1418 <button1419 class="peach-btn peach-btn--subtle peach-btn--sm"1420 onClick={() => void handleMarkNetworkTrusted()}1421 disabled={markingNetwork()}1422 >1423 {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1424 </button>1425 </Show>1426 </div>1427 </div>14281429 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1430 <span class="peach-sync-section-label">Advanced</span>1431 <Show1432 when={resetIdentityConfirm()}1433 fallback={1434 <div class="peach-sync-advanced-row">1435 <div class="peach-sync-advanced-info">1436 <span class="peach-sync-advanced-name">Reset sync identity</span>1437 <span class="peach-sync-device-meta">1438 Generates a new keypair and unpairs every device.1439 </span>1440 </div>1441 <button1442 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1443 onClick={() => setResetIdentityConfirm(true)}1444 >1445 Reset1446 </button>1447 </div>1448 }1449 >1450 <div class="peach-sync-reset-confirm">1451 <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1452 <p class="peach-sync-reset-confirm-body">1453 This will unpair all devices. You will need to pair them again from scratch.1454 </p>1455 <div class="peach-settings-actions--row">1456 <button1457 class="peach-btn peach-btn--ghost peach-btn--sm"1458 onClick={() => setResetIdentityConfirm(false)}1459 disabled={resetIdentityRunning()}1460 >1461 Cancel1462 </button>1463 <button1464 class="peach-btn peach-btn--danger peach-btn--sm"1465 onClick={() => void handleResetSyncIdentity()}1466 disabled={resetIdentityRunning()}1467 >1468 {resetIdentityRunning() ? "Resetting..." : "Reset"}1469 </button>1470 </div>1471 </div>1472 </Show>1473 </div>1474 </Show>1475 </div>1476 </div>1477 );1478 }14791480 function renderAppearance() {1481 const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1482 { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1483 { id: "cormorant", label: "Cormorant", preview: "Aa" },1484 { id: "inter", label: "Inter", preview: "Aa" },1485 { id: "system", label: "System", preview: "Aa" },1486 ];1487 const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1488 { id: "small", label: "Small" },1489 { id: "medium", label: "Default" },1490 { id: "large", label: "Large" },1491 ];1492 return (1493 <div class="peach-settings-body">1494 <div class="peach-settings-group">1495 <p class="peach-settings-sub-label">THEME</p>1496 <div class="peach-settings-row peach-settings-row--meta">1497 <span>Theme</span>1498 <button1499 class="peach-btn peach-btn--subtle peach-btn--sm"1500 onClick={() =>1501 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1502 }1503 >1504 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1505 </button>1506 </div>15071508 <p class="peach-settings-sub-label">ACCENT</p>1509 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1510 <span>Accent</span>1511 <div class="peach-accent-picker">1512 <For each={ACCENT_COLORS}>1513 {(color) => (1514 <button1515 class="peach-accent-swatch"1516 classList={{ "is-active": props.accentColor === color.name }}1517 style={{ background: color.hex }}1518 onClick={() => void props.onAccentChange(color.name)}1519 aria-label={color.name}1520 title={color.name}1521 />1522 )}1523 </For>1524 </div>1525 </div>15261527 <p class="peach-settings-sub-label">DISPLAY FONT</p>1528 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1529 <span>Font</span>1530 <div class="peach-font-picker">1531 <For each={fontOptions}>1532 {(opt) => (1533 <button1534 class="peach-font-swatch"1535 classList={{ "is-active": props.displayFont === opt.id }}1536 data-font-preview={opt.id}1537 onClick={() => void props.onDisplayFontChange(opt.id)}1538 aria-label={opt.label}1539 title={opt.label}1540 >1541 <span class="peach-font-swatch-glyph">{opt.preview}</span>1542 <span class="peach-font-swatch-name">{opt.label}</span>1543 </button>1544 )}1545 </For>1546 </div>1547 </div>15481549 <p class="peach-settings-sub-label">TEXT SIZE</p>1550 <div class="peach-settings-row peach-settings-row--meta">1551 <span>Size</span>1552 <div class="peach-pill-group">1553 <For each={sizeOptions}>1554 {(opt) => (1555 <button1556 class="peach-btn peach-btn--ghost peach-btn--sm"1557 classList={{ "is-active": props.textSize === opt.id }}1558 onClick={() => void props.onTextSizeChange(opt.id)}1559 >1560 {opt.label}1561 </button>1562 )}1563 </For>1564 </div>1565 </div>15661567 <p class="peach-settings-sub-label">MOTION</p>1568 <div class="peach-settings-row peach-settings-row--meta">1569 <span>Reduce motion</span>1570 <button1571 class="peach-btn peach-btn--subtle peach-btn--sm"1572 onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1573 >1574 {props.reduceMotion ? "On" : "Off"}1575 </button>1576 </div>1577 </div>1578 </div>1579 );1580 }15811582 function renderShortcuts() {1583 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1584 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15851586 function startCapture(action: ShortcutAction) {1587 setConflictMsg(null);1588 setCapturingAction(action);1589 }1590 function cancelCapture() {1591 setCapturingAction(null);1592 setConflictMsg(null);1593 }1594 function commitBinding(action: ShortcutAction, binding: Keybinding) {1595 const conflict = findConflict(props.shortcuts, binding, action);1596 if (conflict) {1597 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1598 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1599 return;1600 }1601 const next = { ...props.shortcuts, [action]: binding };1602 setCapturingAction(null);1603 setConflictMsg(null);1604 void props.onShortcutsChange(next);1605 }1606 function resetAction(action: ShortcutAction) {1607 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1608 void props.onShortcutsChange(next);1609 }1610 function resetAll() {1611 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1612 }16131614 1615 1616 function handleCaptureKey(e: KeyboardEvent) {1617 e.preventDefault();1618 e.stopPropagation();1619 if (e.key === "Escape") { cancelCapture(); return; }1620 const binding = keybindingFromEvent(e);1621 if (!binding) return; 1622 const action = capturingAction();1623 if (action) commitBinding(action, binding);1624 }16251626 return (1627 <div class="peach-settings-body">1628 <div class="peach-settings-group peach-shortcuts-group">1629 <p class="peach-settings-sub-label">ENTRY VIEW</p>1630 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1631 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1632 </For>1633 </div>16341635 <div class="peach-settings-group peach-shortcuts-group">1636 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1637 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1638 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1639 </For>1640 </div>16411642 <Show when={conflictMsg()}>1643 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1644 </Show>16451646 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1647 <div class="peach-settings-row peach-settings-row--meta">1648 <span>Inline autofill on page</span>1649 <a1650 class="peach-settings-link"1651 href="chrome://extensions/shortcuts"1652 onClick={(e) => {1653 e.preventDefault();1654 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1655 }}1656 >1657 ⌘⇧P — customize ↗1658 </a>1659 </div>1660 <button1661 class="peach-btn peach-btn--subtle peach-btn--sm"1662 style="align-self:flex-start"1663 onClick={resetAll}1664 >1665 Reset all to defaults1666 </button>1667 </div>1668 </div>1669 );16701671 function renderRow(1672 desc: typeof SHORTCUT_DESCRIPTORS[number],1673 capturing: () => ShortcutAction | null,1674 onStart: (a: ShortcutAction) => void,1675 onCancel: () => void,1676 onReset: (a: ShortcutAction) => void,1677 onKey: (e: KeyboardEvent) => void,1678 ) {1679 const isActive = () => capturing() === desc.action;1680 const binding = () => props.shortcuts[desc.action];1681 const isCustom = () => {1682 const b = binding();1683 const d = DEFAULT_BINDINGS[desc.action];1684 return b.key !== d.key1685 || Boolean(b.ctrl) !== Boolean(d.ctrl)1686 || Boolean(b.meta) !== Boolean(d.meta)1687 || Boolean(b.shift) !== Boolean(d.shift)1688 || Boolean(b.alt) !== Boolean(d.alt);1689 };1690 return (1691 <div class="peach-shortcut-row">1692 <span class="peach-shortcut-row-label">{desc.label}</span>1693 <button1694 class="peach-shortcut-chip"1695 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1696 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1697 onKeyDown={(e) => { if (isActive()) onKey(e); }}1698 ref={(el) => {1699 1700 if (el) {1701 queueMicrotask(() => { if (isActive()) el.focus(); });1702 }1703 }}1704 >1705 {isActive() ? "Press a key…" : formatKeybinding(binding())}1706 </button>1707 <Show when={isCustom() && !isActive()}>1708 <button1709 class="peach-shortcut-reset"1710 title="Reset to default"1711 onClick={() => onReset(desc.action)}1712 >1713 ↺1714 </button>1715 </Show>1716 </div>1717 );1718 }1719 }17201721 const PRO_FEATURES: Array<{1722 feature: ProFeature;1723 name: string;1724 desc: string;1725 }> = [1726 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1727 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1728 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1729 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1730 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1731 ];17321733 const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17341735 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1736 const [deactivating, setDeactivating] = createSignal(false);17371738 async function handleDeactivateLicense() {1739 setDeactivating(true);1740 await sendMessage({ type: "DEACTIVATE_LICENSE" });1741 setLicenseInfo(null);1742 await props.onLicenseChange?.();1743 setDeactivating(false);1744 }17451746 async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1747 setCanaryTestingId(entry.id);1748 setCanaryTestResult(null);1749 const res = await sendMessage({1750 type: "FIRE_CANARY_PING",1751 payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1752 }) as { ok: boolean };1753 setCanaryTestResult({ id: entry.id, ok: res.ok });1754 setCanaryTestingId(null);1755 setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1756 }17571758 function renderLicense() {1759 async function onActivate() {1760 await handleActivateLicense();1761 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1762 await props.onLicenseChange?.();1763 }1764 }17651766 return (1767 <div class="peach-settings-body">1768 <Show1769 when={isPro()}1770 fallback={1771 <Show1772 when={showKeyEntry()}1773 fallback={1774 <div class="peach-pro-upgrade">1775 <div class="peach-pro-upgrade-hero">1776 <div class="peach-pro-upgrade-mark-wrap">1777 <PeachMark class="peach-pro-upgrade-mark" />1778 </div>1779 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1780 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1781 </div>17821783 <div class="peach-pro-features">1784 <For each={PRO_FEATURES}>1785 {(item) => (1786 <div1787 class="peach-pro-feature"1788 classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1789 >1790 <span class="peach-pro-feature-mark">✦</span>1791 <div class="peach-pro-feature-text">1792 <span class="peach-pro-feature-name">{item.name}</span>1793 <span class="peach-pro-feature-desc">{item.desc}</span>1794 </div>1795 </div>1796 )}1797 </For>1798 </div>17991800 <div class="peach-pro-upgrade-actions">1801 <p class="peach-pro-pricing">$39 · once · yours forever</p>1802 <a1803 href="https://peachpasswords.com/#pricing"1804 target="_blank"1805 rel="noopener noreferrer"1806 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1807 >1808 Get Peach Pro1809 </a>1810 <div class="peach-pro-key-divider"><span>or</span></div>1811 <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1812 Activate a license key1813 </button>1814 </div>1815 </div>1816 }1817 >1818 <div class="peach-pro-key-ceremony">1819 <LockRuneCanvas />1820 <p class="peach-pro-key-ceremony-label">Enter your license key</p>1821 <input1822 class="peach-underline-input peach-pro-key-input"1823 type="text"1824 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1825 value={licenseKeyInput()}1826 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1827 onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1828 autocomplete="off"1829 spellcheck={false}1830 />1831 <Show when={licenseError()}>1832 <p class="peach-pro-key-error">That key doesn't look right.</p>1833 </Show>1834 <button1835 class="peach-btn peach-btn--accent peach-pro-key-activate"1836 onClick={() => void onActivate()}1837 disabled={activatingLicense() || !licenseKeyInput().trim()}1838 >1839 {activatingLicense() ? "activating…" : "Activate"}1840 </button>1841 <button1842 class="peach-pro-key-cancel"1843 onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1844 >1845 cancel1846 </button>1847 </div>1848 </Show>1849 }1850 >1851 <div class="peach-pro-active">1852 <div class="peach-pro-active-hero">1853 <div class="peach-pro-active-mark-wrap">1854 <PeachMark class="peach-pro-active-mark" />1855 </div>1856 <h2 class="peach-pro-active-title">Peach Pro</h2>1857 <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1858 </div>18591860 <Show when={licenseKey()}>1861 <div class="peach-settings-license">1862 <span class="peach-settings-label">LICENSE KEY</span>1863 <div class="peach-settings-license-row">1864 <span class="peach-settings-license-key">{licenseKey()!}</span>1865 <button1866 class="peach-settings-license-copy"1867 onClick={() => copyLicenseKey(licenseKey()!)}1868 >1869 {copied() ? "✓" : "copy"}1870 </button>1871 </div>1872 </div>1873 </Show>18741875 <div class="peach-pro-active-features">1876 <For each={PRO_FEATURES}>1877 {(item) => (1878 <div class="peach-pro-active-feature">1879 <span class="peach-pro-active-feature-mark">✦</span>1880 <span class="peach-pro-active-feature-name">{item.name}</span>1881 </div>1882 )}1883 </For>1884 </div>18851886 <button1887 class="peach-pro-deactivate"1888 onClick={() => void handleDeactivateLicense()}1889 disabled={deactivating()}1890 >1891 {deactivating() ? "Deactivating…" : "Deactivate"}1892 </button>1893 </div>1894 </Show>1895 </div>1896 );1897 }18981899 function renderFeedback() {1900 return (1901 <div class="peach-settings-body">1902 <div class="peach-settings-group peach-feedback-group">1903 <p class="peach-feedback-intro">1904 Help us improve Peach. Every report goes directly to our inbox.1905 </p>19061907 <p class="peach-settings-sub-label">TYPE</p>1908 <div class="peach-feedback-chips">1909 <For each={FEEDBACK_CATEGORIES}>1910 {(cat) => (1911 <button1912 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1913 onClick={() => setFbCategory(cat)}1914 >1915 {cat}1916 </button>1917 )}1918 </For>1919 </div>19201921 <p class="peach-settings-sub-label">MESSAGE</p>1922 <textarea1923 class="peach-feedback-textarea"1924 placeholder="Describe the issue or idea..."1925 value={fbMessage()}1926 onInput={(e) => setFbMessage(e.currentTarget.value)}1927 />19281929 <p class="peach-feedback-meta">1930 Version {version} · will be included automatically1931 </p>19321933 <Show when={fbResult() === "success"}>1934 <p class="peach-feedback-result peach-feedback-result--ok">1935 Sent — thank you1936 </p>1937 </Show>1938 <Show when={fbResult() === "error"}>1939 <p class="peach-feedback-result peach-feedback-result--err">1940 Failed to send — try again1941 </p>1942 </Show>19431944 <button1945 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1946 onClick={() => void handleSubmitFeedback()}1947 disabled={fbSubmitting() || !fbMessage().trim()}1948 >1949 {fbSubmitting() ? "Sending..." : "Send feedback"}1950 </button>19511952 <p class="peach-feedback-anon">1953 Sent anonymously · no account required1954 </p>1955 </div>1956 </div>1957 );1958 }19591960 return (1961 <Show1962 when={showNewRecoveryWords()}1963 fallback={1964 <div class="peach-overlay-view ov-enter-right" style="display:flex">1965 <div class="peach-overlay-header">1966 <button1967 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1968 onClick={handleHeaderBack}1969 aria-label="Back"1970 >1971 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1972 <path d="M6 1L1 6L6 11" />1973 </svg>1974 </button>1975 <span class="peach-overlay-title">{currentTitle()}</span>1976 <div class="peach-overlay-spacer"></div>1977 </div>19781979 <Switch fallback={renderHome()}>1980 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1981 <Match when={activeSection() === "vault"}>{renderVault()}</Match>1982 <Match when={activeSection() === "sync"}>{renderSync()}</Match>1983 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1984 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1985 <Match when={activeSection() === "license"}>{renderLicense()}</Match>1986 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1987 </Switch>1988 </div>1989 }1990 >1991 <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />1992 </Show>1993 );1994}