1import { createEffect, 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";25import { getHasPaired } from "../../background/sync-storage";26import { SyncInfoSheet } from "../components/SyncInfoSheet";2728interface LicenseInfo {29 valid: boolean;30 plan: "free" | "pro";31 issued?: string;32}3334interface SettingsProps {35 entriesCount: number;36 breachMonitoring: boolean;37 lockOnClose: boolean;38 autoLockMinutes: number;39 trashRetentionDays: number;40 theme: "dark" | "light";41 accentColor: AccentColorName;42 displayFont: "cormorant-italic" | "cormorant" | "inter" | "system";43 textSize: "small" | "medium" | "large";44 reduceMotion: boolean;45 isPro?: boolean;46 initialSection?: string | null;47 upgradeHighlight?: ProFeature | null;48 onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;49 onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;50 onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;51 onTrashRetentionChange: (days: number) => void | Promise<void>;52 onThemeChange: (theme: "dark" | "light") => void;53 onAccentChange: (name: AccentColorName) => void | Promise<void>;54 onDisplayFontChange: (font: "cormorant-italic" | "cormorant" | "inter" | "system") => void | Promise<void>;55 onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise<void>;56 onReduceMotionChange: (enabled: boolean) => void | Promise<void>;57 shortcuts: Record<ShortcutAction, Keybinding>;58 onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;59 onLicenseChange?: () => void | Promise<void>;60 onBack: () => void;61 onImport: () => void;62 onExport: () => void;63 onBreachCheckDone: () => void;64 onOpenHealth?: () => void;65 onOpenSyncPairing?: () => void;66 onOpenFileImport?: () => void;67 onOpenPanic?: () => void;68}6970type SectionName =71 | "security"72 | "vault"73 | "sync"74 | "appearance"75 | "shortcuts"76 | "license"77 | "feedback";7879const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];8081function formatActivatedSince(issued: string | undefined): string {82 if (!issued) return "Active";83 try {84 const d = new Date(issued);85 if (Number.isNaN(d.getTime())) return "Active";86 return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;87 } catch {88 return "Active";89 }90}9192function isSectionName(value: string): value is SectionName {93 return (94 value === "security" ||95 value === "vault" ||96 value === "sync" ||97 value === "appearance" ||98 value === "shortcuts" ||99 value === "license" ||100 value === "feedback"101 );102}103104export function SettingsView(props: SettingsProps) {105 const version = chrome.runtime.getManifest().version;106 const initial = props.initialSection;107 const [activeSection, setActiveSection] = createSignal<SectionName | null>(108 initial && isSectionName(initial) ? (initial as SectionName) : null109 );110111 const [checkingBreaches, setCheckingBreaches] = createSignal(false);112 const [breachResult, setBreachResult] = createSignal<string | null>(null);113 const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);114 const [licenseKeyInput, setLicenseKeyInput] = createSignal("");115 const [licenseError, setLicenseError] = createSignal<string | null>(null);116 const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);117 const [activatingLicense, setActivatingLicense] = createSignal(false);118119 const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);120 const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);121 const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");122 const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);123 124 125 126 const [syncProStatus, setSyncProStatus] = createSignal(false);127 const isPro = () => props.isPro ?? syncProStatus();128 const [hasPaired, setHasPairedLocal] = createSignal(false);129 const [showSyncInfoSheet, setShowSyncInfoSheet] = createSignal(false);130 const [syncingNow, setSyncingNow] = createSignal(false);131 const [biometricSupported, setBiometricSupported] = createSignal(false);132 const [biometricStored, setBiometricStored] = createSignal(false);133 const [biometricBusy, setBiometricBusy] = createSignal(false);134 const [biometricError, setBiometricError] = createSignal<string | null>(null);135 const [syncFeedback, setSyncFeedback] = createSignal<136 { kind: "ok" | "err"; text: string } | null137 >(null);138 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);139 const [passkeySaving, setPasskeySaving] = createSignal(false);140141 const [dedupCount, setDedupCount] = createSignal<number | null>(null);142 const [dedupRunning, setDedupRunning] = createSignal(false);143 const [dedupDone, setDedupDone] = createSignal<number | null>(null);144145 const [declinedCardCount, setDeclinedCardCount] = createSignal(0);146147 148 149 150 const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);151 const [historyLoading, setHistoryLoading] = createSignal(false);152 const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);153154 async function loadVaultHistory() {155 setHistoryLoading(true);156 try {157 const res = await sendMessage({ type: "GET_VAULT_HISTORY" });158 const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];159 setHistorySnapshots(list);160 } finally {161 setHistoryLoading(false);162 }163 }164165 async function handleRestoreVaultSnapshot(id: string) {166 if (historyRestoring()) return;167 setHistoryRestoring(id);168 const res = await sendMessage({169 type: "RESTORE_VAULT_SNAPSHOT",170 payload: { snapshotId: id },171 });172 setHistoryRestoring(null);173 if ((res as { ok: boolean }).ok) {174 175 176 props.onBack();177 }178 }179180 function formatSnapshotTime(savedAt: number): string {181 const diff = Date.now() - savedAt;182 if (diff < 60_000) return "Just now";183 if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;184 const d = new Date(savedAt);185 const isToday = new Date().toDateString() === d.toDateString();186 const isYest =187 new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();188 const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });189 if (isToday) return `Today ${t}`;190 if (isYest) return `Yesterday ${t}`;191 return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;192 }193194 function reasonLabel(reason: VaultSnapshot["reason"]): string {195 return (196 {197 local_save: "saved",198 sync_received: "synced",199 import: "imported",200 restore: "restored",201 } as const202 )[reason] ?? reason;203 }204205 const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);206207 const [showChangePw, setShowChangePw] = createSignal(false);208 const [changePwCurrent, setChangePwCurrent] = createSignal("");209 const [changePwNew, setChangePwNew] = createSignal("");210 const [changePwConfirm, setChangePwConfirm] = createSignal("");211 const [changePwSaving, setChangePwSaving] = createSignal(false);212 const [changePwError, setChangePwError] = createSignal<string | null>(null);213 const [changePwSuccess, setChangePwSuccess] = createSignal(false);214 const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);215216 const [duressEnabled, setDuressEnabled] = createSignal(false);217 const [showDuressSetup, setShowDuressSetup] = createSignal(false);218 const [duressPassword, setDuressPassword] = createSignal("");219 const [duressConfirm, setDuressConfirm] = createSignal("");220 const [duressMainPassword, setDuressMainPassword] = createSignal("");221 const [duressSaving, setDuressSaving] = createSignal(false);222 const [duressError, setDuressError] = createSignal<string | null>(null);223 const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);224225 const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);226227 const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);228 const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);229230 const [fbCategory, setFbCategory] = createSignal("Bug report");231 const [fbMessage, setFbMessage] = createSignal("");232 const [fbSubmitting, setFbSubmitting] = createSignal(false);233 const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);234235 onMount(async () => {236 const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes, pairedFlag] = await Promise.all([237 sendMessage({ type: "GET_LICENSE" }),238 sendMessage({ type: "GET_SYNC_STATE" }),239 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),240 sendMessage({ type: "IS_DURESS_ENABLED" }),241 sendMessage({ type: "GET_SETTINGS" }),242 sendMessage({ type: "GET_LICENSE_KEY" }),243 getHasPaired(),244 ]);245246 setHasPairedLocal(pairedFlag);247248 249 250 void loadVaultHistory();251252 if ((licenseRes as { ok: boolean }).ok) {253 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);254 }255256 if ((syncRes as { ok: boolean }).ok) {257 const data = syncRes as { ok: boolean; data?: {258 pairedDevices?: PairedDevice[];259 trustedNetworks?: TrustedNetwork[];260 status?: SyncStatus;261 lastSyncAt?: number;262 isPro?: boolean;263 }};264 if (data.data) {265 setPairedDevices(data.data.pairedDevices ?? []);266 setTrustedNetworks(data.data.trustedNetworks ?? []);267 setSyncStatus(data.data.status ?? "offline");268 setLastSyncAt(data.data.lastSyncAt);269 setSyncProStatus(data.data.isPro ?? false);270 }271 }272273 if ((passkeyRes as { ok: boolean }).ok) {274 setPasskeyState(275 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null276 );277 }278279 if ((duressRes as { ok: boolean }).ok) {280 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);281 }282283 if ((settingsRes as { ok: boolean }).ok) {284 const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;285 setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);286 }287288 if ((keyRes as { ok: boolean }).ok) {289 setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);290 }291292 const entriesRes = await sendMessage({ type: "GET_ENTRIES" });293 if ((entriesRes as { ok: boolean }).ok) {294 const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];295 setCanaryEntries(296 entries297 .filter((e) => e.canary?.webhookUrl)298 .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))299 );300 }301302 chrome.runtime.onMessage.addListener((msg) => {303 if (msg.type === "SYNC_STATE_UPDATE") {304 setPairedDevices(msg.pairedDevices ?? []);305 setTrustedNetworks(msg.trustedNetworks ?? []);306 setSyncStatus(msg.status ?? "offline");307 setLastSyncAt(msg.lastSyncAt);308 setSyncProStatus(msg.isPro ?? false);309 }310 if (msg.type === "PAIRING_COMPLETED") {311 setHasPairedLocal(true);312 }313 });314315 if (isNativePlatform()) {316 const biometric = nativePlugin<{ isAvailable: () => Promise<{ available: boolean }> }>("BiometricPlugin");317 const keystore = nativePlugin<{ hasMasterKey: () => Promise<{ exists: boolean }> }>("KeystorePlugin");318 try {319 if (biometric) {320 const a = await biometric.isAvailable();321 setBiometricSupported(a.available);322 }323 if (keystore) {324 const k = await keystore.hasMasterKey();325 setBiometricStored(k.exists);326 }327 } catch { }328 }329 });330331 createEffect(() => {332 if (activeSection() === "sync") {333 setShowSyncInfoSheet(!isPro() && !hasPaired());334 } else {335 setShowSyncInfoSheet(false);336 }337 });338339 async function handleEnableBiometrics() {340 setBiometricBusy(true);341 setBiometricError(null);342 try {343 const keyRes = (await sendMessage({ type: "GET_DERIVED_KEY" })) as {344 ok: boolean;345 data?: { masterKeyHex: string };346 error?: string;347 };348 if (!keyRes.ok || !keyRes.data?.masterKeyHex) {349 setBiometricError(keyRes.error === "vault_locked"350 ? "unlock the vault first"351 : "couldn't read derived key");352 return;353 }354 const keystore = nativePlugin<{ storeMasterKey: (p: { masterKeyHex: string }) => Promise<void> }>("KeystorePlugin");355 if (!keystore) {356 setBiometricError("biometric plugin unavailable");357 return;358 }359 await keystore.storeMasterKey({ masterKeyHex: keyRes.data.masterKeyHex });360 await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: true } });361 setBiometricStored(true);362 } catch (e) {363 setBiometricError(e instanceof Error ? e.message : "failed to enable biometrics");364 } finally {365 setBiometricBusy(false);366 }367 }368369 async function handleDisableBiometrics() {370 setBiometricBusy(true);371 setBiometricError(null);372 try {373 const keystore = nativePlugin<{ clearMasterKey: () => Promise<void> }>("KeystorePlugin");374 if (keystore) await keystore.clearMasterKey();375 await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: false } });376 setBiometricStored(false);377 } catch (e) {378 setBiometricError(e instanceof Error ? e.message : "failed to disable biometrics");379 } finally {380 setBiometricBusy(false);381 }382 }383384 async function handleChangePassword() {385 setChangePwError(null);386 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }387 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }388 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }389 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }390 setChangePwSaving(true);391 const res = await sendMessage({392 type: "CHANGE_PASSWORD",393 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },394 }) as { ok: boolean; error?: string };395 if (res.ok) {396 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");397 setShowChangePw(false);398 setShowNewRecoveryWords(true);399 } else {400 setChangePwError(res.error ?? "failed to change password");401 }402 setChangePwSaving(false);403 }404405 const [copied, setCopied] = createSignal(false);406407 async function copyLicenseKey(key: string) {408 await navigator.clipboard.writeText(key);409 setCopied(true);410 setTimeout(() => setCopied(false), 2000);411 }412413 async function handleActivateLicense() {414 const key = licenseKeyInput().trim();415 if (!key) return;416 setActivatingLicense(true);417 setLicenseError(null);418 setLicenseSuccess(null);419 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });420 setActivatingLicense(false);421 if ((res as { ok: boolean }).ok) {422 const data = (res as { ok: boolean; data?: LicenseInfo }).data;423 if (data?.valid) {424 setLicenseInfo(data);425 setLicenseKey(key);426 setLicenseSuccess("License activated!");427 setLicenseKeyInput("");428 } else {429 setLicenseError("Invalid license key");430 }431 } else {432 setLicenseError("Activation failed");433 }434 }435436 async function handleTogglePasskeys() {437 const current = passkeyState();438 if (!current || !current.supported) return;439 setPasskeySaving(true);440 const res = await sendMessage({441 type: "SET_PASSKEY_PROVIDER_ENABLED",442 payload: { enabled: !current.enabled },443 });444 setPasskeySaving(false);445 if ((res as { ok: boolean }).ok) {446 setPasskeyState(447 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null448 );449 }450 }451452 async function handleCheckAllBreaches() {453 setCheckingBreaches(true);454 setBreachResult(null);455 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });456 setCheckingBreaches(false);457 if ((res as { ok: boolean }).ok) {458 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};459 const breached = Object.values(data).filter((count) => count > 0).length;460 setBreachResult(461 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"462 );463 props.onBreachCheckDone();464 }465 }466467 async function handleSyncNow() {468 if (syncStatus() === "not_trusted") return;469 setSyncingNow(true);470 setSyncFeedback(null);471 const res = await sendMessage({ type: "TRIGGER_SYNC" });472 const ok = (res as { ok: boolean }).ok;473 if (ok) {474 const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;475 const count = data?.syncedCount ?? 0;476 setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });477 setLastSyncAt(Date.now());478 void refreshSyncState();479 } else {480 const error = (res as { error?: string }).error;481 const map: Record<string, string> = {482 no_paired_devices: "No paired devices",483 no_devices_reachable: "No devices found",484 vault_locked: "Vault is locked",485 pro_required: "Sync requires Pro",486 };487 setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });488 }489 setTimeout(() => {490 setSyncingNow(false);491 setSyncFeedback(null);492 }, 3500);493 }494495 async function refreshSyncState() {496 const res = await sendMessage({ type: "GET_SYNC_STATE" });497 const data = (res as { ok: boolean; data?: {498 pairedDevices?: PairedDevice[];499 trustedNetworks?: TrustedNetwork[];500 status?: SyncStatus;501 lastSyncAt?: number;502 }}).data;503 if (!data) return;504 setPairedDevices(data.pairedDevices ?? []);505 setTrustedNetworks(data.trustedNetworks ?? []);506 setSyncStatus(data.status ?? "offline");507 setLastSyncAt(data.lastSyncAt);508 }509510 async function handleRemoveDevice(deviceId: string) {511 setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));512 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });513 void refreshSyncState();514 }515516 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {517 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });518 void refreshSyncState();519 }520521 const [markingNetwork, setMarkingNetwork] = createSignal(false);522523 async function handleMarkNetworkTrusted() {524 if (markingNetwork()) return;525 setMarkingNetwork(true);526 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });527 await refreshSyncState();528 setMarkingNetwork(false);529 }530531 const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);532 const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);533534 async function handleResetSyncIdentity() {535 setResetIdentityRunning(true);536 const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });537 setResetIdentityRunning(false);538 if ((res as { ok: boolean }).ok) {539 setPairedDevices([]);540 setResetIdentityConfirm(false);541 }542 }543544 function formatRelativeTime(timestamp?: number): string {545 if (!timestamp) return "Never";546 const diff = Date.now() - timestamp;547 const minutes = Math.floor(diff / 60000);548 const hours = Math.floor(diff / 3600000);549 const days = Math.floor(diff / 86400000);550 if (minutes < 1) return "Just now";551 if (minutes < 60) return `${minutes}m ago`;552 if (hours < 24) return `${hours}h ago`;553 return `${days}d ago`;554 }555556 function getStatusColor(status: SyncStatus): string {557 switch (status) {558 case "synced": return "var(--peach-sync-ok)";559 case "syncing": return "var(--peach-sync-busy)";560 case "no_peers": return "var(--peach-sync-off)";561 case "not_trusted": return "var(--peach-sync-err)";562 case "offline": return "var(--peach-sync-off)";563 default: return "var(--peach-sync-off)";564 }565 }566567 function getStatusLabel(status: SyncStatus): string {568 switch (status) {569 case "synced": return "Synced";570 case "syncing": return "Syncing";571 case "no_peers": return "No peers visible";572 case "not_trusted": return "Not on a trusted network";573 case "offline": return "Offline";574 default: return "Unknown";575 }576 }577578 function isCurrentNetworkTrusted(): boolean {579 return trustedNetworks().length > 0;580 }581582 async function handleSetupDuress() {583 const mainPw = duressMainPassword().trim();584 const duressPw = duressPassword().trim();585 const confirmPw = duressConfirm().trim();586587 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }588 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }589 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }590 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }591592 setDuressSaving(true);593 setDuressError(null);594595 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };596 if (!verifyRes.ok || !verifyRes.data) {597 setDuressError("incorrect main password");598 setDuressSaving(false);599 return;600 }601602 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };603 setDuressSaving(false);604 if (res.ok) {605 setDuressEnabled(true);606 setShowDuressSetup(false);607 setDuressPassword("");608 setDuressConfirm("");609 setDuressMainPassword("");610 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");611 setTimeout(() => setDuressSuccess(null), 5000);612 } else {613 setDuressError(res.error ?? "failed to set duress password");614 }615 }616617 async function handleClearDeclinedCards() {618 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });619 setDeclinedCardCount(0);620 }621622 async function handleCountDuplicates() {623 setDedupRunning(true);624 setDedupDone(null);625 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };626 setDedupRunning(false);627 if (res.ok) setDedupCount(res.data ?? 0);628 }629630 async function handleDeduplicate() {631 setDedupRunning(true);632 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };633 setDedupRunning(false);634 if (res.ok) {635 setDedupDone(res.data ?? 0);636 setDedupCount(0);637 }638 }639640 async function handleDisableDuress() {641 const mainPw = duressMainPassword().trim();642 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }643644 setDuressSaving(true);645 setDuressError(null);646647 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };648 if (!verifyRes.ok || !verifyRes.data) {649 setDuressError("incorrect main password");650 setDuressSaving(false);651 return;652 }653654 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };655 setDuressSaving(false);656 if (res.ok) {657 setDuressEnabled(false);658 setShowDuressSetup(false);659 setDuressMainPassword("");660 setDuressSuccess("Duress password disabled.");661 setTimeout(() => setDuressSuccess(null), 4000);662 } else {663 setDuressError(res.error ?? "failed to disable duress password");664 }665 }666667 async function handleSubmitFeedback() {668 if (!fbMessage().trim() || fbSubmitting()) return;669 setFbSubmitting(true);670 setFbResult(null);671 const res = await sendMessage({672 type: "SUBMIT_FEEDBACK",673 payload: {674 category: fbCategory(),675 message: fbMessage().trim(),676 version,677 browser: navigator.userAgent,678 },679 }) as { ok: boolean; error?: string };680 setFbSubmitting(false);681 if (res.ok) {682 setFbResult("success");683 setFbMessage("");684 setTimeout(() => setFbResult(null), 4000);685 } else {686 setFbResult("error");687 }688 }689690 function handleHeaderBack() {691 if (activeSection() !== null) {692 setActiveSection(null);693 } else {694 props.onBack();695 }696 }697698 function currentTitle(): string {699 const s = activeSection();700 if (s === null) return "settings";701 if (s === "license") return "account";702 return s;703 }704705 function renderCatRow(opts: {706 section: SectionName;707 name: string;708 desc: string;709 icon: () => any;710 pro?: boolean;711 }) {712 return (713 <button714 class="peach-settings-cat-row"715 onClick={() => setActiveSection(opts.section)}716 >717 <div class="peach-settings-cat-icon">{opts.icon()}</div>718 <div class="peach-settings-cat-content">719 <span class="peach-settings-cat-name">720 {opts.name}721 <Show when={opts.pro && !isPro()}>722 <span class="peach-pro-mark" aria-label="Pro feature">✦</span>723 </Show>724 </span>725 <span class="peach-settings-cat-desc">{opts.desc}</span>726 </div>727 <svg728 class="peach-settings-cat-chevron"729 width="6" height="10" viewBox="0 0 6 10" fill="none"730 stroke="currentColor" stroke-width="1.1"731 stroke-linecap="round" stroke-linejoin="round"732 >733 <path d="M1 1l4 4-4 4" />734 </svg>735 </button>736 );737 }738739 const iconShield = () => (740 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">741 <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>742 </svg>743 );744 const iconKey = () => (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 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>747 </svg>748 );749 const iconLock = () => (750 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">751 <rect x="3" y="7" width="10" height="7" rx="1.5"/>752 <path d="M6 7V5a3 3 0 016 0v2"/>753 </svg>754 );755 const iconSync = () => (756 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">757 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>758 <path d="M14 6l-2 2-2-2"/>759 <path d="M2 10l2-2 2 2"/>760 </svg>761 );762 const iconSun = () => (763 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">764 <circle cx="8" cy="8" r="3"/>765 <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"/>766 </svg>767 );768 const iconKeys = () => (769 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">770 <rect x="2" y="5" width="5" height="4" rx="1"/>771 <rect x="9" y="7" width="5" height="4" rx="1"/>772 <path d="M7 7h2m-2 2h2"/>773 </svg>774 );775 const iconBubble = () => (776 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">777 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>778 </svg>779 );780781 function renderHome() {782 return (783 <div class="peach-settings-body">784 <p class="peach-settings-nav-label">PROTECTION</p>785 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}786 {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}787788 <p class="peach-settings-nav-label">VAULT</p>789 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}790 {renderCatRow({ section: "sync", name: "Sync", desc: "Pair devices · sync is free", icon: iconSync })}791792 <p class="peach-settings-nav-label">PREFERENCES</p>793 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}794 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}795796 <p class="peach-settings-nav-label">SUPPORT</p>797 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}798799 <div class="peach-settings-footer">800 <span>Peach {version}</span>801 <span class="peach-divider"></span>802 <span>Browser extension</span>803 </div>804 </div>805 );806 }807808 function renderSecurity() {809 return (810 <div class="peach-settings-body">811 <div class="peach-settings-group">812 <p class="peach-settings-sub-label">LOCKING</p>813 <div class="peach-settings-row">814 <span>Lock on close</span>815 <button816 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}817 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}818 aria-label="Toggle lock on close"819 >820 <span class="peach-toggle-knob"></span>821 </button>822 </div>823 <div class="peach-settings-row">824 <span>Auto-lock after</span>825 <select826 class="peach-settings-select"827 value={String(props.autoLockMinutes)}828 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}829 >830 <option value="1">1 minute</option>831 <option value="5">5 minutes</option>832 <option value="15">15 minutes</option>833 <option value="30">30 minutes</option>834 <option value="0">Never</option>835 </select>836 </div>837838 <Show when={isNativePlatform() && biometricSupported()}>839 <p class="peach-settings-sub-label">BIOMETRICS</p>840 <div class="peach-settings-row">841 <span>Unlock with biometrics</span>842 <button843 class={`peach-toggle ${biometricStored() ? "is-on" : ""}`}844 onClick={() => void (biometricStored() ? handleDisableBiometrics() : handleEnableBiometrics())}845 disabled={biometricBusy()}846 aria-label="Toggle biometric unlock"847 >848 <span class="peach-toggle-knob"></span>849 </button>850 </div>851 <Show when={biometricError()}>852 <div class="peach-settings-row peach-settings-row--meta">853 <span class="peach-hint peach-hint-error">{biometricError()}</span>854 </div>855 </Show>856 </Show>857858 <p class="peach-settings-sub-label">BREACH PROTECTION</p>859 <div class="peach-settings-row">860 <span>Breach monitoring</span>861 <button862 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}863 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}864 aria-label="Toggle breach monitoring"865 >866 <span class="peach-toggle-knob"></span>867 </button>868 </div>869 <div class="peach-settings-row peach-settings-row--meta">870 <span>Check all passwords</span>871 <button872 class="peach-btn peach-btn--subtle peach-btn--sm"873 onClick={() => void handleCheckAllBreaches()}874 disabled={checkingBreaches()}875 >876 {checkingBreaches() ? "checking..." : "run"}877 </button>878 </div>879 <Show when={breachResult()}>880 <div class="peach-settings-row peach-settings-row--meta">881 <span>Last check</span>882 <span class="peach-settings-value">{breachResult()}</span>883 </div>884 </Show>885886 <p class="peach-settings-sub-label">MASTER PASSWORD</p>887 <div class="peach-settings-row peach-settings-row--meta">888 <span>Master password</span>889 <button890 class="peach-btn peach-btn--subtle peach-btn--sm"891 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}892 >893 {showChangePw() ? "cancel" : "change"}894 </button>895 </div>896 <Show when={showChangePw()}>897 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">898 <input899 class="peach-settings-input"900 type="password"901 placeholder="current password"902 value={changePwCurrent()}903 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}904 autocomplete="current-password"905 />906 <input907 class="peach-settings-input"908 type="password"909 placeholder="new password (min 12 chars)"910 value={changePwNew()}911 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}912 autocomplete="new-password"913 />914 <input915 class="peach-settings-input"916 type="password"917 placeholder="confirm new password"918 value={changePwConfirm()}919 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}920 autocomplete="new-password"921 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}922 />923 <Show when={changePwError()}>924 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>925 </Show>926 <Show when={changePwSuccess()}>927 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>928 </Show>929 <button930 class="peach-btn peach-btn--accent peach-btn--sm"931 style="align-self:flex-start"932 onClick={() => void handleChangePassword()}933 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}934 >935 {changePwSaving() ? "saving…" : "save new password"}936 </button>937 </div>938 </Show>939940 <p class="peach-settings-sub-label">DURESS PASSWORD</p>941 <div class="peach-settings-row peach-settings-row--top">942 <div class="peach-settings-row-main">943 <span>Duress password</span>944 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">945 If entered under coercion, shows a decoy empty vault instead of your real one.946 </span>947 </div>948 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>949 {duressEnabled() ? "on" : "off"}950 </div>951 </div>952953 <Show when={duressEnabled() && !showDuressSetup()}>954 <div class="peach-settings-row peach-settings-row--meta">955 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">956 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.957 </span>958 </div>959 </Show>960961 <Show when={duressSuccess()}>962 <div class="peach-settings-row peach-settings-row--meta">963 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>964 </div>965 </Show>966967 <Show when={!showDuressSetup()}>968 <div class="peach-settings-row peach-settings-row--meta">969 <button970 class="peach-btn peach-btn--subtle peach-btn--sm"971 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}972 >973 {duressEnabled() ? "Change / disable" : "Set up"}974 </button>975 </div>976 </Show>977978 <Show when={showDuressSetup()}>979 <div class="peach-duress-form">980 <label class="peach-label">main password (to confirm)</label>981 <input982 class="peach-settings-input"983 type="password"984 placeholder="your current master password"985 autocomplete="current-password"986 value={duressMainPassword()}987 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}988 />989 <Show when={!duressEnabled()}>990 <label class="peach-label">new duress password</label>991 <input992 class="peach-settings-input"993 type="password"994 placeholder="duress password (min 8 chars)"995 autocomplete="new-password"996 value={duressPassword()}997 onInput={(e) => setDuressPassword(e.currentTarget.value)}998 />999 <label class="peach-label">confirm duress password</label>1000 <input1001 class="peach-settings-input"1002 type="password"1003 placeholder="confirm duress password"1004 autocomplete="new-password"1005 value={duressConfirm()}1006 onInput={(e) => setDuressConfirm(e.currentTarget.value)}1007 />1008 </Show>10091010 <Show when={duressError()}>1011 <p class="peach-hint peach-hint-error">{duressError()}</p>1012 </Show>10131014 <div class="peach-duress-actions">1015 <button1016 class="peach-btn peach-btn--subtle peach-btn--sm"1017 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}1018 >1019 cancel1020 </button>1021 <Show when={duressEnabled()}>1022 <button1023 class="peach-btn peach-btn--ghost peach-btn--sm"1024 style="color:var(--peach-breach)"1025 onClick={() => void handleDisableDuress()}1026 disabled={duressSaving()}1027 >1028 {duressSaving() ? "disabling..." : "disable duress"}1029 </button>1030 </Show>1031 <Show when={!duressEnabled()}>1032 <button1033 class="peach-btn peach-btn--accent peach-btn--sm"1034 onClick={() => void handleSetupDuress()}1035 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}1036 >1037 {duressSaving() ? "saving..." : "save"}1038 </button>1039 </Show>1040 </div>1041 </div>1042 </Show>10431044 <p class="peach-settings-sub-label">CANARY ENTRIES</p>1045 <div class="peach-settings-row peach-settings-row--top">1046 <div class="peach-settings-row-main">1047 <span>Canary entries</span>1048 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1049 Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.1050 </span>1051 </div>1052 <span class="peach-settings-value" style="flex-shrink:0">1053 {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}1054 </span>1055 </div>1056 <Show when={canaryEntries().length > 0}>1057 <For each={canaryEntries()}>1058 {(entry) => (1059 <div class="peach-settings-row peach-settings-row--meta">1060 <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>1061 <button1062 class="peach-btn peach-btn--subtle peach-btn--sm"1063 onClick={() => void handleTestCanary(entry)}1064 disabled={canaryTestingId() === entry.id}1065 style="flex-shrink:0"1066 >1067 {canaryTestingId() === entry.id1068 ? "sending…"1069 : canaryTestResult()?.id === entry.id1070 ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1071 : "test"}1072 </button>1073 </div>1074 )}1075 </For>1076 </Show>10771078 <p class="peach-settings-sub-label">PASSKEYS</p>1079 <div class="peach-settings-row peach-settings-row--top">1080 <div class="peach-settings-row-main">1081 <span>Intercept passkey requests</span>1082 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1083 Peach can handle passkey sign-in and creation on supported Chromium browsers.1084 </span>1085 </div>1086 <button1087 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1088 onClick={() => void handleTogglePasskeys()}1089 aria-label="Toggle passkey interception"1090 disabled={!passkeyState()?.supported || passkeySaving()}1091 >1092 <span class="peach-toggle-knob"></span>1093 </button>1094 </div>1095 <div class="peach-settings-row peach-settings-row--meta">1096 <span>Status</span>1097 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1098 </div>1099 <div class="peach-settings-row peach-settings-row--meta">1100 <span>Browser</span>1101 <span class="peach-settings-value">1102 {passkeyState()?.browserLabel ?? "Unknown"}1103 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1104 </span>1105 </div>1106 <div class="peach-info-notice" style="margin-top:8px">1107 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1108 </div>1109 <Show when={passkeyState()?.conflict}>1110 <div class="peach-settings-row peach-settings-row--meta">1111 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1112 </div>1113 </Show>1114 </div>1115 </div>1116 );1117 }11181119 function renderVault() {1120 return (1121 <div class="peach-settings-body">1122 <div class="peach-settings-group">1123 <p class="peach-settings-sub-label">ENTRIES</p>1124 <div class="peach-settings-row peach-settings-row--meta">1125 <span>Entries</span>1126 <span class="peach-settings-value">{props.entriesCount}</span>1127 </div>1128 <div class="peach-settings-row peach-settings-row--meta">1129 <span>Duplicates</span>1130 <Show when={dedupDone() !== null} fallback={1131 <button1132 class="peach-btn peach-btn--subtle peach-btn--sm"1133 onClick={() => void handleCountDuplicates()}1134 disabled={dedupRunning()}1135 >1136 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1137 </button>1138 }>1139 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1140 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1141 </span>1142 </Show>1143 </div>1144 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1145 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1146 <div class="peach-restore-warning">1147 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1148 </div>1149 <div style="display:flex;gap:6px">1150 <button1151 class="peach-btn peach-btn--subtle peach-btn--sm"1152 onClick={() => setDedupCount(null)}1153 >1154 cancel1155 </button>1156 <button1157 class="peach-btn peach-btn--accent peach-btn--sm"1158 onClick={() => void handleDeduplicate()}1159 disabled={dedupRunning()}1160 >1161 {dedupRunning() ? "removing..." : "remove duplicates"}1162 </button>1163 </div>1164 </div>1165 </Show>11661167 <p class="peach-settings-sub-label">MAINTENANCE</p>1168 <div class="peach-settings-row peach-settings-row--meta">1169 <span>Trash auto-delete</span>1170 <select1171 class="peach-settings-select"1172 value={String(props.trashRetentionDays)}1173 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1174 >1175 <option value="7">7 days</option>1176 <option value="30">30 days</option>1177 <option value="90">90 days</option>1178 <option value="0">Never</option>1179 </select>1180 </div>1181 <div class="peach-settings-row peach-settings-row--meta">1182 <span>Vault Health</span>1183 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1184 open1185 </button>1186 </div>1187 <Show when={declinedCardCount() > 0}>1188 <div class="peach-settings-row peach-settings-row--meta">1189 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1190 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1191 Clear1192 </button>1193 </div>1194 </Show>11951196 <p class="peach-settings-sub-label">DATA</p>1197 <div class="peach-settings-row peach-settings-row--meta">1198 <span>Import passwords</span>1199 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1200 open1201 </button>1202 </div>1203 <div class="peach-settings-row peach-settings-row--meta">1204 <span>Export backup</span>1205 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1206 open1207 </button>1208 </div>12091210 <p class="peach-settings-sub-label">VAULT HISTORY</p>1211 <Show when={historyLoading()}>1212 <p class="peach-settings-hint">Loading…</p>1213 </Show>1214 <Show when={!historyLoading() && historySnapshots().length === 0}>1215 <p class="peach-settings-hint">1216 No snapshots yet. Peach saves automatically before every change —1217 saves, syncs, and imports.1218 </p>1219 </Show>1220 <Show when={!historyLoading() && historySnapshots().length > 0}>1221 <p class="peach-settings-hint">1222 Restoring locks your vault. Unlock with the password that1223 encrypted that snapshot to confirm.1224 </p>1225 <div class="peach-history-list">1226 <For each={historySnapshots()}>1227 {(snap) => (1228 <div class="peach-history-row">1229 <div class="peach-history-meta">1230 <span class="peach-history-time">1231 {formatSnapshotTime(snap.savedAt)}1232 </span>1233 <span class="peach-history-sub">1234 {[1235 snap.entryCount !== null1236 ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1237 : null,1238 reasonLabel(snap.reason),1239 ]1240 .filter(Boolean)1241 .join(" · ")}1242 </span>1243 </div>1244 <button1245 class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1246 historyRestoring() === snap.id ? " is-busy" : ""1247 }`}1248 onClick={() => void handleRestoreVaultSnapshot(snap.id)}1249 disabled={historyRestoring() !== null}1250 >1251 <Show when={historyRestoring() === snap.id} fallback="Restore">1252 …1253 </Show>1254 </button>1255 </div>1256 )}1257 </For>1258 </div>1259 <p class="peach-history-footer">1260 {historySnapshots().length} of 10 snapshots1261 </p>1262 </Show>12631264 <button1265 class="peach-settings-advanced-toggle"1266 onClick={() => setShowVaultAdvanced((v) => !v)}1267 aria-expanded={showVaultAdvanced()}1268 >1269 <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1270 <span class="peach-settings-advanced-chevron">1271 {showVaultAdvanced() ? "▲" : "▼"}1272 </span>1273 </button>12741275 <Show when={showVaultAdvanced()}>1276 <div class="peach-danger-card">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">Replace vault from backup</h3>1280 </div>1281 <p class="peach-danger-card-copy">1282 Wipes the current vault and restores from a backup file.1283 Recoverable only if your backup is good.1284 </p>1285 <button1286 class="peach-danger-card-button"1287 onClick={() => props.onOpenFileImport?.()}1288 >1289 Choose backup file1290 </button>1291 </div>12921293 <Show when={props.onOpenPanic}>1294 <div class="peach-danger-card peach-danger-card--hazard">1295 <div class="peach-danger-card-head">1296 <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1297 <h3 class="peach-danger-card-title">Destroy vault</h3>1298 </div>1299 <p class="peach-danger-card-copy">1300 Permanently wipes every entry, setting, and license on this1301 device. There is no recovery.1302 </p>1303 <button1304 class="peach-danger-card-button"1305 onClick={() => props.onOpenPanic?.()}1306 >1307 Begin destruction1308 </button>1309 </div>1310 </Show>1311 </Show>1312 </div>1313 </div>1314 );1315 }13161317 function renderSync() {1318 return (1319 <>1320 <Show when={showSyncInfoSheet()}>1321 <SyncInfoSheet1322 onGotIt={() => setShowSyncInfoSheet(false)}1323 onLearnPro={() => {1324 setShowSyncInfoSheet(false);1325 setActiveSection("license");1326 }}1327 />1328 </Show>13291330 <div class="peach-settings-body">1331 <div class="peach-settings-group">1332 <p class="peach-sync-network-note">1333 Sync works on your home and trusted networks. Some public or1334 corporate networks block device connections.1335 </p>13361337 {}1338 <button1339 class="peach-settings-row peach-settings-row--tappable"1340 onClick={() => props.onOpenSyncPairing?.()}1341 >1342 <span>Add a device</span>1343 <svg width="6" height="10" viewBox="0 0 6 10" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1344 <path d="M1 1l4 4-4 4" />1345 </svg>1346 </button>13471348 {}1349 <Show when={pairedDevices().length > 0}>1350 <div class="peach-history-list">1351 <For each={pairedDevices()}>1352 {(device) => (1353 <div class="peach-history-row">1354 <div class="peach-history-meta">1355 <span class="peach-history-time">{device.displayName}</span>1356 <span class="peach-history-sub">1357 {device.lastSyncAt1358 ? `Last synced ${formatRelativeTime(device.lastSyncAt)}`1359 : "Never synced"}1360 </span>1361 </div>1362 <button1363 class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1364 onClick={() => void handleRemoveDevice(device.deviceId)}1365 >1366 Remove1367 </button>1368 </div>1369 )}1370 </For>1371 </div>1372 </Show>13731374 {}1375 <Show1376 when={isPro()}1377 fallback={1378 <Show when={hasPaired()}>1379 <div class="peach-sync-pro-hint">1380 <p class="peach-settings-hint">1381 Upgrade to Pro for automatic background sync across all your devices.1382 </p>1383 <button1384 class="peach-btn peach-btn--subtle peach-btn--sm"1385 onClick={() => setActiveSection("license")}1386 >1387 Upgrade to Pro1388 </button>1389 </div>1390 </Show>1391 }1392 >1393 <div class="peach-settings-row">1394 <span>Status</span>1395 <span class="peach-settings-value peach-settings-value--status">1396 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1397 {getStatusLabel(syncStatus())}1398 </span>1399 </div>1400 <div class="peach-settings-row peach-settings-row--meta">1401 <span>Last sync</span>1402 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1403 </div>1404 <div class="peach-settings-row peach-settings-row--meta">1405 <button1406 class="peach-btn peach-btn--subtle peach-btn--sm"1407 onClick={() => void handleSyncNow()}1408 disabled={syncStatus() === "not_trusted" || syncingNow()}1409 >1410 <Switch>1411 <Match when={syncingNow()}>Syncing…</Match>1412 <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1413 <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1414 <Match when={true}>Sync now</Match>1415 </Switch>1416 </button>1417 </div>1418 <p class="peach-settings-hint">1419 If you edit on multiple devices before syncing, the most recently1420 changed device wins. Use Vault History to recover older versions.1421 </p>14221423 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1424 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1425 <For each={trustedNetworks()}>1426 {(network) => (1427 <div class="peach-sync-device-row">1428 <span class="peach-sync-device-name">1429 {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1430 </span>1431 <Show1432 when={!network.isTailscale}1433 fallback={<span class="peach-sync-device-meta">Always trusted</span>}1434 >1435 <button1436 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1437 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1438 >1439 Remove1440 </button>1441 </Show>1442 </div>1443 )}1444 </For>1445 <div class="peach-settings-actions--row">1446 <Show when={!isCurrentNetworkTrusted()}>1447 <button1448 class="peach-btn peach-btn--subtle peach-btn--sm"1449 onClick={() => void handleMarkNetworkTrusted()}1450 disabled={markingNetwork()}1451 >1452 {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1453 </button>1454 </Show>1455 </div>1456 </div>14571458 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1459 <span class="peach-sync-section-label">Advanced</span>1460 <Show1461 when={resetIdentityConfirm()}1462 fallback={1463 <div class="peach-sync-advanced-row">1464 <div class="peach-sync-advanced-info">1465 <span class="peach-sync-advanced-name">Reset sync identity</span>1466 <span class="peach-sync-device-meta">1467 Generates a new keypair and unpairs every device.1468 </span>1469 </div>1470 <button1471 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1472 onClick={() => setResetIdentityConfirm(true)}1473 >1474 Reset1475 </button>1476 </div>1477 }1478 >1479 <div class="peach-sync-reset-confirm">1480 <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1481 <p class="peach-sync-reset-confirm-body">1482 This will unpair all devices. You will need to pair them again from scratch.1483 </p>1484 <div class="peach-settings-actions--row">1485 <button1486 class="peach-btn peach-btn--ghost peach-btn--sm"1487 onClick={() => setResetIdentityConfirm(false)}1488 disabled={resetIdentityRunning()}1489 >1490 Cancel1491 </button>1492 <button1493 class="peach-btn peach-btn--danger peach-btn--sm"1494 onClick={() => void handleResetSyncIdentity()}1495 disabled={resetIdentityRunning()}1496 >1497 {resetIdentityRunning() ? "Resetting..." : "Reset"}1498 </button>1499 </div>1500 </div>1501 </Show>1502 </div>1503 </Show>1504 </div>1505 </div>1506 </>1507 );1508 }15091510 function renderAppearance() {1511 const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1512 { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1513 { id: "cormorant", label: "Cormorant", preview: "Aa" },1514 { id: "inter", label: "Inter", preview: "Aa" },1515 { id: "system", label: "System", preview: "Aa" },1516 ];1517 const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1518 { id: "small", label: "Small" },1519 { id: "medium", label: "Default" },1520 { id: "large", label: "Large" },1521 ];1522 return (1523 <div class="peach-settings-body">1524 <div class="peach-settings-group">1525 <p class="peach-settings-sub-label">THEME</p>1526 <div class="peach-settings-row peach-settings-row--meta">1527 <span>Theme</span>1528 <button1529 class="peach-btn peach-btn--subtle peach-btn--sm"1530 onClick={() =>1531 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1532 }1533 >1534 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1535 </button>1536 </div>15371538 <p class="peach-settings-sub-label">ACCENT</p>1539 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1540 <span>Accent</span>1541 <div class="peach-accent-picker">1542 <For each={ACCENT_COLORS}>1543 {(color) => (1544 <button1545 class="peach-accent-swatch"1546 classList={{ "is-active": props.accentColor === color.name }}1547 style={{ background: color.hex }}1548 onClick={() => void props.onAccentChange(color.name)}1549 aria-label={color.name}1550 title={color.name}1551 />1552 )}1553 </For>1554 </div>1555 </div>15561557 <p class="peach-settings-sub-label">DISPLAY FONT</p>1558 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1559 <span>Font</span>1560 <div class="peach-font-picker">1561 <For each={fontOptions}>1562 {(opt) => (1563 <button1564 class="peach-font-swatch"1565 classList={{ "is-active": props.displayFont === opt.id }}1566 data-font-preview={opt.id}1567 onClick={() => void props.onDisplayFontChange(opt.id)}1568 aria-label={opt.label}1569 title={opt.label}1570 >1571 <span class="peach-font-swatch-glyph">{opt.preview}</span>1572 <span class="peach-font-swatch-name">{opt.label}</span>1573 </button>1574 )}1575 </For>1576 </div>1577 </div>15781579 <p class="peach-settings-sub-label">TEXT SIZE</p>1580 <div class="peach-settings-row peach-settings-row--meta">1581 <span>Size</span>1582 <div class="peach-pill-group">1583 <For each={sizeOptions}>1584 {(opt) => (1585 <button1586 class="peach-btn peach-btn--ghost peach-btn--sm"1587 classList={{ "is-active": props.textSize === opt.id }}1588 onClick={() => void props.onTextSizeChange(opt.id)}1589 >1590 {opt.label}1591 </button>1592 )}1593 </For>1594 </div>1595 </div>15961597 <p class="peach-settings-sub-label">MOTION</p>1598 <div class="peach-settings-row peach-settings-row--meta">1599 <span>Reduce motion</span>1600 <button1601 class="peach-btn peach-btn--subtle peach-btn--sm"1602 onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1603 >1604 {props.reduceMotion ? "On" : "Off"}1605 </button>1606 </div>1607 </div>1608 </div>1609 );1610 }16111612 function renderShortcuts() {1613 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1614 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);16151616 function startCapture(action: ShortcutAction) {1617 setConflictMsg(null);1618 setCapturingAction(action);1619 }1620 function cancelCapture() {1621 setCapturingAction(null);1622 setConflictMsg(null);1623 }1624 function commitBinding(action: ShortcutAction, binding: Keybinding) {1625 const conflict = findConflict(props.shortcuts, binding, action);1626 if (conflict) {1627 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1628 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1629 return;1630 }1631 const next = { ...props.shortcuts, [action]: binding };1632 setCapturingAction(null);1633 setConflictMsg(null);1634 void props.onShortcutsChange(next);1635 }1636 function resetAction(action: ShortcutAction) {1637 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1638 void props.onShortcutsChange(next);1639 }1640 function resetAll() {1641 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1642 }16431644 1645 1646 function handleCaptureKey(e: KeyboardEvent) {1647 e.preventDefault();1648 e.stopPropagation();1649 if (e.key === "Escape") { cancelCapture(); return; }1650 const binding = keybindingFromEvent(e);1651 if (!binding) return; 1652 const action = capturingAction();1653 if (action) commitBinding(action, binding);1654 }16551656 return (1657 <div class="peach-settings-body">1658 <div class="peach-settings-group peach-shortcuts-group">1659 <p class="peach-settings-sub-label">ENTRY VIEW</p>1660 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1661 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1662 </For>1663 </div>16641665 <div class="peach-settings-group peach-shortcuts-group">1666 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1667 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1668 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1669 </For>1670 </div>16711672 <Show when={conflictMsg()}>1673 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1674 </Show>16751676 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1677 <div class="peach-settings-row peach-settings-row--meta">1678 <span>Inline autofill on page</span>1679 <a1680 class="peach-settings-link"1681 href="chrome://extensions/shortcuts"1682 onClick={(e) => {1683 e.preventDefault();1684 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1685 }}1686 >1687 ⌘⇧P — customize ↗1688 </a>1689 </div>1690 <button1691 class="peach-btn peach-btn--subtle peach-btn--sm"1692 style="align-self:flex-start"1693 onClick={resetAll}1694 >1695 Reset all to defaults1696 </button>1697 </div>1698 </div>1699 );17001701 function renderRow(1702 desc: typeof SHORTCUT_DESCRIPTORS[number],1703 capturing: () => ShortcutAction | null,1704 onStart: (a: ShortcutAction) => void,1705 onCancel: () => void,1706 onReset: (a: ShortcutAction) => void,1707 onKey: (e: KeyboardEvent) => void,1708 ) {1709 const isActive = () => capturing() === desc.action;1710 const binding = () => props.shortcuts[desc.action];1711 const isCustom = () => {1712 const b = binding();1713 const d = DEFAULT_BINDINGS[desc.action];1714 return b.key !== d.key1715 || Boolean(b.ctrl) !== Boolean(d.ctrl)1716 || Boolean(b.meta) !== Boolean(d.meta)1717 || Boolean(b.shift) !== Boolean(d.shift)1718 || Boolean(b.alt) !== Boolean(d.alt);1719 };1720 return (1721 <div class="peach-shortcut-row">1722 <span class="peach-shortcut-row-label">{desc.label}</span>1723 <button1724 class="peach-shortcut-chip"1725 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1726 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1727 onKeyDown={(e) => { if (isActive()) onKey(e); }}1728 ref={(el) => {1729 1730 if (el) {1731 queueMicrotask(() => { if (isActive()) el.focus(); });1732 }1733 }}1734 >1735 {isActive() ? "Press a key…" : formatKeybinding(binding())}1736 </button>1737 <Show when={isCustom() && !isActive()}>1738 <button1739 class="peach-shortcut-reset"1740 title="Reset to default"1741 onClick={() => onReset(desc.action)}1742 >1743 ↺1744 </button>1745 </Show>1746 </div>1747 );1748 }1749 }17501751 const PRO_FEATURES: Array<{1752 feature: ProFeature;1753 name: string;1754 desc: string;1755 }> = [1756 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1757 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1758 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1759 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1760 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1761 ];17621763 const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17641765 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1766 const [deactivating, setDeactivating] = createSignal(false);17671768 async function handleDeactivateLicense() {1769 setDeactivating(true);1770 await sendMessage({ type: "DEACTIVATE_LICENSE" });1771 setLicenseInfo(null);1772 await props.onLicenseChange?.();1773 setDeactivating(false);1774 }17751776 async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1777 setCanaryTestingId(entry.id);1778 setCanaryTestResult(null);1779 const res = await sendMessage({1780 type: "FIRE_CANARY_PING",1781 payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1782 }) as { ok: boolean };1783 setCanaryTestResult({ id: entry.id, ok: res.ok });1784 setCanaryTestingId(null);1785 setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1786 }17871788 function renderLicense() {1789 async function onActivate() {1790 await handleActivateLicense();1791 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1792 await props.onLicenseChange?.();1793 }1794 }17951796 return (1797 <div class="peach-settings-body">1798 <Show1799 when={isPro()}1800 fallback={1801 <Show1802 when={showKeyEntry()}1803 fallback={1804 <div class="peach-pro-upgrade">1805 <div class="peach-pro-upgrade-hero">1806 <div class="peach-pro-upgrade-mark-wrap">1807 <PeachMark class="peach-pro-upgrade-mark" />1808 </div>1809 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1810 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1811 </div>18121813 <div class="peach-pro-features">1814 <For each={PRO_FEATURES}>1815 {(item) => (1816 <div1817 class="peach-pro-feature"1818 classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1819 >1820 <span class="peach-pro-feature-mark">✦</span>1821 <div class="peach-pro-feature-text">1822 <span class="peach-pro-feature-name">{item.name}</span>1823 <span class="peach-pro-feature-desc">{item.desc}</span>1824 </div>1825 </div>1826 )}1827 </For>1828 </div>18291830 <div class="peach-pro-upgrade-actions">1831 <p class="peach-pro-pricing">$39 · once · yours forever</p>1832 <a1833 href="https://peachpasswords.com/#pricing"1834 target="_blank"1835 rel="noopener noreferrer"1836 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1837 >1838 Get Peach Pro1839 </a>1840 <div class="peach-pro-key-divider"><span>or</span></div>1841 <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1842 Activate a license key1843 </button>1844 </div>1845 </div>1846 }1847 >1848 <div class="peach-pro-key-ceremony">1849 <LockRuneCanvas />1850 <p class="peach-pro-key-ceremony-label">Enter your license key</p>1851 <input1852 class="peach-underline-input peach-pro-key-input"1853 type="text"1854 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1855 value={licenseKeyInput()}1856 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1857 onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1858 autocomplete="off"1859 spellcheck={false}1860 />1861 <Show when={licenseError()}>1862 <p class="peach-pro-key-error">That key doesn't look right.</p>1863 </Show>1864 <button1865 class="peach-btn peach-btn--accent peach-pro-key-activate"1866 onClick={() => void onActivate()}1867 disabled={activatingLicense() || !licenseKeyInput().trim()}1868 >1869 {activatingLicense() ? "activating…" : "Activate"}1870 </button>1871 <button1872 class="peach-pro-key-cancel"1873 onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1874 >1875 cancel1876 </button>1877 </div>1878 </Show>1879 }1880 >1881 <div class="peach-pro-active">1882 <div class="peach-pro-active-hero">1883 <div class="peach-pro-active-mark-wrap">1884 <PeachMark class="peach-pro-active-mark" />1885 </div>1886 <h2 class="peach-pro-active-title">Peach Pro</h2>1887 <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1888 </div>18891890 <Show when={licenseKey()}>1891 <div class="peach-settings-license">1892 <span class="peach-settings-label">LICENSE KEY</span>1893 <div class="peach-settings-license-row">1894 <span class="peach-settings-license-key">{licenseKey()!}</span>1895 <button1896 class="peach-settings-license-copy"1897 onClick={() => copyLicenseKey(licenseKey()!)}1898 >1899 {copied() ? "✓" : "copy"}1900 </button>1901 </div>1902 </div>1903 </Show>19041905 <div class="peach-pro-active-features">1906 <For each={PRO_FEATURES}>1907 {(item) => (1908 <div class="peach-pro-active-feature">1909 <span class="peach-pro-active-feature-mark">✦</span>1910 <span class="peach-pro-active-feature-name">{item.name}</span>1911 </div>1912 )}1913 </For>1914 </div>19151916 <button1917 class="peach-pro-deactivate"1918 onClick={() => void handleDeactivateLicense()}1919 disabled={deactivating()}1920 >1921 {deactivating() ? "Deactivating…" : "Deactivate"}1922 </button>1923 </div>1924 </Show>1925 </div>1926 );1927 }19281929 function renderFeedback() {1930 return (1931 <div class="peach-settings-body">1932 <div class="peach-settings-group peach-feedback-group">1933 <p class="peach-feedback-intro">1934 Help us improve Peach. Every report goes directly to our inbox.1935 </p>19361937 <p class="peach-settings-sub-label">TYPE</p>1938 <div class="peach-feedback-chips">1939 <For each={FEEDBACK_CATEGORIES}>1940 {(cat) => (1941 <button1942 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1943 onClick={() => setFbCategory(cat)}1944 >1945 {cat}1946 </button>1947 )}1948 </For>1949 </div>19501951 <p class="peach-settings-sub-label">MESSAGE</p>1952 <textarea1953 class="peach-feedback-textarea"1954 placeholder="Describe the issue or idea..."1955 value={fbMessage()}1956 onInput={(e) => setFbMessage(e.currentTarget.value)}1957 />19581959 <p class="peach-feedback-meta">1960 Version {version} · will be included automatically1961 </p>19621963 <Show when={fbResult() === "success"}>1964 <p class="peach-feedback-result peach-feedback-result--ok">1965 Sent — thank you1966 </p>1967 </Show>1968 <Show when={fbResult() === "error"}>1969 <p class="peach-feedback-result peach-feedback-result--err">1970 Failed to send — try again1971 </p>1972 </Show>19731974 <button1975 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1976 onClick={() => void handleSubmitFeedback()}1977 disabled={fbSubmitting() || !fbMessage().trim()}1978 >1979 {fbSubmitting() ? "Sending..." : "Send feedback"}1980 </button>19811982 <p class="peach-feedback-anon">1983 Sent anonymously · no account required1984 </p>1985 </div>1986 </div>1987 );1988 }19891990 return (1991 <Show1992 when={showNewRecoveryWords()}1993 fallback={1994 <div class="peach-overlay-view ov-enter-right" style="display:flex">1995 <div class="peach-overlay-header">1996 <button1997 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1998 onClick={handleHeaderBack}1999 aria-label="Back"2000 >2001 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">2002 <path d="M6 1L1 6L6 11" />2003 </svg>2004 </button>2005 <span class="peach-overlay-title">{currentTitle()}</span>2006 <div class="peach-overlay-spacer"></div>2007 </div>20082009 <Switch fallback={renderHome()}>2010 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>2011 <Match when={activeSection() === "vault"}>{renderVault()}</Match>2012 <Match when={activeSection() === "sync"}>{renderSync()}</Match>2013 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2014 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2015 <Match when={activeSection() === "license"}>{renderLicense()}</Match>2016 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2017 </Switch>2018 </div>2019 }2020 >2021 <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />2022 </Show>2023 );2024}