1import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2import { Capacitor, registerPlugin } from "@capacitor/core";3import { sendMessage } from "../App";4import { PeachMark } from "../components/PeachMark";5import { LockRuneCanvas } from "../components/LockRuneCanvas";6import { biometricAvailable } from "../../shared/biometric";7import type { Canary } from "../../shared/types";8import { ACCENT_COLORS } from "../../shared/types";9import type {10 AccentColorName,11 PasskeyProviderState,12 ProFeature,13 VaultSnapshot,14} from "../../shared/types";15import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types";16import {17 DEFAULT_BINDINGS,18 findConflict,19 formatKeybinding,20 keybindingFromEvent,21 SHORTCUT_DESCRIPTORS,22 type Keybinding,23 type ShortcutAction,24} from "../../shared/shortcuts";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"76 | "mobile";7778const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7980function formatActivatedSince(issued: string | undefined): string {81 if (!issued) return "Active";82 try {83 const d = new Date(issued);84 if (Number.isNaN(d.getTime())) return "Active";85 return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;86 } catch {87 return "Active";88 }89}9091function isSectionName(value: string): value is SectionName {92 return (93 value === "security" ||94 value === "vault" ||95 value === "sync" ||96 value === "appearance" ||97 value === "shortcuts" ||98 value === "license" ||99 value === "feedback" ||100 value === "mobile"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 [syncingNow, setSyncingNow] = createSignal(false);129 const [syncFeedback, setSyncFeedback] = createSignal<130 { kind: "ok" | "err"; text: string } | null131 >(null);132 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);133 const [passkeySaving, setPasskeySaving] = createSignal(false);134135 const [dedupCount, setDedupCount] = createSignal<number | null>(null);136 const [dedupRunning, setDedupRunning] = createSignal(false);137 const [dedupDone, setDedupDone] = createSignal<number | null>(null);138139 const [declinedCardCount, setDeclinedCardCount] = createSignal(0);140141 142 143 144 const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);145 const [historyLoading, setHistoryLoading] = createSignal(false);146 const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);147148 async function loadVaultHistory() {149 setHistoryLoading(true);150 try {151 const res = await sendMessage({ type: "GET_VAULT_HISTORY" });152 const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];153 setHistorySnapshots(list);154 } finally {155 setHistoryLoading(false);156 }157 }158159 async function handleRestoreVaultSnapshot(id: string) {160 if (historyRestoring()) return;161 setHistoryRestoring(id);162 const res = await sendMessage({163 type: "RESTORE_VAULT_SNAPSHOT",164 payload: { snapshotId: id },165 });166 setHistoryRestoring(null);167 if ((res as { ok: boolean }).ok) {168 169 170 props.onBack();171 }172 }173174 function formatSnapshotTime(savedAt: number): string {175 const diff = Date.now() - savedAt;176 if (diff < 60_000) return "Just now";177 if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;178 const d = new Date(savedAt);179 const isToday = new Date().toDateString() === d.toDateString();180 const isYest =181 new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();182 const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });183 if (isToday) return `Today ${t}`;184 if (isYest) return `Yesterday ${t}`;185 return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;186 }187188 function reasonLabel(reason: VaultSnapshot["reason"]): string {189 return (190 {191 local_save: "saved",192 sync_received: "synced",193 import: "imported",194 restore: "restored",195 } as const196 )[reason] ?? reason;197 }198199 const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);200201 const [showChangePw, setShowChangePw] = createSignal(false);202 const [changePwCurrent, setChangePwCurrent] = createSignal("");203 const [changePwNew, setChangePwNew] = createSignal("");204 const [changePwConfirm, setChangePwConfirm] = createSignal("");205 const [changePwSaving, setChangePwSaving] = createSignal(false);206 const [changePwError, setChangePwError] = createSignal<string | null>(null);207 const [changePwSuccess, setChangePwSuccess] = createSignal(false);208209 const [duressEnabled, setDuressEnabled] = createSignal(false);210 const [showDuressSetup, setShowDuressSetup] = createSignal(false);211 const [duressPassword, setDuressPassword] = createSignal("");212 const [duressConfirm, setDuressConfirm] = createSignal("");213 const [duressMainPassword, setDuressMainPassword] = createSignal("");214 const [duressSaving, setDuressSaving] = createSignal(false);215 const [duressError, setDuressError] = createSignal<string | null>(null);216 const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);217218 const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);219220 221 function getAutofillPlugin() {222 try {223 return registerPlugin<{224 isEnabledAsAutofillService(): Promise<{ enabled: boolean }>;225 openAutofillSettings(): Promise<void>;226 }>("Autofill");227 } catch {228 return null;229 }230 }231 const AutofillPlugin = getAutofillPlugin();232 const [autofillEnabled, setAutofillEnabled] = createSignal(false);233 const [biometricEnabled, setBiometricEnabled] = createSignal(false);234 const isAndroid = Capacitor.getPlatform() === "android";235 const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);236 const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);237238 const [fbCategory, setFbCategory] = createSignal("Bug report");239 const [fbMessage, setFbMessage] = createSignal("");240 const [fbSubmitting, setFbSubmitting] = createSignal(false);241 const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);242243 onMount(async () => {244 const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([245 sendMessage({ type: "GET_LICENSE" }),246 sendMessage({ type: "GET_SYNC_STATE" }),247 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),248 sendMessage({ type: "IS_DURESS_ENABLED" }),249 sendMessage({ type: "GET_SETTINGS" }),250 sendMessage({ type: "GET_LICENSE_KEY" }),251 ]);252253 254 255 void loadVaultHistory();256257 if ((licenseRes as { ok: boolean }).ok) {258 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);259 }260261 if ((syncRes as { ok: boolean }).ok) {262 const data = syncRes as { ok: boolean; data?: {263 pairedDevices?: PairedDevice[];264 trustedNetworks?: TrustedNetwork[];265 status?: SyncStatus;266 lastSyncAt?: number;267 isPro?: boolean;268 }};269 if (data.data) {270 setPairedDevices(data.data.pairedDevices ?? []);271 setTrustedNetworks(data.data.trustedNetworks ?? []);272 setSyncStatus(data.data.status ?? "offline");273 setLastSyncAt(data.data.lastSyncAt);274 setSyncProStatus(data.data.isPro ?? false);275 }276 }277278 if ((passkeyRes as { ok: boolean }).ok) {279 setPasskeyState(280 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null281 );282 }283284 if ((duressRes as { ok: boolean }).ok) {285 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);286 }287288 if ((settingsRes as { ok: boolean }).ok) {289 const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;290 setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);291 }292293 if ((keyRes as { ok: boolean }).ok) {294 setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);295 }296297 const entriesRes = await sendMessage({ type: "GET_ENTRIES" });298 if ((entriesRes as { ok: boolean }).ok) {299 const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];300 setCanaryEntries(301 entries302 .filter((e) => e.canary?.webhookUrl)303 .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))304 );305 }306307 308 if (isAndroid) {309 if (AutofillPlugin) {310 const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();311 setAutofillEnabled(enabled);312 }313 const bio = await biometricAvailable();314 setBiometricEnabled(bio);315 }316317 chrome.runtime.onMessage.addListener((msg) => {318 if (msg.type === "SYNC_STATE_UPDATE") {319 setPairedDevices(msg.pairedDevices ?? []);320 setTrustedNetworks(msg.trustedNetworks ?? []);321 setSyncStatus(msg.status ?? "offline");322 setLastSyncAt(msg.lastSyncAt);323 setSyncProStatus(msg.isPro ?? false);324 }325 });326 });327328 async function handleChangePassword() {329 setChangePwError(null);330 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }331 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }332 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }333 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }334 setChangePwSaving(true);335 const res = await sendMessage({336 type: "CHANGE_PASSWORD",337 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },338 }) as { ok: boolean; error?: string };339 if (res.ok) {340 setChangePwSuccess(true);341 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");342 setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);343 } else {344 setChangePwError(res.error ?? "failed to change password");345 }346 setChangePwSaving(false);347 }348349 const [copied, setCopied] = createSignal(false);350351 async function copyLicenseKey(key: string) {352 await navigator.clipboard.writeText(key);353 setCopied(true);354 setTimeout(() => setCopied(false), 2000);355 }356357 async function handleActivateLicense() {358 const key = licenseKeyInput().trim();359 if (!key) return;360 setActivatingLicense(true);361 setLicenseError(null);362 setLicenseSuccess(null);363 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });364 setActivatingLicense(false);365 if ((res as { ok: boolean }).ok) {366 const data = (res as { ok: boolean; data?: LicenseInfo }).data;367 if (data?.valid) {368 setLicenseInfo(data);369 setLicenseKey(key);370 setLicenseSuccess("License activated!");371 setLicenseKeyInput("");372 } else {373 setLicenseError("Invalid license key");374 }375 } else {376 setLicenseError("Activation failed");377 }378 }379380 async function handleTogglePasskeys() {381 const current = passkeyState();382 if (!current || !current.supported) return;383 setPasskeySaving(true);384 const res = await sendMessage({385 type: "SET_PASSKEY_PROVIDER_ENABLED",386 payload: { enabled: !current.enabled },387 });388 setPasskeySaving(false);389 if ((res as { ok: boolean }).ok) {390 setPasskeyState(391 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null392 );393 }394 }395396 async function handleCheckAllBreaches() {397 setCheckingBreaches(true);398 setBreachResult(null);399 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });400 setCheckingBreaches(false);401 if ((res as { ok: boolean }).ok) {402 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};403 const breached = Object.values(data).filter((count) => count > 0).length;404 setBreachResult(405 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"406 );407 props.onBreachCheckDone();408 }409 }410411 async function handleSyncNow() {412 if (syncStatus() === "not_trusted") return;413 setSyncingNow(true);414 setSyncFeedback(null);415 const res = await sendMessage({ type: "TRIGGER_SYNC" });416 const ok = (res as { ok: boolean }).ok;417 if (ok) {418 const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;419 const count = data?.syncedCount ?? 0;420 setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });421 setLastSyncAt(Date.now());422 void refreshSyncState();423 } else {424 const error = (res as { error?: string }).error;425 const map: Record<string, string> = {426 no_paired_devices: "No paired devices",427 no_devices_reachable: "No devices found",428 vault_locked: "Vault is locked",429 pro_required: "Sync requires Pro",430 };431 setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });432 }433 setTimeout(() => {434 setSyncingNow(false);435 setSyncFeedback(null);436 }, 3500);437 }438439 async function refreshSyncState() {440 const res = await sendMessage({ type: "GET_SYNC_STATE" });441 const data = (res as { ok: boolean; data?: {442 pairedDevices?: PairedDevice[];443 trustedNetworks?: TrustedNetwork[];444 status?: SyncStatus;445 lastSyncAt?: number;446 }}).data;447 if (!data) return;448 setPairedDevices(data.pairedDevices ?? []);449 setTrustedNetworks(data.trustedNetworks ?? []);450 setSyncStatus(data.status ?? "offline");451 setLastSyncAt(data.lastSyncAt);452 }453454 async function handleRemoveDevice(deviceId: string) {455 setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));456 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });457 void refreshSyncState();458 }459460 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {461 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });462 void refreshSyncState();463 }464465 const [markingNetwork, setMarkingNetwork] = createSignal(false);466467 async function handleMarkNetworkTrusted() {468 if (markingNetwork()) return;469 setMarkingNetwork(true);470 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });471 await refreshSyncState();472 setMarkingNetwork(false);473 }474475 const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);476 const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);477478 async function handleResetSyncIdentity() {479 setResetIdentityRunning(true);480 if (Capacitor.isNativePlatform()) {481 const { clearAndroidSyncIdentity } = await import("../../shared/android-sync-identity");482 await clearAndroidSyncIdentity();483 }484 const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });485 setResetIdentityRunning(false);486 if ((res as { ok: boolean }).ok) {487 setPairedDevices([]);488 setResetIdentityConfirm(false);489 }490 }491492 function formatRelativeTime(timestamp?: number): string {493 if (!timestamp) return "Never";494 const diff = Date.now() - timestamp;495 const minutes = Math.floor(diff / 60000);496 const hours = Math.floor(diff / 3600000);497 const days = Math.floor(diff / 86400000);498 if (minutes < 1) return "Just now";499 if (minutes < 60) return `${minutes}m ago`;500 if (hours < 24) return `${hours}h ago`;501 return `${days}d ago`;502 }503504 function getStatusColor(status: SyncStatus): string {505 switch (status) {506 case "synced": return "var(--peach-sync-ok)";507 case "syncing": return "var(--peach-sync-busy)";508 case "no_peers": return "var(--peach-sync-off)";509 case "not_trusted": return "var(--peach-sync-err)";510 case "offline": return "var(--peach-sync-off)";511 default: return "var(--peach-sync-off)";512 }513 }514515 function getStatusLabel(status: SyncStatus): string {516 switch (status) {517 case "synced": return "Synced";518 case "syncing": return "Syncing";519 case "no_peers": return "No peers visible";520 case "not_trusted": return "Not on a trusted network";521 case "offline": return "Offline";522 default: return "Unknown";523 }524 }525526 function isCurrentNetworkTrusted(): boolean {527 return trustedNetworks().length > 0;528 }529530 async function handleSetupDuress() {531 const mainPw = duressMainPassword().trim();532 const duressPw = duressPassword().trim();533 const confirmPw = duressConfirm().trim();534535 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }536 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }537 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }538 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }539540 setDuressSaving(true);541 setDuressError(null);542543 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };544 if (!verifyRes.ok || !verifyRes.data) {545 setDuressError("incorrect main password");546 setDuressSaving(false);547 return;548 }549550 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };551 setDuressSaving(false);552 if (res.ok) {553 setDuressEnabled(true);554 setShowDuressSetup(false);555 setDuressPassword("");556 setDuressConfirm("");557 setDuressMainPassword("");558 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");559 setTimeout(() => setDuressSuccess(null), 5000);560 } else {561 setDuressError(res.error ?? "failed to set duress password");562 }563 }564565 async function handleClearDeclinedCards() {566 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });567 setDeclinedCardCount(0);568 }569570 async function handleCountDuplicates() {571 setDedupRunning(true);572 setDedupDone(null);573 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };574 setDedupRunning(false);575 if (res.ok) setDedupCount(res.data ?? 0);576 }577578 async function handleDeduplicate() {579 setDedupRunning(true);580 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };581 setDedupRunning(false);582 if (res.ok) {583 setDedupDone(res.data ?? 0);584 setDedupCount(0);585 }586 }587588 async function handleDisableDuress() {589 const mainPw = duressMainPassword().trim();590 if (!mainPw) { setDuressError("enter your main password to confirm"); 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: "DISABLE_DURESS" }) as { ok: boolean; error?: string };603 setDuressSaving(false);604 if (res.ok) {605 setDuressEnabled(false);606 setShowDuressSetup(false);607 setDuressMainPassword("");608 setDuressSuccess("Duress password disabled.");609 setTimeout(() => setDuressSuccess(null), 4000);610 } else {611 setDuressError(res.error ?? "failed to disable duress password");612 }613 }614615 async function handleSubmitFeedback() {616 if (!fbMessage().trim() || fbSubmitting()) return;617 setFbSubmitting(true);618 setFbResult(null);619 const res = await sendMessage({620 type: "SUBMIT_FEEDBACK",621 payload: {622 category: fbCategory(),623 message: fbMessage().trim(),624 version,625 browser: navigator.userAgent,626 },627 }) as { ok: boolean; error?: string };628 setFbSubmitting(false);629 if (res.ok) {630 setFbResult("success");631 setFbMessage("");632 setTimeout(() => setFbResult(null), 4000);633 } else {634 setFbResult("error");635 }636 }637638 function handleHeaderBack() {639 if (activeSection() !== null) {640 setActiveSection(null);641 } else {642 props.onBack();643 }644 }645646 function currentTitle(): string {647 const s = activeSection();648 if (s === null) return "settings";649 if (s === "license") return "account";650 return s;651 }652653 function renderCatRow(opts: {654 section: SectionName;655 name: string;656 desc: string;657 icon: () => any;658 pro?: boolean;659 }) {660 return (661 <button662 class="peach-settings-cat-row"663 onClick={() => setActiveSection(opts.section)}664 >665 <div class="peach-settings-cat-icon">{opts.icon()}</div>666 <div class="peach-settings-cat-content">667 <span class="peach-settings-cat-name">668 {opts.name}669 <Show when={opts.pro && !isPro()}>670 <span class="peach-pro-mark" aria-label="Pro feature">✦</span>671 </Show>672 </span>673 <span class="peach-settings-cat-desc">{opts.desc}</span>674 </div>675 <svg676 class="peach-settings-cat-chevron"677 width="6" height="10" viewBox="0 0 6 10" fill="none"678 stroke="currentColor" stroke-width="1.1"679 stroke-linecap="round" stroke-linejoin="round"680 >681 <path d="M1 1l4 4-4 4" />682 </svg>683 </button>684 );685 }686687 const iconShield = () => (688 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">689 <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>690 </svg>691 );692 const iconKey = () => (693 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">694 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>695 </svg>696 );697 const iconLock = () => (698 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">699 <rect x="3" y="7" width="10" height="7" rx="1.5"/>700 <path d="M6 7V5a3 3 0 016 0v2"/>701 </svg>702 );703 const iconSync = () => (704 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">705 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>706 <path d="M14 6l-2 2-2-2"/>707 <path d="M2 10l2-2 2 2"/>708 </svg>709 );710 const iconSun = () => (711 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">712 <circle cx="8" cy="8" r="3"/>713 <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"/>714 </svg>715 );716 const iconKeys = () => (717 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">718 <rect x="2" y="5" width="5" height="4" rx="1"/>719 <rect x="9" y="7" width="5" height="4" rx="1"/>720 <path d="M7 7h2m-2 2h2"/>721 </svg>722 );723 const iconBubble = () => (724 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">725 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>726 </svg>727 );728729 function renderHome() {730 return (731 <div class="peach-settings-body">732 <p class="peach-settings-nav-label">PROTECTION</p>733 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}734 {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}735736 <p class="peach-settings-nav-label">VAULT</p>737 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}738 {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}739740 <p class="peach-settings-nav-label">PREFERENCES</p>741 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}742 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}743 <Show when={isAndroid}>744 <p class="peach-settings-nav-label">MOBILE</p>745 {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}746 </Show>747748 <p class="peach-settings-nav-label">SUPPORT</p>749 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}750751 <div class="peach-settings-footer">752 <span>Peach {version}</span>753 <span class="peach-divider"></span>754 <span>Browser extension</span>755 </div>756 </div>757 );758 }759760 function renderSecurity() {761 return (762 <div class="peach-settings-body">763 <div class="peach-settings-group">764 <p class="peach-settings-sub-label">LOCKING</p>765 <div class="peach-settings-row">766 <span>Lock on close</span>767 <button768 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}769 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}770 aria-label="Toggle lock on close"771 >772 <span class="peach-toggle-knob"></span>773 </button>774 </div>775 <div class="peach-settings-row">776 <span>Auto-lock after</span>777 <select778 class="peach-settings-select"779 value={String(props.autoLockMinutes)}780 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}781 >782 <option value="1">1 minute</option>783 <option value="5">5 minutes</option>784 <option value="15">15 minutes</option>785 <option value="30">30 minutes</option>786 <option value="0">Never</option>787 </select>788 </div>789790 <p class="peach-settings-sub-label">BREACH PROTECTION</p>791 <div class="peach-settings-row">792 <span>Breach monitoring</span>793 <button794 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}795 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}796 aria-label="Toggle breach monitoring"797 >798 <span class="peach-toggle-knob"></span>799 </button>800 </div>801 <div class="peach-settings-row peach-settings-row--meta">802 <span>Check all passwords</span>803 <button804 class="peach-btn peach-btn--subtle peach-btn--sm"805 onClick={() => void handleCheckAllBreaches()}806 disabled={checkingBreaches()}807 >808 {checkingBreaches() ? "checking..." : "run"}809 </button>810 </div>811 <Show when={breachResult()}>812 <div class="peach-settings-row peach-settings-row--meta">813 <span>Last check</span>814 <span class="peach-settings-value">{breachResult()}</span>815 </div>816 </Show>817818 <p class="peach-settings-sub-label">MASTER PASSWORD</p>819 <div class="peach-settings-row peach-settings-row--meta">820 <span>Master password</span>821 <button822 class="peach-btn peach-btn--subtle peach-btn--sm"823 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}824 >825 {showChangePw() ? "cancel" : "change"}826 </button>827 </div>828 <Show when={showChangePw()}>829 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">830 <input831 class="peach-settings-input"832 type="password"833 placeholder="current password"834 value={changePwCurrent()}835 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}836 autocomplete="current-password"837 />838 <input839 class="peach-settings-input"840 type="password"841 placeholder="new password (min 12 chars)"842 value={changePwNew()}843 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}844 autocomplete="new-password"845 />846 <input847 class="peach-settings-input"848 type="password"849 placeholder="confirm new password"850 value={changePwConfirm()}851 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}852 autocomplete="new-password"853 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}854 />855 <Show when={changePwError()}>856 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>857 </Show>858 <Show when={changePwSuccess()}>859 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>860 </Show>861 <button862 class="peach-btn peach-btn--accent peach-btn--sm"863 style="align-self:flex-start"864 onClick={() => void handleChangePassword()}865 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}866 >867 {changePwSaving() ? "saving…" : "save new password"}868 </button>869 </div>870 </Show>871872 <p class="peach-settings-sub-label">DURESS PASSWORD</p>873 <div class="peach-settings-row peach-settings-row--top">874 <div class="peach-settings-row-main">875 <span>Duress password</span>876 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">877 If entered under coercion, shows a decoy empty vault instead of your real one.878 </span>879 </div>880 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>881 {duressEnabled() ? "on" : "off"}882 </div>883 </div>884885 <Show when={duressEnabled() && !showDuressSetup()}>886 <div class="peach-settings-row peach-settings-row--meta">887 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">888 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.889 </span>890 </div>891 </Show>892893 <Show when={duressSuccess()}>894 <div class="peach-settings-row peach-settings-row--meta">895 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>896 </div>897 </Show>898899 <Show when={!showDuressSetup()}>900 <div class="peach-settings-row peach-settings-row--meta">901 <button902 class="peach-btn peach-btn--subtle peach-btn--sm"903 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}904 >905 {duressEnabled() ? "Change / disable" : "Set up"}906 </button>907 </div>908 </Show>909910 <Show when={showDuressSetup()}>911 <div class="peach-duress-form">912 <label class="peach-label">main password (to confirm)</label>913 <input914 class="peach-settings-input"915 type="password"916 placeholder="your current master password"917 autocomplete="current-password"918 value={duressMainPassword()}919 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}920 />921 <Show when={!duressEnabled()}>922 <label class="peach-label">new duress password</label>923 <input924 class="peach-settings-input"925 type="password"926 placeholder="duress password (min 8 chars)"927 autocomplete="new-password"928 value={duressPassword()}929 onInput={(e) => setDuressPassword(e.currentTarget.value)}930 />931 <label class="peach-label">confirm duress password</label>932 <input933 class="peach-settings-input"934 type="password"935 placeholder="confirm duress password"936 autocomplete="new-password"937 value={duressConfirm()}938 onInput={(e) => setDuressConfirm(e.currentTarget.value)}939 />940 </Show>941942 <Show when={duressError()}>943 <p class="peach-hint peach-hint-error">{duressError()}</p>944 </Show>945946 <div class="peach-duress-actions">947 <button948 class="peach-btn peach-btn--subtle peach-btn--sm"949 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}950 >951 cancel952 </button>953 <Show when={duressEnabled()}>954 <button955 class="peach-btn peach-btn--ghost peach-btn--sm"956 style="color:var(--peach-breach)"957 onClick={() => void handleDisableDuress()}958 disabled={duressSaving()}959 >960 {duressSaving() ? "disabling..." : "disable duress"}961 </button>962 </Show>963 <Show when={!duressEnabled()}>964 <button965 class="peach-btn peach-btn--accent peach-btn--sm"966 onClick={() => void handleSetupDuress()}967 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}968 >969 {duressSaving() ? "saving..." : "save"}970 </button>971 </Show>972 </div>973 </div>974 </Show>975976 <p class="peach-settings-sub-label">CANARY ENTRIES</p>977 <div class="peach-settings-row peach-settings-row--top">978 <div class="peach-settings-row-main">979 <span>Canary entries</span>980 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">981 Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.982 </span>983 </div>984 <span class="peach-settings-value" style="flex-shrink:0">985 {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}986 </span>987 </div>988 <Show when={canaryEntries().length > 0}>989 <For each={canaryEntries()}>990 {(entry) => (991 <div class="peach-settings-row peach-settings-row--meta">992 <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>993 <button994 class="peach-btn peach-btn--subtle peach-btn--sm"995 onClick={() => void handleTestCanary(entry)}996 disabled={canaryTestingId() === entry.id}997 style="flex-shrink:0"998 >999 {canaryTestingId() === entry.id1000 ? "sending…"1001 : canaryTestResult()?.id === entry.id1002 ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1003 : "test"}1004 </button>1005 </div>1006 )}1007 </For>1008 </Show>10091010 <p class="peach-settings-sub-label">PASSKEYS</p>1011 <div class="peach-settings-row peach-settings-row--top">1012 <div class="peach-settings-row-main">1013 <span>Intercept passkey requests</span>1014 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1015 Peach can handle passkey sign-in and creation on supported Chromium browsers.1016 </span>1017 </div>1018 <button1019 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1020 onClick={() => void handleTogglePasskeys()}1021 aria-label="Toggle passkey interception"1022 disabled={!passkeyState()?.supported || passkeySaving()}1023 >1024 <span class="peach-toggle-knob"></span>1025 </button>1026 </div>1027 <div class="peach-settings-row peach-settings-row--meta">1028 <span>Status</span>1029 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1030 </div>1031 <div class="peach-settings-row peach-settings-row--meta">1032 <span>Browser</span>1033 <span class="peach-settings-value">1034 {passkeyState()?.browserLabel ?? "Unknown"}1035 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1036 </span>1037 </div>1038 <div class="peach-info-notice" style="margin-top:8px">1039 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1040 </div>1041 <Show when={passkeyState()?.conflict}>1042 <div class="peach-settings-row peach-settings-row--meta">1043 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1044 </div>1045 </Show>1046 </div>1047 </div>1048 );1049 }10501051 function renderVault() {1052 return (1053 <div class="peach-settings-body">1054 <div class="peach-settings-group">1055 <p class="peach-settings-sub-label">ENTRIES</p>1056 <div class="peach-settings-row peach-settings-row--meta">1057 <span>Entries</span>1058 <span class="peach-settings-value">{props.entriesCount}</span>1059 </div>1060 <div class="peach-settings-row peach-settings-row--meta">1061 <span>Duplicates</span>1062 <Show when={dedupDone() !== null} fallback={1063 <button1064 class="peach-btn peach-btn--subtle peach-btn--sm"1065 onClick={() => void handleCountDuplicates()}1066 disabled={dedupRunning()}1067 >1068 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1069 </button>1070 }>1071 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1072 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1073 </span>1074 </Show>1075 </div>1076 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1077 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1078 <div class="peach-restore-warning">1079 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1080 </div>1081 <div style="display:flex;gap:6px">1082 <button1083 class="peach-btn peach-btn--subtle peach-btn--sm"1084 onClick={() => setDedupCount(null)}1085 >1086 cancel1087 </button>1088 <button1089 class="peach-btn peach-btn--accent peach-btn--sm"1090 onClick={() => void handleDeduplicate()}1091 disabled={dedupRunning()}1092 >1093 {dedupRunning() ? "removing..." : "remove duplicates"}1094 </button>1095 </div>1096 </div>1097 </Show>10981099 <p class="peach-settings-sub-label">MAINTENANCE</p>1100 <div class="peach-settings-row peach-settings-row--meta">1101 <span>Trash auto-delete</span>1102 <select1103 class="peach-settings-select"1104 value={String(props.trashRetentionDays)}1105 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1106 >1107 <option value="7">7 days</option>1108 <option value="30">30 days</option>1109 <option value="90">90 days</option>1110 <option value="0">Never</option>1111 </select>1112 </div>1113 <div class="peach-settings-row peach-settings-row--meta">1114 <span>Vault Health</span>1115 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1116 open1117 </button>1118 </div>1119 <Show when={declinedCardCount() > 0}>1120 <div class="peach-settings-row peach-settings-row--meta">1121 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1122 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1123 Clear1124 </button>1125 </div>1126 </Show>11271128 <p class="peach-settings-sub-label">DATA</p>1129 <div class="peach-settings-row peach-settings-row--meta">1130 <span>Import passwords</span>1131 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1132 open1133 </button>1134 </div>1135 <div class="peach-settings-row peach-settings-row--meta">1136 <span>Export backup</span>1137 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1138 open1139 </button>1140 </div>11411142 <p class="peach-settings-sub-label">VAULT HISTORY</p>1143 <Show when={historyLoading()}>1144 <p class="peach-settings-hint">Loading…</p>1145 </Show>1146 <Show when={!historyLoading() && historySnapshots().length === 0}>1147 <p class="peach-settings-hint">1148 No snapshots yet. Peach saves automatically before every change —1149 saves, syncs, and imports.1150 </p>1151 </Show>1152 <Show when={!historyLoading() && historySnapshots().length > 0}>1153 <p class="peach-settings-hint">1154 Restoring locks your vault. Unlock with the password that1155 encrypted that snapshot to confirm.1156 </p>1157 <div class="peach-history-list">1158 <For each={historySnapshots()}>1159 {(snap) => (1160 <div class="peach-history-row">1161 <div class="peach-history-meta">1162 <span class="peach-history-time">1163 {formatSnapshotTime(snap.savedAt)}1164 </span>1165 <span class="peach-history-sub">1166 {[1167 snap.entryCount !== null1168 ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1169 : null,1170 reasonLabel(snap.reason),1171 ]1172 .filter(Boolean)1173 .join(" · ")}1174 </span>1175 </div>1176 <button1177 class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1178 historyRestoring() === snap.id ? " is-busy" : ""1179 }`}1180 onClick={() => void handleRestoreVaultSnapshot(snap.id)}1181 disabled={historyRestoring() !== null}1182 >1183 <Show when={historyRestoring() === snap.id} fallback="Restore">1184 …1185 </Show>1186 </button>1187 </div>1188 )}1189 </For>1190 </div>1191 <p class="peach-history-footer">1192 {historySnapshots().length} of 10 snapshots1193 </p>1194 </Show>11951196 <button1197 class="peach-settings-advanced-toggle"1198 onClick={() => setShowVaultAdvanced((v) => !v)}1199 aria-expanded={showVaultAdvanced()}1200 >1201 <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1202 <span class="peach-settings-advanced-chevron">1203 {showVaultAdvanced() ? "▲" : "▼"}1204 </span>1205 </button>12061207 <Show when={showVaultAdvanced()}>1208 <div class="peach-danger-card">1209 <div class="peach-danger-card-head">1210 <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1211 <h3 class="peach-danger-card-title">Replace vault from backup</h3>1212 </div>1213 <p class="peach-danger-card-copy">1214 Wipes the current vault and restores from a backup file.1215 Recoverable only if your backup is good.1216 </p>1217 <button1218 class="peach-danger-card-button"1219 onClick={() => props.onOpenFileImport?.()}1220 >1221 Choose backup file1222 </button>1223 </div>12241225 <Show when={props.onOpenPanic}>1226 <div class="peach-danger-card peach-danger-card--hazard">1227 <div class="peach-danger-card-head">1228 <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1229 <h3 class="peach-danger-card-title">Destroy vault</h3>1230 </div>1231 <p class="peach-danger-card-copy">1232 Permanently wipes every entry, setting, and license on this1233 device. There is no recovery.1234 </p>1235 <button1236 class="peach-danger-card-button"1237 onClick={() => props.onOpenPanic?.()}1238 >1239 Begin destruction1240 </button>1241 </div>1242 </Show>1243 </Show>1244 </div>1245 </div>1246 );1247 }12481249 function renderSync() {1250 if (!isPro()) {1251 return (1252 <div class="peach-settings-body">1253 <div class="peach-pro-upgrade">1254 <div class="peach-pro-upgrade-hero">1255 <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1256 <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1257 <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1258 <path d="M13.5 15.5C15 12 17 10 18 10C19 10 21 12 22.5 15.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.6"/>1259 <path d="M13.5 20.5C15 24 17 26 18 26C19 26 21 24 22.5 20.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.6"/>1260 <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1261 <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1262 <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1263 </svg>1264 <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1265 <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1266 </div>12671268 <div class="peach-pro-features">1269 <div class="peach-pro-feature">1270 <span class="peach-pro-feature-mark">✦</span>1271 <div class="peach-pro-feature-text">1272 <span class="peach-pro-feature-name">Always in sync</span>1273 <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1274 </div>1275 </div>1276 <div class="peach-pro-feature">1277 <span class="peach-pro-feature-mark">✦</span>1278 <div class="peach-pro-feature-text">1279 <span class="peach-pro-feature-name">No cloud. No relay.</span>1280 <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1281 </div>1282 </div>1283 <div class="peach-pro-feature">1284 <span class="peach-pro-feature-mark">✦</span>1285 <div class="peach-pro-feature-text">1286 <span class="peach-pro-feature-name">Tailscale supported</span>1287 <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1288 </div>1289 </div>1290 <div class="peach-pro-feature">1291 <span class="peach-pro-feature-mark">✦</span>1292 <div class="peach-pro-feature-text">1293 <span class="peach-pro-feature-name">Zero friction after setup</span>1294 <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1295 </div>1296 </div>1297 </div>12981299 <button1300 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1301 onClick={() => setActiveSection("license")}1302 >1303 Upgrade to Pro1304 </button>1305 <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1306 Already have a key? Enter it →1307 </button>1308 </div>1309 </div>1310 );1311 }13121313 return (1314 <div class="peach-settings-body">1315 <div class="peach-settings-group">1316 <div class="peach-settings-row">1317 <span>Status</span>1318 <span class="peach-settings-value peach-settings-value--status">1319 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1320 {getStatusLabel(syncStatus())}1321 </span>1322 </div>1323 <div class="peach-settings-row peach-settings-row--meta">1324 <span>Last sync</span>1325 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1326 </div>1327 <div class="peach-settings-row peach-settings-row--meta">1328 <button1329 class="peach-btn peach-btn--subtle peach-btn--sm"1330 onClick={() => void handleSyncNow()}1331 disabled={syncStatus() === "not_trusted" || syncingNow()}1332 >1333 <Switch>1334 <Match when={syncingNow()}>Syncing…</Match>1335 <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1336 <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1337 <Match when={true}>Sync now</Match>1338 </Switch>1339 </button>1340 </div>1341 <p class="peach-settings-hint">1342 If you edit on multiple devices before syncing, the most recently1343 changed device wins. Use Vault History above to recover older1344 versions if needed.1345 </p>13461347 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1348 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1349 <Show when={pairedDevices().length === 0}>1350 <p class="peach-settings-hint">1351 No paired devices yet. Add a device to start syncing.1352 </p>1353 </Show>1354 <Show when={pairedDevices().length > 0}>1355 <div class="peach-history-list">1356 <For each={pairedDevices()}>1357 {(device) => (1358 <div class="peach-history-row">1359 <div class="peach-history-meta">1360 <span class="peach-history-time">{device.displayName}</span>1361 <span class="peach-history-sub">1362 {device.lastSyncAt1363 ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1364 : "Never synced"}1365 </span>1366 </div>1367 <button1368 class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1369 onClick={() => void handleRemoveDevice(device.deviceId)}1370 >1371 Remove1372 </button>1373 </div>1374 )}1375 </For>1376 </div>1377 </Show>1378 <div class="peach-settings-actions--row">1379 <button1380 class="peach-btn peach-btn--subtle peach-btn--sm"1381 onClick={() => props.onOpenSyncPairing?.()}1382 >1383 Add a device1384 </button>1385 </div>1386 </div>13871388 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1389 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1390 <For each={trustedNetworks()}>1391 {(network) => (1392 <div class="peach-sync-device-row">1393 <span class="peach-sync-device-name">1394 {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1395 </span>1396 <Show1397 when={!network.isTailscale}1398 fallback={<span class="peach-sync-device-meta">Always trusted</span>}1399 >1400 <button1401 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1402 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1403 >1404 Remove1405 </button>1406 </Show>1407 </div>1408 )}1409 </For>1410 <div class="peach-settings-actions--row">1411 <Show when={!isCurrentNetworkTrusted()}>1412 <button1413 class="peach-btn peach-btn--subtle peach-btn--sm"1414 onClick={() => void handleMarkNetworkTrusted()}1415 disabled={markingNetwork()}1416 >1417 {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1418 </button>1419 </Show>1420 </div>1421 </div>14221423 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1424 <span class="peach-sync-section-label">Advanced</span>1425 <Show1426 when={resetIdentityConfirm()}1427 fallback={1428 <div class="peach-sync-advanced-row">1429 <div class="peach-sync-advanced-info">1430 <span class="peach-sync-advanced-name">Reset sync identity</span>1431 <span class="peach-sync-device-meta">1432 Generates a new keypair and unpairs every device.1433 </span>1434 </div>1435 <button1436 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1437 onClick={() => setResetIdentityConfirm(true)}1438 >1439 Reset1440 </button>1441 </div>1442 }1443 >1444 <div class="peach-sync-reset-confirm">1445 <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1446 <p class="peach-sync-reset-confirm-body">1447 This will unpair all devices. You will need to pair them again from scratch.1448 </p>1449 <div class="peach-settings-actions--row">1450 <button1451 class="peach-btn peach-btn--ghost peach-btn--sm"1452 onClick={() => setResetIdentityConfirm(false)}1453 disabled={resetIdentityRunning()}1454 >1455 Cancel1456 </button>1457 <button1458 class="peach-btn peach-btn--danger peach-btn--sm"1459 onClick={() => void handleResetSyncIdentity()}1460 disabled={resetIdentityRunning()}1461 >1462 {resetIdentityRunning() ? "Resetting..." : "Reset"}1463 </button>1464 </div>1465 </div>1466 </Show>1467 </div>1468 </div>1469 </div>1470 );1471 }14721473 function renderAppearance() {1474 const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1475 { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1476 { id: "cormorant", label: "Cormorant", preview: "Aa" },1477 { id: "inter", label: "Inter", preview: "Aa" },1478 { id: "system", label: "System", preview: "Aa" },1479 ];1480 const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1481 { id: "small", label: "Small" },1482 { id: "medium", label: "Default" },1483 { id: "large", label: "Large" },1484 ];1485 return (1486 <div class="peach-settings-body">1487 <div class="peach-settings-group">1488 <p class="peach-settings-sub-label">THEME</p>1489 <div class="peach-settings-row peach-settings-row--meta">1490 <span>Theme</span>1491 <button1492 class="peach-btn peach-btn--subtle peach-btn--sm"1493 onClick={() =>1494 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1495 }1496 >1497 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1498 </button>1499 </div>15001501 <p class="peach-settings-sub-label">ACCENT</p>1502 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1503 <span>Accent</span>1504 <div class="peach-accent-picker">1505 <For each={ACCENT_COLORS}>1506 {(color) => (1507 <button1508 class="peach-accent-swatch"1509 classList={{ "is-active": props.accentColor === color.name }}1510 style={{ background: color.hex }}1511 onClick={() => void props.onAccentChange(color.name)}1512 aria-label={color.name}1513 title={color.name}1514 />1515 )}1516 </For>1517 </div>1518 </div>15191520 <p class="peach-settings-sub-label">DISPLAY FONT</p>1521 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1522 <span>Font</span>1523 <div class="peach-font-picker">1524 <For each={fontOptions}>1525 {(opt) => (1526 <button1527 class="peach-font-swatch"1528 classList={{ "is-active": props.displayFont === opt.id }}1529 data-font-preview={opt.id}1530 onClick={() => void props.onDisplayFontChange(opt.id)}1531 aria-label={opt.label}1532 title={opt.label}1533 >1534 <span class="peach-font-swatch-glyph">{opt.preview}</span>1535 <span class="peach-font-swatch-name">{opt.label}</span>1536 </button>1537 )}1538 </For>1539 </div>1540 </div>15411542 <p class="peach-settings-sub-label">TEXT SIZE</p>1543 <div class="peach-settings-row peach-settings-row--meta">1544 <span>Size</span>1545 <div class="peach-pill-group">1546 <For each={sizeOptions}>1547 {(opt) => (1548 <button1549 class="peach-btn peach-btn--ghost peach-btn--sm"1550 classList={{ "is-active": props.textSize === opt.id }}1551 onClick={() => void props.onTextSizeChange(opt.id)}1552 >1553 {opt.label}1554 </button>1555 )}1556 </For>1557 </div>1558 </div>15591560 <p class="peach-settings-sub-label">MOTION</p>1561 <div class="peach-settings-row peach-settings-row--meta">1562 <span>Reduce motion</span>1563 <button1564 class="peach-btn peach-btn--subtle peach-btn--sm"1565 onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1566 >1567 {props.reduceMotion ? "On" : "Off"}1568 </button>1569 </div>1570 </div>1571 </div>1572 );1573 }15741575 function renderShortcuts() {1576 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1577 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15781579 function startCapture(action: ShortcutAction) {1580 setConflictMsg(null);1581 setCapturingAction(action);1582 }1583 function cancelCapture() {1584 setCapturingAction(null);1585 setConflictMsg(null);1586 }1587 function commitBinding(action: ShortcutAction, binding: Keybinding) {1588 const conflict = findConflict(props.shortcuts, binding, action);1589 if (conflict) {1590 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1591 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1592 return;1593 }1594 const next = { ...props.shortcuts, [action]: binding };1595 setCapturingAction(null);1596 setConflictMsg(null);1597 void props.onShortcutsChange(next);1598 }1599 function resetAction(action: ShortcutAction) {1600 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1601 void props.onShortcutsChange(next);1602 }1603 function resetAll() {1604 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1605 }16061607 1608 1609 function handleCaptureKey(e: KeyboardEvent) {1610 e.preventDefault();1611 e.stopPropagation();1612 if (e.key === "Escape") { cancelCapture(); return; }1613 const binding = keybindingFromEvent(e);1614 if (!binding) return; 1615 const action = capturingAction();1616 if (action) commitBinding(action, binding);1617 }16181619 return (1620 <div class="peach-settings-body">1621 <div class="peach-settings-group peach-shortcuts-group">1622 <p class="peach-settings-sub-label">ENTRY VIEW</p>1623 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1624 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1625 </For>1626 </div>16271628 <div class="peach-settings-group peach-shortcuts-group">1629 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1630 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1631 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1632 </For>1633 </div>16341635 <Show when={conflictMsg()}>1636 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1637 </Show>16381639 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1640 <div class="peach-settings-row peach-settings-row--meta">1641 <span>Inline autofill on page</span>1642 <a1643 class="peach-settings-link"1644 href="chrome://extensions/shortcuts"1645 onClick={(e) => {1646 e.preventDefault();1647 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1648 }}1649 >1650 ⌘⇧P — customize ↗1651 </a>1652 </div>1653 <button1654 class="peach-btn peach-btn--subtle peach-btn--sm"1655 style="align-self:flex-start"1656 onClick={resetAll}1657 >1658 Reset all to defaults1659 </button>1660 </div>1661 </div>1662 );16631664 function renderRow(1665 desc: typeof SHORTCUT_DESCRIPTORS[number],1666 capturing: () => ShortcutAction | null,1667 onStart: (a: ShortcutAction) => void,1668 onCancel: () => void,1669 onReset: (a: ShortcutAction) => void,1670 onKey: (e: KeyboardEvent) => void,1671 ) {1672 const isActive = () => capturing() === desc.action;1673 const binding = () => props.shortcuts[desc.action];1674 const isCustom = () => {1675 const b = binding();1676 const d = DEFAULT_BINDINGS[desc.action];1677 return b.key !== d.key1678 || Boolean(b.ctrl) !== Boolean(d.ctrl)1679 || Boolean(b.meta) !== Boolean(d.meta)1680 || Boolean(b.shift) !== Boolean(d.shift)1681 || Boolean(b.alt) !== Boolean(d.alt);1682 };1683 return (1684 <div class="peach-shortcut-row">1685 <span class="peach-shortcut-row-label">{desc.label}</span>1686 <button1687 class="peach-shortcut-chip"1688 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1689 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1690 onKeyDown={(e) => { if (isActive()) onKey(e); }}1691 ref={(el) => {1692 1693 if (el) {1694 queueMicrotask(() => { if (isActive()) el.focus(); });1695 }1696 }}1697 >1698 {isActive() ? "Press a key…" : formatKeybinding(binding())}1699 </button>1700 <Show when={isCustom() && !isActive()}>1701 <button1702 class="peach-shortcut-reset"1703 title="Reset to default"1704 onClick={() => onReset(desc.action)}1705 >1706 ↺1707 </button>1708 </Show>1709 </div>1710 );1711 }1712 }17131714 const PRO_FEATURES: Array<{1715 feature: ProFeature;1716 name: string;1717 desc: string;1718 }> = [1719 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1720 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1721 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1722 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1723 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1724 ];17251726 const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17271728 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1729 const [deactivating, setDeactivating] = createSignal(false);17301731 async function handleDeactivateLicense() {1732 setDeactivating(true);1733 await sendMessage({ type: "DEACTIVATE_LICENSE" });1734 setLicenseInfo(null);1735 await props.onLicenseChange?.();1736 setDeactivating(false);1737 }17381739 async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1740 setCanaryTestingId(entry.id);1741 setCanaryTestResult(null);1742 const res = await sendMessage({1743 type: "FIRE_CANARY_PING",1744 payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1745 }) as { ok: boolean };1746 setCanaryTestResult({ id: entry.id, ok: res.ok });1747 setCanaryTestingId(null);1748 setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1749 }17501751 function renderLicense() {1752 async function onActivate() {1753 await handleActivateLicense();1754 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1755 await props.onLicenseChange?.();1756 }1757 }17581759 return (1760 <div class="peach-settings-body">1761 <Show1762 when={isPro()}1763 fallback={1764 <Show1765 when={showKeyEntry()}1766 fallback={1767 <div class="peach-pro-upgrade">1768 <div class="peach-pro-upgrade-hero">1769 <div class="peach-pro-upgrade-mark-wrap">1770 <PeachMark class="peach-pro-upgrade-mark" />1771 </div>1772 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1773 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1774 </div>17751776 <div class="peach-pro-features">1777 <For each={PRO_FEATURES}>1778 {(item) => (1779 <div1780 class="peach-pro-feature"1781 classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1782 >1783 <span class="peach-pro-feature-mark">✦</span>1784 <div class="peach-pro-feature-text">1785 <span class="peach-pro-feature-name">{item.name}</span>1786 <span class="peach-pro-feature-desc">{item.desc}</span>1787 </div>1788 </div>1789 )}1790 </For>1791 </div>17921793 <div class="peach-pro-upgrade-actions">1794 <p class="peach-pro-pricing">$39 · once · yours forever</p>1795 <a1796 href="https://peachpasswords.com/#pricing"1797 target="_blank"1798 rel="noopener noreferrer"1799 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1800 >1801 Get Peach Pro1802 </a>1803 <div class="peach-pro-key-divider"><span>or</span></div>1804 <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1805 Activate a license key1806 </button>1807 </div>1808 </div>1809 }1810 >1811 <div class="peach-pro-key-ceremony">1812 <LockRuneCanvas />1813 <p class="peach-pro-key-ceremony-label">Enter your license key</p>1814 <input1815 class="peach-underline-input peach-pro-key-input"1816 type="text"1817 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1818 value={licenseKeyInput()}1819 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1820 onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1821 autocomplete="off"1822 spellcheck={false}1823 />1824 <Show when={licenseError()}>1825 <p class="peach-pro-key-error">That key doesn't look right.</p>1826 </Show>1827 <button1828 class="peach-btn peach-btn--accent peach-pro-key-activate"1829 onClick={() => void onActivate()}1830 disabled={activatingLicense() || !licenseKeyInput().trim()}1831 >1832 {activatingLicense() ? "activating…" : "Activate"}1833 </button>1834 <button1835 class="peach-pro-key-cancel"1836 onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1837 >1838 cancel1839 </button>1840 </div>1841 </Show>1842 }1843 >1844 <div class="peach-pro-active">1845 <div class="peach-pro-active-hero">1846 <div class="peach-pro-active-mark-wrap">1847 <PeachMark class="peach-pro-active-mark" />1848 </div>1849 <h2 class="peach-pro-active-title">Peach Pro</h2>1850 <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1851 </div>18521853 <Show when={licenseKey()}>1854 <div class="peach-settings-license">1855 <span class="peach-settings-label">LICENSE KEY</span>1856 <div class="peach-settings-license-row">1857 <span class="peach-settings-license-key">{licenseKey()!}</span>1858 <button1859 class="peach-settings-license-copy"1860 onClick={() => copyLicenseKey(licenseKey()!)}1861 >1862 {copied() ? "✓" : "copy"}1863 </button>1864 </div>1865 </div>1866 </Show>18671868 <div class="peach-pro-active-features">1869 <For each={PRO_FEATURES}>1870 {(item) => (1871 <div class="peach-pro-active-feature">1872 <span class="peach-pro-active-feature-mark">✦</span>1873 <span class="peach-pro-active-feature-name">{item.name}</span>1874 </div>1875 )}1876 </For>1877 </div>18781879 <button1880 class="peach-pro-deactivate"1881 onClick={() => void handleDeactivateLicense()}1882 disabled={deactivating()}1883 >1884 {deactivating() ? "Deactivating…" : "Deactivate"}1885 </button>1886 </div>1887 </Show>1888 </div>1889 );1890 }18911892 function renderFeedback() {1893 return (1894 <div class="peach-settings-body">1895 <div class="peach-settings-group peach-feedback-group">1896 <p class="peach-feedback-intro">1897 Help us improve Peach. Every report goes directly to our inbox.1898 </p>18991900 <p class="peach-settings-sub-label">TYPE</p>1901 <div class="peach-feedback-chips">1902 <For each={FEEDBACK_CATEGORIES}>1903 {(cat) => (1904 <button1905 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1906 onClick={() => setFbCategory(cat)}1907 >1908 {cat}1909 </button>1910 )}1911 </For>1912 </div>19131914 <p class="peach-settings-sub-label">MESSAGE</p>1915 <textarea1916 class="peach-feedback-textarea"1917 placeholder="Describe the issue or idea..."1918 value={fbMessage()}1919 onInput={(e) => setFbMessage(e.currentTarget.value)}1920 />19211922 <p class="peach-feedback-meta">1923 Version {version} · will be included automatically1924 </p>19251926 <Show when={fbResult() === "success"}>1927 <p class="peach-feedback-result peach-feedback-result--ok">1928 Sent — thank you1929 </p>1930 </Show>1931 <Show when={fbResult() === "error"}>1932 <p class="peach-feedback-result peach-feedback-result--err">1933 Failed to send — try again1934 </p>1935 </Show>19361937 <button1938 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1939 onClick={() => void handleSubmitFeedback()}1940 disabled={fbSubmitting() || !fbMessage().trim()}1941 >1942 {fbSubmitting() ? "Sending..." : "Send feedback"}1943 </button>19441945 <p class="peach-feedback-anon">1946 Sent anonymously · no account required1947 </p>1948 </div>1949 </div>1950 );1951 }19521953 function renderMobile() {1954 return (1955 <div class="peach-settings-body">1956 <div class="peach-settings-group">1957 <p class="peach-settings-label">Autofill service</p>1958 <div class="peach-mobile-row">1959 <span class="peach-mobile-status">1960 {autofillEnabled() ? "Enabled" : "Disabled"}1961 </span>1962 <button1963 class="peach-btn peach-btn--subtle peach-btn--sm"1964 onClick={() => void AutofillPlugin?.openAutofillSettings()}1965 >1966 {autofillEnabled() ? "Change" : "Enable"}1967 </button>1968 </div>1969 </div>19701971 <div class="peach-settings-group">1972 <p class="peach-settings-label">Biometric unlock</p>1973 <div class="peach-mobile-row">1974 <span class="peach-mobile-status">1975 {biometricEnabled() ? "Available" : "Not available"}1976 </span>1977 </div>1978 </div>1979 </div>1980 );1981 }19821983 return (1984 <div class="peach-overlay-view ov-enter-right" style="display:flex">1985 <div class="peach-overlay-header">1986 <button1987 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1988 onClick={handleHeaderBack}1989 aria-label="Back"1990 >1991 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1992 <path d="M6 1L1 6L6 11" />1993 </svg>1994 </button>1995 <span class="peach-overlay-title">{currentTitle()}</span>1996 <div class="peach-overlay-spacer"></div>1997 </div>19981999 <Switch fallback={renderHome()}>2000 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>2001 <Match when={activeSection() === "vault"}>{renderVault()}</Match>2002 <Match when={activeSection() === "sync"}>{renderSync()}</Match>2003 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2004 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2005 <Match when={activeSection() === "license"}>{renderLicense()}</Match>2006 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2007 <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>2008 </Switch>2009 </div>2010 );2011}