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