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 const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });481 setResetIdentityRunning(false);482 if ((res as { ok: boolean }).ok) {483 setPairedDevices([]);484 setResetIdentityConfirm(false);485 }486 }487488 function formatRelativeTime(timestamp?: number): string {489 if (!timestamp) return "Never";490 const diff = Date.now() - timestamp;491 const minutes = Math.floor(diff / 60000);492 const hours = Math.floor(diff / 3600000);493 const days = Math.floor(diff / 86400000);494 if (minutes < 1) return "Just now";495 if (minutes < 60) return `${minutes}m ago`;496 if (hours < 24) return `${hours}h ago`;497 return `${days}d ago`;498 }499500 function getStatusColor(status: SyncStatus): string {501 switch (status) {502 case "synced": return "var(--peach-sync-ok)";503 case "syncing": return "var(--peach-sync-busy)";504 case "no_peers": return "var(--peach-sync-off)";505 case "not_trusted": return "var(--peach-sync-err)";506 case "offline": return "var(--peach-sync-off)";507 default: return "var(--peach-sync-off)";508 }509 }510511 function getStatusLabel(status: SyncStatus): string {512 switch (status) {513 case "synced": return "Synced";514 case "syncing": return "Syncing";515 case "no_peers": return "No peers visible";516 case "not_trusted": return "Not on a trusted network";517 case "offline": return "Offline";518 default: return "Unknown";519 }520 }521522 function isCurrentNetworkTrusted(): boolean {523 return trustedNetworks().length > 0;524 }525526 async function handleSetupDuress() {527 const mainPw = duressMainPassword().trim();528 const duressPw = duressPassword().trim();529 const confirmPw = duressConfirm().trim();530531 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }532 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }533 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }534 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }535536 setDuressSaving(true);537 setDuressError(null);538539 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };540 if (!verifyRes.ok || !verifyRes.data) {541 setDuressError("incorrect main password");542 setDuressSaving(false);543 return;544 }545546 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };547 setDuressSaving(false);548 if (res.ok) {549 setDuressEnabled(true);550 setShowDuressSetup(false);551 setDuressPassword("");552 setDuressConfirm("");553 setDuressMainPassword("");554 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");555 setTimeout(() => setDuressSuccess(null), 5000);556 } else {557 setDuressError(res.error ?? "failed to set duress password");558 }559 }560561 async function handleClearDeclinedCards() {562 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });563 setDeclinedCardCount(0);564 }565566 async function handleCountDuplicates() {567 setDedupRunning(true);568 setDedupDone(null);569 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };570 setDedupRunning(false);571 if (res.ok) setDedupCount(res.data ?? 0);572 }573574 async function handleDeduplicate() {575 setDedupRunning(true);576 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };577 setDedupRunning(false);578 if (res.ok) {579 setDedupDone(res.data ?? 0);580 setDedupCount(0);581 }582 }583584 async function handleDisableDuress() {585 const mainPw = duressMainPassword().trim();586 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }587588 setDuressSaving(true);589 setDuressError(null);590591 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };592 if (!verifyRes.ok || !verifyRes.data) {593 setDuressError("incorrect main password");594 setDuressSaving(false);595 return;596 }597598 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };599 setDuressSaving(false);600 if (res.ok) {601 setDuressEnabled(false);602 setShowDuressSetup(false);603 setDuressMainPassword("");604 setDuressSuccess("Duress password disabled.");605 setTimeout(() => setDuressSuccess(null), 4000);606 } else {607 setDuressError(res.error ?? "failed to disable duress password");608 }609 }610611 async function handleSubmitFeedback() {612 if (!fbMessage().trim() || fbSubmitting()) return;613 setFbSubmitting(true);614 setFbResult(null);615 const res = await sendMessage({616 type: "SUBMIT_FEEDBACK",617 payload: {618 category: fbCategory(),619 message: fbMessage().trim(),620 version,621 browser: navigator.userAgent,622 },623 }) as { ok: boolean; error?: string };624 setFbSubmitting(false);625 if (res.ok) {626 setFbResult("success");627 setFbMessage("");628 setTimeout(() => setFbResult(null), 4000);629 } else {630 setFbResult("error");631 }632 }633634 function handleHeaderBack() {635 if (activeSection() !== null) {636 setActiveSection(null);637 } else {638 props.onBack();639 }640 }641642 function currentTitle(): string {643 const s = activeSection();644 if (s === null) return "settings";645 if (s === "license") return "account";646 return s;647 }648649 function renderCatRow(opts: {650 section: SectionName;651 name: string;652 desc: string;653 icon: () => any;654 pro?: boolean;655 }) {656 return (657 <button658 class="peach-settings-cat-row"659 onClick={() => setActiveSection(opts.section)}660 >661 <div class="peach-settings-cat-icon">{opts.icon()}</div>662 <div class="peach-settings-cat-content">663 <span class="peach-settings-cat-name">664 {opts.name}665 <Show when={opts.pro && !isPro()}>666 <span class="peach-pro-mark" aria-label="Pro feature">✦</span>667 </Show>668 </span>669 <span class="peach-settings-cat-desc">{opts.desc}</span>670 </div>671 <svg672 class="peach-settings-cat-chevron"673 width="6" height="10" viewBox="0 0 6 10" fill="none"674 stroke="currentColor" stroke-width="1.1"675 stroke-linecap="round" stroke-linejoin="round"676 >677 <path d="M1 1l4 4-4 4" />678 </svg>679 </button>680 );681 }682683 const iconShield = () => (684 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">685 <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>686 </svg>687 );688 const iconKey = () => (689 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">690 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>691 </svg>692 );693 const iconLock = () => (694 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">695 <rect x="3" y="7" width="10" height="7" rx="1.5"/>696 <path d="M6 7V5a3 3 0 016 0v2"/>697 </svg>698 );699 const iconSync = () => (700 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">701 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>702 <path d="M14 6l-2 2-2-2"/>703 <path d="M2 10l2-2 2 2"/>704 </svg>705 );706 const iconSun = () => (707 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">708 <circle cx="8" cy="8" r="3"/>709 <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"/>710 </svg>711 );712 const iconKeys = () => (713 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">714 <rect x="2" y="5" width="5" height="4" rx="1"/>715 <rect x="9" y="7" width="5" height="4" rx="1"/>716 <path d="M7 7h2m-2 2h2"/>717 </svg>718 );719 const iconBubble = () => (720 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">721 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>722 </svg>723 );724725 function renderHome() {726 return (727 <div class="peach-settings-body">728 <p class="peach-settings-nav-label">PROTECTION</p>729 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}730 {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}731732 <p class="peach-settings-nav-label">VAULT</p>733 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}734 {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}735736 <p class="peach-settings-nav-label">PREFERENCES</p>737 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}738 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}739 <Show when={isAndroid}>740 <p class="peach-settings-nav-label">MOBILE</p>741 {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}742 </Show>743744 <p class="peach-settings-nav-label">SUPPORT</p>745 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}746747 <div class="peach-settings-footer">748 <span>Peach {version}</span>749 <span class="peach-divider"></span>750 <span>Browser extension</span>751 </div>752 </div>753 );754 }755756 function renderSecurity() {757 return (758 <div class="peach-settings-body">759 <div class="peach-settings-group">760 <p class="peach-settings-sub-label">LOCKING</p>761 <div class="peach-settings-row">762 <span>Lock on close</span>763 <button764 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}765 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}766 aria-label="Toggle lock on close"767 >768 <span class="peach-toggle-knob"></span>769 </button>770 </div>771 <div class="peach-settings-row">772 <span>Auto-lock after</span>773 <select774 class="peach-settings-select"775 value={String(props.autoLockMinutes)}776 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}777 >778 <option value="1">1 minute</option>779 <option value="5">5 minutes</option>780 <option value="15">15 minutes</option>781 <option value="30">30 minutes</option>782 <option value="0">Never</option>783 </select>784 </div>785786 <p class="peach-settings-sub-label">BREACH PROTECTION</p>787 <div class="peach-settings-row">788 <span>Breach monitoring</span>789 <button790 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}791 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}792 aria-label="Toggle breach monitoring"793 >794 <span class="peach-toggle-knob"></span>795 </button>796 </div>797 <div class="peach-settings-row peach-settings-row--meta">798 <span>Check all passwords</span>799 <button800 class="peach-btn peach-btn--subtle peach-btn--sm"801 onClick={() => void handleCheckAllBreaches()}802 disabled={checkingBreaches()}803 >804 {checkingBreaches() ? "checking..." : "run"}805 </button>806 </div>807 <Show when={breachResult()}>808 <div class="peach-settings-row peach-settings-row--meta">809 <span>Last check</span>810 <span class="peach-settings-value">{breachResult()}</span>811 </div>812 </Show>813814 <p class="peach-settings-sub-label">MASTER PASSWORD</p>815 <div class="peach-settings-row peach-settings-row--meta">816 <span>Master password</span>817 <button818 class="peach-btn peach-btn--subtle peach-btn--sm"819 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}820 >821 {showChangePw() ? "cancel" : "change"}822 </button>823 </div>824 <Show when={showChangePw()}>825 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">826 <input827 class="peach-settings-input"828 type="password"829 placeholder="current password"830 value={changePwCurrent()}831 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}832 autocomplete="current-password"833 />834 <input835 class="peach-settings-input"836 type="password"837 placeholder="new password (min 12 chars)"838 value={changePwNew()}839 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}840 autocomplete="new-password"841 />842 <input843 class="peach-settings-input"844 type="password"845 placeholder="confirm new password"846 value={changePwConfirm()}847 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}848 autocomplete="new-password"849 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}850 />851 <Show when={changePwError()}>852 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>853 </Show>854 <Show when={changePwSuccess()}>855 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>856 </Show>857 <button858 class="peach-btn peach-btn--accent peach-btn--sm"859 style="align-self:flex-start"860 onClick={() => void handleChangePassword()}861 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}862 >863 {changePwSaving() ? "saving…" : "save new password"}864 </button>865 </div>866 </Show>867868 <p class="peach-settings-sub-label">DURESS PASSWORD</p>869 <div class="peach-settings-row peach-settings-row--top">870 <div class="peach-settings-row-main">871 <span>Duress password</span>872 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">873 If entered under coercion, shows a decoy empty vault instead of your real one.874 </span>875 </div>876 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>877 {duressEnabled() ? "on" : "off"}878 </div>879 </div>880881 <Show when={duressEnabled() && !showDuressSetup()}>882 <div class="peach-settings-row peach-settings-row--meta">883 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">884 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.885 </span>886 </div>887 </Show>888889 <Show when={duressSuccess()}>890 <div class="peach-settings-row peach-settings-row--meta">891 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>892 </div>893 </Show>894895 <Show when={!showDuressSetup()}>896 <div class="peach-settings-row peach-settings-row--meta">897 <button898 class="peach-btn peach-btn--subtle peach-btn--sm"899 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}900 >901 {duressEnabled() ? "Change / disable" : "Set up"}902 </button>903 </div>904 </Show>905906 <Show when={showDuressSetup()}>907 <div class="peach-duress-form">908 <label class="peach-label">main password (to confirm)</label>909 <input910 class="peach-settings-input"911 type="password"912 placeholder="your current master password"913 autocomplete="current-password"914 value={duressMainPassword()}915 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}916 />917 <Show when={!duressEnabled()}>918 <label class="peach-label">new duress password</label>919 <input920 class="peach-settings-input"921 type="password"922 placeholder="duress password (min 8 chars)"923 autocomplete="new-password"924 value={duressPassword()}925 onInput={(e) => setDuressPassword(e.currentTarget.value)}926 />927 <label class="peach-label">confirm duress password</label>928 <input929 class="peach-settings-input"930 type="password"931 placeholder="confirm duress password"932 autocomplete="new-password"933 value={duressConfirm()}934 onInput={(e) => setDuressConfirm(e.currentTarget.value)}935 />936 </Show>937938 <Show when={duressError()}>939 <p class="peach-hint peach-hint-error">{duressError()}</p>940 </Show>941942 <div class="peach-duress-actions">943 <button944 class="peach-btn peach-btn--subtle peach-btn--sm"945 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}946 >947 cancel948 </button>949 <Show when={duressEnabled()}>950 <button951 class="peach-btn peach-btn--ghost peach-btn--sm"952 style="color:var(--peach-breach)"953 onClick={() => void handleDisableDuress()}954 disabled={duressSaving()}955 >956 {duressSaving() ? "disabling..." : "disable duress"}957 </button>958 </Show>959 <Show when={!duressEnabled()}>960 <button961 class="peach-btn peach-btn--accent peach-btn--sm"962 onClick={() => void handleSetupDuress()}963 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}964 >965 {duressSaving() ? "saving..." : "save"}966 </button>967 </Show>968 </div>969 </div>970 </Show>971972 <p class="peach-settings-sub-label">CANARY ENTRIES</p>973 <div class="peach-settings-row peach-settings-row--top">974 <div class="peach-settings-row-main">975 <span>Canary entries</span>976 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">977 Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.978 </span>979 </div>980 <span class="peach-settings-value" style="flex-shrink:0">981 {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}982 </span>983 </div>984 <Show when={canaryEntries().length > 0}>985 <For each={canaryEntries()}>986 {(entry) => (987 <div class="peach-settings-row peach-settings-row--meta">988 <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>989 <button990 class="peach-btn peach-btn--subtle peach-btn--sm"991 onClick={() => void handleTestCanary(entry)}992 disabled={canaryTestingId() === entry.id}993 style="flex-shrink:0"994 >995 {canaryTestingId() === entry.id996 ? "sending…"997 : canaryTestResult()?.id === entry.id998 ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")999 : "test"}1000 </button>1001 </div>1002 )}1003 </For>1004 </Show>10051006 <p class="peach-settings-sub-label">PASSKEYS</p>1007 <div class="peach-settings-row peach-settings-row--top">1008 <div class="peach-settings-row-main">1009 <span>Intercept passkey requests</span>1010 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1011 Peach can handle passkey sign-in and creation on supported Chromium browsers.1012 </span>1013 </div>1014 <button1015 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1016 onClick={() => void handleTogglePasskeys()}1017 aria-label="Toggle passkey interception"1018 disabled={!passkeyState()?.supported || passkeySaving()}1019 >1020 <span class="peach-toggle-knob"></span>1021 </button>1022 </div>1023 <div class="peach-settings-row peach-settings-row--meta">1024 <span>Status</span>1025 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1026 </div>1027 <div class="peach-settings-row peach-settings-row--meta">1028 <span>Browser</span>1029 <span class="peach-settings-value">1030 {passkeyState()?.browserLabel ?? "Unknown"}1031 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1032 </span>1033 </div>1034 <div class="peach-info-notice" style="margin-top:8px">1035 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1036 </div>1037 <Show when={passkeyState()?.conflict}>1038 <div class="peach-settings-row peach-settings-row--meta">1039 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1040 </div>1041 </Show>1042 </div>1043 </div>1044 );1045 }10461047 function renderVault() {1048 return (1049 <div class="peach-settings-body">1050 <div class="peach-settings-group">1051 <p class="peach-settings-sub-label">ENTRIES</p>1052 <div class="peach-settings-row peach-settings-row--meta">1053 <span>Entries</span>1054 <span class="peach-settings-value">{props.entriesCount}</span>1055 </div>1056 <div class="peach-settings-row peach-settings-row--meta">1057 <span>Duplicates</span>1058 <Show when={dedupDone() !== null} fallback={1059 <button1060 class="peach-btn peach-btn--subtle peach-btn--sm"1061 onClick={() => void handleCountDuplicates()}1062 disabled={dedupRunning()}1063 >1064 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1065 </button>1066 }>1067 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1068 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1069 </span>1070 </Show>1071 </div>1072 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1073 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1074 <div class="peach-restore-warning">1075 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1076 </div>1077 <div style="display:flex;gap:6px">1078 <button1079 class="peach-btn peach-btn--subtle peach-btn--sm"1080 onClick={() => setDedupCount(null)}1081 >1082 cancel1083 </button>1084 <button1085 class="peach-btn peach-btn--accent peach-btn--sm"1086 onClick={() => void handleDeduplicate()}1087 disabled={dedupRunning()}1088 >1089 {dedupRunning() ? "removing..." : "remove duplicates"}1090 </button>1091 </div>1092 </div>1093 </Show>10941095 <p class="peach-settings-sub-label">MAINTENANCE</p>1096 <div class="peach-settings-row peach-settings-row--meta">1097 <span>Trash auto-delete</span>1098 <select1099 class="peach-settings-select"1100 value={String(props.trashRetentionDays)}1101 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1102 >1103 <option value="7">7 days</option>1104 <option value="30">30 days</option>1105 <option value="90">90 days</option>1106 <option value="0">Never</option>1107 </select>1108 </div>1109 <div class="peach-settings-row peach-settings-row--meta">1110 <span>Vault Health</span>1111 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1112 open1113 </button>1114 </div>1115 <Show when={declinedCardCount() > 0}>1116 <div class="peach-settings-row peach-settings-row--meta">1117 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1118 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1119 Clear1120 </button>1121 </div>1122 </Show>11231124 <p class="peach-settings-sub-label">DATA</p>1125 <div class="peach-settings-row peach-settings-row--meta">1126 <span>Import passwords</span>1127 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1128 open1129 </button>1130 </div>1131 <div class="peach-settings-row peach-settings-row--meta">1132 <span>Export backup</span>1133 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1134 open1135 </button>1136 </div>11371138 <p class="peach-settings-sub-label">VAULT HISTORY</p>1139 <Show when={historyLoading()}>1140 <p class="peach-settings-hint">Loading…</p>1141 </Show>1142 <Show when={!historyLoading() && historySnapshots().length === 0}>1143 <p class="peach-settings-hint">1144 No snapshots yet. Peach saves automatically before every change —1145 saves, syncs, and imports.1146 </p>1147 </Show>1148 <Show when={!historyLoading() && historySnapshots().length > 0}>1149 <p class="peach-settings-hint">1150 Restoring locks your vault. Unlock with the password that1151 encrypted that snapshot to confirm.1152 </p>1153 <div class="peach-history-list">1154 <For each={historySnapshots()}>1155 {(snap) => (1156 <div class="peach-history-row">1157 <div class="peach-history-meta">1158 <span class="peach-history-time">1159 {formatSnapshotTime(snap.savedAt)}1160 </span>1161 <span class="peach-history-sub">1162 {[1163 snap.entryCount !== null1164 ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1165 : null,1166 reasonLabel(snap.reason),1167 ]1168 .filter(Boolean)1169 .join(" · ")}1170 </span>1171 </div>1172 <button1173 class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1174 historyRestoring() === snap.id ? " is-busy" : ""1175 }`}1176 onClick={() => void handleRestoreVaultSnapshot(snap.id)}1177 disabled={historyRestoring() !== null}1178 >1179 <Show when={historyRestoring() === snap.id} fallback="Restore">1180 …1181 </Show>1182 </button>1183 </div>1184 )}1185 </For>1186 </div>1187 <p class="peach-history-footer">1188 {historySnapshots().length} of 10 snapshots1189 </p>1190 </Show>11911192 <button1193 class="peach-settings-advanced-toggle"1194 onClick={() => setShowVaultAdvanced((v) => !v)}1195 aria-expanded={showVaultAdvanced()}1196 >1197 <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1198 <span class="peach-settings-advanced-chevron">1199 {showVaultAdvanced() ? "▲" : "▼"}1200 </span>1201 </button>12021203 <Show when={showVaultAdvanced()}>1204 <div class="peach-danger-card">1205 <div class="peach-danger-card-head">1206 <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1207 <h3 class="peach-danger-card-title">Replace vault from backup</h3>1208 </div>1209 <p class="peach-danger-card-copy">1210 Wipes the current vault and restores from a backup file.1211 Recoverable only if your backup is good.1212 </p>1213 <button1214 class="peach-danger-card-button"1215 onClick={() => props.onOpenFileImport?.()}1216 >1217 Choose backup file1218 </button>1219 </div>12201221 <Show when={props.onOpenPanic}>1222 <div class="peach-danger-card peach-danger-card--hazard">1223 <div class="peach-danger-card-head">1224 <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1225 <h3 class="peach-danger-card-title">Destroy vault</h3>1226 </div>1227 <p class="peach-danger-card-copy">1228 Permanently wipes every entry, setting, and license on this1229 device. There is no recovery.1230 </p>1231 <button1232 class="peach-danger-card-button"1233 onClick={() => props.onOpenPanic?.()}1234 >1235 Begin destruction1236 </button>1237 </div>1238 </Show>1239 </Show>1240 </div>1241 </div>1242 );1243 }12441245 function renderSync() {1246 if (!isPro()) {1247 return (1248 <div class="peach-settings-body">1249 <div class="peach-pro-upgrade">1250 <div class="peach-pro-upgrade-hero">1251 <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1252 <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1253 <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1254 <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"/>1255 <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"/>1256 <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1257 <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1258 <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1259 </svg>1260 <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1261 <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1262 </div>12631264 <div class="peach-pro-features">1265 <div class="peach-pro-feature">1266 <span class="peach-pro-feature-mark">✦</span>1267 <div class="peach-pro-feature-text">1268 <span class="peach-pro-feature-name">Always in sync</span>1269 <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1270 </div>1271 </div>1272 <div class="peach-pro-feature">1273 <span class="peach-pro-feature-mark">✦</span>1274 <div class="peach-pro-feature-text">1275 <span class="peach-pro-feature-name">No cloud. No relay.</span>1276 <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1277 </div>1278 </div>1279 <div class="peach-pro-feature">1280 <span class="peach-pro-feature-mark">✦</span>1281 <div class="peach-pro-feature-text">1282 <span class="peach-pro-feature-name">Tailscale supported</span>1283 <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1284 </div>1285 </div>1286 <div class="peach-pro-feature">1287 <span class="peach-pro-feature-mark">✦</span>1288 <div class="peach-pro-feature-text">1289 <span class="peach-pro-feature-name">Zero friction after setup</span>1290 <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1291 </div>1292 </div>1293 </div>12941295 <button1296 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1297 onClick={() => setActiveSection("license")}1298 >1299 Upgrade to Pro1300 </button>1301 <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1302 Already have a key? Enter it →1303 </button>1304 </div>1305 </div>1306 );1307 }13081309 return (1310 <div class="peach-settings-body">1311 <div class="peach-settings-group">1312 <div class="peach-settings-row">1313 <span>Status</span>1314 <span class="peach-settings-value peach-settings-value--status">1315 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1316 {getStatusLabel(syncStatus())}1317 </span>1318 </div>1319 <div class="peach-settings-row peach-settings-row--meta">1320 <span>Last sync</span>1321 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1322 </div>1323 <div class="peach-settings-row peach-settings-row--meta">1324 <button1325 class="peach-btn peach-btn--subtle peach-btn--sm"1326 onClick={() => void handleSyncNow()}1327 disabled={syncStatus() === "not_trusted" || syncingNow()}1328 >1329 <Switch>1330 <Match when={syncingNow()}>Syncing…</Match>1331 <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1332 <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1333 <Match when={true}>Sync now</Match>1334 </Switch>1335 </button>1336 </div>1337 <p class="peach-settings-hint">1338 If you edit on multiple devices before syncing, the most recently1339 changed device wins. Use Vault History above to recover older1340 versions if needed.1341 </p>13421343 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1344 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1345 <Show when={pairedDevices().length === 0}>1346 <p class="peach-settings-hint">1347 No paired devices yet. Add a device to start syncing.1348 </p>1349 </Show>1350 <Show when={pairedDevices().length > 0}>1351 <div class="peach-history-list">1352 <For each={pairedDevices()}>1353 {(device) => (1354 <div class="peach-history-row">1355 <div class="peach-history-meta">1356 <span class="peach-history-time">{device.displayName}</span>1357 <span class="peach-history-sub">1358 {device.lastSyncAt1359 ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1360 : "Never synced"}1361 </span>1362 </div>1363 <button1364 class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1365 onClick={() => void handleRemoveDevice(device.deviceId)}1366 >1367 Remove1368 </button>1369 </div>1370 )}1371 </For>1372 </div>1373 </Show>1374 <div class="peach-settings-actions--row">1375 <button1376 class="peach-btn peach-btn--subtle peach-btn--sm"1377 onClick={() => props.onOpenSyncPairing?.()}1378 >1379 Add a device1380 </button>1381 </div>1382 </div>13831384 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1385 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1386 <For each={trustedNetworks()}>1387 {(network) => (1388 <div class="peach-sync-device-row">1389 <span class="peach-sync-device-name">1390 {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1391 </span>1392 <Show1393 when={!network.isTailscale}1394 fallback={<span class="peach-sync-device-meta">Always trusted</span>}1395 >1396 <button1397 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1398 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1399 >1400 Remove1401 </button>1402 </Show>1403 </div>1404 )}1405 </For>1406 <div class="peach-settings-actions--row">1407 <Show when={!isCurrentNetworkTrusted()}>1408 <button1409 class="peach-btn peach-btn--subtle peach-btn--sm"1410 onClick={() => void handleMarkNetworkTrusted()}1411 disabled={markingNetwork()}1412 >1413 {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1414 </button>1415 </Show>1416 </div>1417 </div>14181419 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1420 <span class="peach-sync-section-label">Advanced</span>1421 <Show1422 when={resetIdentityConfirm()}1423 fallback={1424 <div class="peach-sync-advanced-row">1425 <div class="peach-sync-advanced-info">1426 <span class="peach-sync-advanced-name">Reset sync identity</span>1427 <span class="peach-sync-device-meta">1428 Generates a new keypair and unpairs every device.1429 </span>1430 </div>1431 <button1432 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1433 onClick={() => setResetIdentityConfirm(true)}1434 >1435 Reset1436 </button>1437 </div>1438 }1439 >1440 <div class="peach-sync-reset-confirm">1441 <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1442 <p class="peach-sync-reset-confirm-body">1443 This will unpair all devices. You will need to pair them again from scratch.1444 </p>1445 <div class="peach-settings-actions--row">1446 <button1447 class="peach-btn peach-btn--ghost peach-btn--sm"1448 onClick={() => setResetIdentityConfirm(false)}1449 disabled={resetIdentityRunning()}1450 >1451 Cancel1452 </button>1453 <button1454 class="peach-btn peach-btn--danger peach-btn--sm"1455 onClick={() => void handleResetSyncIdentity()}1456 disabled={resetIdentityRunning()}1457 >1458 {resetIdentityRunning() ? "Resetting..." : "Reset"}1459 </button>1460 </div>1461 </div>1462 </Show>1463 </div>1464 </div>1465 </div>1466 );1467 }14681469 function renderAppearance() {1470 const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1471 { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1472 { id: "cormorant", label: "Cormorant", preview: "Aa" },1473 { id: "inter", label: "Inter", preview: "Aa" },1474 { id: "system", label: "System", preview: "Aa" },1475 ];1476 const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1477 { id: "small", label: "Small" },1478 { id: "medium", label: "Default" },1479 { id: "large", label: "Large" },1480 ];1481 return (1482 <div class="peach-settings-body">1483 <div class="peach-settings-group">1484 <p class="peach-settings-sub-label">THEME</p>1485 <div class="peach-settings-row peach-settings-row--meta">1486 <span>Theme</span>1487 <button1488 class="peach-btn peach-btn--subtle peach-btn--sm"1489 onClick={() =>1490 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1491 }1492 >1493 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1494 </button>1495 </div>14961497 <p class="peach-settings-sub-label">ACCENT</p>1498 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1499 <span>Accent</span>1500 <div class="peach-accent-picker">1501 <For each={ACCENT_COLORS}>1502 {(color) => (1503 <button1504 class="peach-accent-swatch"1505 classList={{ "is-active": props.accentColor === color.name }}1506 style={{ background: color.hex }}1507 onClick={() => void props.onAccentChange(color.name)}1508 aria-label={color.name}1509 title={color.name}1510 />1511 )}1512 </For>1513 </div>1514 </div>15151516 <p class="peach-settings-sub-label">DISPLAY FONT</p>1517 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1518 <span>Font</span>1519 <div class="peach-font-picker">1520 <For each={fontOptions}>1521 {(opt) => (1522 <button1523 class="peach-font-swatch"1524 classList={{ "is-active": props.displayFont === opt.id }}1525 data-font-preview={opt.id}1526 onClick={() => void props.onDisplayFontChange(opt.id)}1527 aria-label={opt.label}1528 title={opt.label}1529 >1530 <span class="peach-font-swatch-glyph">{opt.preview}</span>1531 <span class="peach-font-swatch-name">{opt.label}</span>1532 </button>1533 )}1534 </For>1535 </div>1536 </div>15371538 <p class="peach-settings-sub-label">TEXT SIZE</p>1539 <div class="peach-settings-row peach-settings-row--meta">1540 <span>Size</span>1541 <div class="peach-pill-group">1542 <For each={sizeOptions}>1543 {(opt) => (1544 <button1545 class="peach-btn peach-btn--ghost peach-btn--sm"1546 classList={{ "is-active": props.textSize === opt.id }}1547 onClick={() => void props.onTextSizeChange(opt.id)}1548 >1549 {opt.label}1550 </button>1551 )}1552 </For>1553 </div>1554 </div>15551556 <p class="peach-settings-sub-label">MOTION</p>1557 <div class="peach-settings-row peach-settings-row--meta">1558 <span>Reduce motion</span>1559 <button1560 class="peach-btn peach-btn--subtle peach-btn--sm"1561 onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1562 >1563 {props.reduceMotion ? "On" : "Off"}1564 </button>1565 </div>1566 </div>1567 </div>1568 );1569 }15701571 function renderShortcuts() {1572 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1573 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15741575 function startCapture(action: ShortcutAction) {1576 setConflictMsg(null);1577 setCapturingAction(action);1578 }1579 function cancelCapture() {1580 setCapturingAction(null);1581 setConflictMsg(null);1582 }1583 function commitBinding(action: ShortcutAction, binding: Keybinding) {1584 const conflict = findConflict(props.shortcuts, binding, action);1585 if (conflict) {1586 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1587 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1588 return;1589 }1590 const next = { ...props.shortcuts, [action]: binding };1591 setCapturingAction(null);1592 setConflictMsg(null);1593 void props.onShortcutsChange(next);1594 }1595 function resetAction(action: ShortcutAction) {1596 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1597 void props.onShortcutsChange(next);1598 }1599 function resetAll() {1600 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1601 }16021603 1604 1605 function handleCaptureKey(e: KeyboardEvent) {1606 e.preventDefault();1607 e.stopPropagation();1608 if (e.key === "Escape") { cancelCapture(); return; }1609 const binding = keybindingFromEvent(e);1610 if (!binding) return; 1611 const action = capturingAction();1612 if (action) commitBinding(action, binding);1613 }16141615 return (1616 <div class="peach-settings-body">1617 <div class="peach-settings-group peach-shortcuts-group">1618 <p class="peach-settings-sub-label">ENTRY VIEW</p>1619 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1620 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1621 </For>1622 </div>16231624 <div class="peach-settings-group peach-shortcuts-group">1625 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1626 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1627 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1628 </For>1629 </div>16301631 <Show when={conflictMsg()}>1632 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1633 </Show>16341635 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1636 <div class="peach-settings-row peach-settings-row--meta">1637 <span>Inline autofill on page</span>1638 <a1639 class="peach-settings-link"1640 href="chrome://extensions/shortcuts"1641 onClick={(e) => {1642 e.preventDefault();1643 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1644 }}1645 >1646 ⌘⇧P — customize ↗1647 </a>1648 </div>1649 <button1650 class="peach-btn peach-btn--subtle peach-btn--sm"1651 style="align-self:flex-start"1652 onClick={resetAll}1653 >1654 Reset all to defaults1655 </button>1656 </div>1657 </div>1658 );16591660 function renderRow(1661 desc: typeof SHORTCUT_DESCRIPTORS[number],1662 capturing: () => ShortcutAction | null,1663 onStart: (a: ShortcutAction) => void,1664 onCancel: () => void,1665 onReset: (a: ShortcutAction) => void,1666 onKey: (e: KeyboardEvent) => void,1667 ) {1668 const isActive = () => capturing() === desc.action;1669 const binding = () => props.shortcuts[desc.action];1670 const isCustom = () => {1671 const b = binding();1672 const d = DEFAULT_BINDINGS[desc.action];1673 return b.key !== d.key1674 || Boolean(b.ctrl) !== Boolean(d.ctrl)1675 || Boolean(b.meta) !== Boolean(d.meta)1676 || Boolean(b.shift) !== Boolean(d.shift)1677 || Boolean(b.alt) !== Boolean(d.alt);1678 };1679 return (1680 <div class="peach-shortcut-row">1681 <span class="peach-shortcut-row-label">{desc.label}</span>1682 <button1683 class="peach-shortcut-chip"1684 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1685 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1686 onKeyDown={(e) => { if (isActive()) onKey(e); }}1687 ref={(el) => {1688 1689 if (el) {1690 queueMicrotask(() => { if (isActive()) el.focus(); });1691 }1692 }}1693 >1694 {isActive() ? "Press a key…" : formatKeybinding(binding())}1695 </button>1696 <Show when={isCustom() && !isActive()}>1697 <button1698 class="peach-shortcut-reset"1699 title="Reset to default"1700 onClick={() => onReset(desc.action)}1701 >1702 ↺1703 </button>1704 </Show>1705 </div>1706 );1707 }1708 }17091710 const PRO_FEATURES: Array<{1711 feature: ProFeature;1712 name: string;1713 desc: string;1714 }> = [1715 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1716 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1717 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1718 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1719 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1720 ];17211722 const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17231724 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1725 const [deactivating, setDeactivating] = createSignal(false);17261727 async function handleDeactivateLicense() {1728 setDeactivating(true);1729 await sendMessage({ type: "DEACTIVATE_LICENSE" });1730 setLicenseInfo(null);1731 await props.onLicenseChange?.();1732 setDeactivating(false);1733 }17341735 async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1736 setCanaryTestingId(entry.id);1737 setCanaryTestResult(null);1738 const res = await sendMessage({1739 type: "FIRE_CANARY_PING",1740 payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1741 }) as { ok: boolean };1742 setCanaryTestResult({ id: entry.id, ok: res.ok });1743 setCanaryTestingId(null);1744 setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1745 }17461747 function renderLicense() {1748 async function onActivate() {1749 await handleActivateLicense();1750 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1751 await props.onLicenseChange?.();1752 }1753 }17541755 return (1756 <div class="peach-settings-body">1757 <Show1758 when={isPro()}1759 fallback={1760 <Show1761 when={showKeyEntry()}1762 fallback={1763 <div class="peach-pro-upgrade">1764 <div class="peach-pro-upgrade-hero">1765 <div class="peach-pro-upgrade-mark-wrap">1766 <PeachMark class="peach-pro-upgrade-mark" />1767 </div>1768 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1769 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1770 </div>17711772 <div class="peach-pro-features">1773 <For each={PRO_FEATURES}>1774 {(item) => (1775 <div1776 class="peach-pro-feature"1777 classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1778 >1779 <span class="peach-pro-feature-mark">✦</span>1780 <div class="peach-pro-feature-text">1781 <span class="peach-pro-feature-name">{item.name}</span>1782 <span class="peach-pro-feature-desc">{item.desc}</span>1783 </div>1784 </div>1785 )}1786 </For>1787 </div>17881789 <div class="peach-pro-upgrade-actions">1790 <p class="peach-pro-pricing">$39 · once · yours forever</p>1791 <a1792 href="https://peachpasswords.com/#pricing"1793 target="_blank"1794 rel="noopener noreferrer"1795 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1796 >1797 Get Peach Pro1798 </a>1799 <div class="peach-pro-key-divider"><span>or</span></div>1800 <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1801 Activate a license key1802 </button>1803 </div>1804 </div>1805 }1806 >1807 <div class="peach-pro-key-ceremony">1808 <LockRuneCanvas />1809 <p class="peach-pro-key-ceremony-label">Enter your license key</p>1810 <input1811 class="peach-underline-input peach-pro-key-input"1812 type="text"1813 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1814 value={licenseKeyInput()}1815 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1816 onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1817 autocomplete="off"1818 spellcheck={false}1819 />1820 <Show when={licenseError()}>1821 <p class="peach-pro-key-error">That key doesn't look right.</p>1822 </Show>1823 <button1824 class="peach-btn peach-btn--accent peach-pro-key-activate"1825 onClick={() => void onActivate()}1826 disabled={activatingLicense() || !licenseKeyInput().trim()}1827 >1828 {activatingLicense() ? "activating…" : "Activate"}1829 </button>1830 <button1831 class="peach-pro-key-cancel"1832 onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1833 >1834 cancel1835 </button>1836 </div>1837 </Show>1838 }1839 >1840 <div class="peach-pro-active">1841 <div class="peach-pro-active-hero">1842 <div class="peach-pro-active-mark-wrap">1843 <PeachMark class="peach-pro-active-mark" />1844 </div>1845 <h2 class="peach-pro-active-title">Peach Pro</h2>1846 <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1847 </div>18481849 <Show when={licenseKey()}>1850 <div class="peach-settings-license">1851 <span class="peach-settings-label">LICENSE KEY</span>1852 <div class="peach-settings-license-row">1853 <span class="peach-settings-license-key">{licenseKey()!}</span>1854 <button1855 class="peach-settings-license-copy"1856 onClick={() => copyLicenseKey(licenseKey()!)}1857 >1858 {copied() ? "✓" : "copy"}1859 </button>1860 </div>1861 </div>1862 </Show>18631864 <div class="peach-pro-active-features">1865 <For each={PRO_FEATURES}>1866 {(item) => (1867 <div class="peach-pro-active-feature">1868 <span class="peach-pro-active-feature-mark">✦</span>1869 <span class="peach-pro-active-feature-name">{item.name}</span>1870 </div>1871 )}1872 </For>1873 </div>18741875 <button1876 class="peach-pro-deactivate"1877 onClick={() => void handleDeactivateLicense()}1878 disabled={deactivating()}1879 >1880 {deactivating() ? "Deactivating…" : "Deactivate"}1881 </button>1882 </div>1883 </Show>1884 </div>1885 );1886 }18871888 function renderFeedback() {1889 return (1890 <div class="peach-settings-body">1891 <div class="peach-settings-group peach-feedback-group">1892 <p class="peach-feedback-intro">1893 Help us improve Peach. Every report goes directly to our inbox.1894 </p>18951896 <p class="peach-settings-sub-label">TYPE</p>1897 <div class="peach-feedback-chips">1898 <For each={FEEDBACK_CATEGORIES}>1899 {(cat) => (1900 <button1901 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1902 onClick={() => setFbCategory(cat)}1903 >1904 {cat}1905 </button>1906 )}1907 </For>1908 </div>19091910 <p class="peach-settings-sub-label">MESSAGE</p>1911 <textarea1912 class="peach-feedback-textarea"1913 placeholder="Describe the issue or idea..."1914 value={fbMessage()}1915 onInput={(e) => setFbMessage(e.currentTarget.value)}1916 />19171918 <p class="peach-feedback-meta">1919 Version {version} · will be included automatically1920 </p>19211922 <Show when={fbResult() === "success"}>1923 <p class="peach-feedback-result peach-feedback-result--ok">1924 Sent — thank you1925 </p>1926 </Show>1927 <Show when={fbResult() === "error"}>1928 <p class="peach-feedback-result peach-feedback-result--err">1929 Failed to send — try again1930 </p>1931 </Show>19321933 <button1934 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1935 onClick={() => void handleSubmitFeedback()}1936 disabled={fbSubmitting() || !fbMessage().trim()}1937 >1938 {fbSubmitting() ? "Sending..." : "Send feedback"}1939 </button>19401941 <p class="peach-feedback-anon">1942 Sent anonymously · no account required1943 </p>1944 </div>1945 </div>1946 );1947 }19481949 function renderMobile() {1950 return (1951 <div class="peach-settings-body">1952 <div class="peach-settings-group">1953 <p class="peach-settings-label">Autofill service</p>1954 <div class="peach-mobile-row">1955 <span class="peach-mobile-status">1956 {autofillEnabled() ? "Enabled" : "Disabled"}1957 </span>1958 <button1959 class="peach-btn peach-btn--subtle peach-btn--sm"1960 onClick={() => void AutofillPlugin?.openAutofillSettings()}1961 >1962 {autofillEnabled() ? "Change" : "Enable"}1963 </button>1964 </div>1965 </div>19661967 <div class="peach-settings-group">1968 <p class="peach-settings-label">Biometric unlock</p>1969 <div class="peach-mobile-row">1970 <span class="peach-mobile-status">1971 {biometricEnabled() ? "Available" : "Not available"}1972 </span>1973 </div>1974 </div>1975 </div>1976 );1977 }19781979 return (1980 <div class="peach-overlay-view ov-enter-right" style="display:flex">1981 <div class="peach-overlay-header">1982 <button1983 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1984 onClick={handleHeaderBack}1985 aria-label="Back"1986 >1987 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1988 <path d="M6 1L1 6L6 11" />1989 </svg>1990 </button>1991 <span class="peach-overlay-title">{currentTitle()}</span>1992 <div class="peach-overlay-spacer"></div>1993 </div>19941995 <Switch fallback={renderHome()}>1996 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1997 <Match when={activeSection() === "vault"}>{renderVault()}</Match>1998 <Match when={activeSection() === "sync"}>{renderSync()}</Match>1999 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2000 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2001 <Match when={activeSection() === "license"}>{renderLicense()}</Match>2002 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2003 <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>2004 </Switch>2005 </div>2006 );2007}