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 isMobile = Capacitor.isNativePlatform();236 const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);237 const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);238239 const [fbCategory, setFbCategory] = createSignal("Bug report");240 const [fbMessage, setFbMessage] = createSignal("");241 const [fbSubmitting, setFbSubmitting] = createSignal(false);242 const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);243244 onMount(async () => {245 const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([246 sendMessage({ type: "GET_LICENSE" }),247 sendMessage({ type: "GET_SYNC_STATE" }),248 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),249 sendMessage({ type: "IS_DURESS_ENABLED" }),250 sendMessage({ type: "GET_SETTINGS" }),251 sendMessage({ type: "GET_LICENSE_KEY" }),252 ]);253254 255 256 void loadVaultHistory();257258 if ((licenseRes as { ok: boolean }).ok) {259 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);260 }261262 if ((syncRes as { ok: boolean }).ok) {263 const data = syncRes as { ok: boolean; data?: {264 pairedDevices?: PairedDevice[];265 trustedNetworks?: TrustedNetwork[];266 status?: SyncStatus;267 lastSyncAt?: number;268 isPro?: boolean;269 }};270 if (data.data) {271 setPairedDevices(data.data.pairedDevices ?? []);272 setTrustedNetworks(data.data.trustedNetworks ?? []);273 setSyncStatus(data.data.status ?? "offline");274 setLastSyncAt(data.data.lastSyncAt);275 setSyncProStatus(data.data.isPro ?? false);276 }277 }278279 if ((passkeyRes as { ok: boolean }).ok) {280 setPasskeyState(281 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null282 );283 }284285 if ((duressRes as { ok: boolean }).ok) {286 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);287 }288289 if ((settingsRes as { ok: boolean }).ok) {290 const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;291 setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);292 }293294 if ((keyRes as { ok: boolean }).ok) {295 setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);296 }297298 const entriesRes = await sendMessage({ type: "GET_ENTRIES" });299 if ((entriesRes as { ok: boolean }).ok) {300 const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];301 setCanaryEntries(302 entries303 .filter((e) => e.canary?.webhookUrl)304 .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))305 );306 }307308 309 if (isAndroid) {310 if (AutofillPlugin) {311 const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();312 setAutofillEnabled(enabled);313 }314 const bio = await biometricAvailable();315 setBiometricEnabled(bio);316 }317318 chrome.runtime.onMessage.addListener((msg) => {319 if (msg.type === "SYNC_STATE_UPDATE") {320 setPairedDevices(msg.pairedDevices ?? []);321 setTrustedNetworks(msg.trustedNetworks ?? []);322 setSyncStatus(msg.status ?? "offline");323 setLastSyncAt(msg.lastSyncAt);324 setSyncProStatus(msg.isPro ?? false);325 }326 });327 });328329 async function handleChangePassword() {330 setChangePwError(null);331 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }332 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }333 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }334 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }335 setChangePwSaving(true);336 const res = await sendMessage({337 type: "CHANGE_PASSWORD",338 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },339 }) as { ok: boolean; error?: string };340 if (res.ok) {341 setChangePwSuccess(true);342 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");343 setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);344 } else {345 setChangePwError(res.error ?? "failed to change password");346 }347 setChangePwSaving(false);348 }349350 const [copied, setCopied] = createSignal(false);351352 async function copyLicenseKey(key: string) {353 await navigator.clipboard.writeText(key);354 setCopied(true);355 setTimeout(() => setCopied(false), 2000);356 }357358 async function handleActivateLicense() {359 const key = licenseKeyInput().trim();360 if (!key) return;361 setActivatingLicense(true);362 setLicenseError(null);363 setLicenseSuccess(null);364 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });365 setActivatingLicense(false);366 if ((res as { ok: boolean }).ok) {367 const data = (res as { ok: boolean; data?: LicenseInfo }).data;368 if (data?.valid) {369 setLicenseInfo(data);370 setLicenseKey(key);371 setLicenseSuccess("License activated!");372 setLicenseKeyInput("");373 } else {374 setLicenseError("Invalid license key");375 }376 } else {377 setLicenseError("Activation failed");378 }379 }380381 async function handleTogglePasskeys() {382 const current = passkeyState();383 if (!current || !current.supported) return;384 setPasskeySaving(true);385 const res = await sendMessage({386 type: "SET_PASSKEY_PROVIDER_ENABLED",387 payload: { enabled: !current.enabled },388 });389 setPasskeySaving(false);390 if ((res as { ok: boolean }).ok) {391 setPasskeyState(392 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null393 );394 }395 }396397 async function handleCheckAllBreaches() {398 setCheckingBreaches(true);399 setBreachResult(null);400 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });401 setCheckingBreaches(false);402 if ((res as { ok: boolean }).ok) {403 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};404 const breached = Object.values(data).filter((count) => count > 0).length;405 setBreachResult(406 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"407 );408 props.onBreachCheckDone();409 }410 }411412 async function handleSyncNow() {413 if (syncStatus() === "not_trusted") return;414 setSyncingNow(true);415 setSyncFeedback(null);416 const res = await sendMessage({ type: "TRIGGER_SYNC" });417 const ok = (res as { ok: boolean }).ok;418 if (ok) {419 const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;420 const count = data?.syncedCount ?? 0;421 setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });422 setLastSyncAt(Date.now());423 void refreshSyncState();424 } else {425 const error = (res as { error?: string }).error;426 const map: Record<string, string> = {427 no_paired_devices: "No paired devices",428 no_devices_reachable: "No devices found",429 vault_locked: "Vault is locked",430 pro_required: "Sync requires Pro",431 };432 setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });433 }434 setTimeout(() => {435 setSyncingNow(false);436 setSyncFeedback(null);437 }, 3500);438 }439440 async function refreshSyncState() {441 const res = await sendMessage({ type: "GET_SYNC_STATE" });442 const data = (res as { ok: boolean; data?: {443 pairedDevices?: PairedDevice[];444 trustedNetworks?: TrustedNetwork[];445 status?: SyncStatus;446 lastSyncAt?: number;447 }}).data;448 if (!data) return;449 setPairedDevices(data.pairedDevices ?? []);450 setTrustedNetworks(data.trustedNetworks ?? []);451 setSyncStatus(data.status ?? "offline");452 setLastSyncAt(data.lastSyncAt);453 }454455 async function handleRemoveDevice(deviceId: string) {456 setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));457 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });458 void refreshSyncState();459 }460461 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {462 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });463 void refreshSyncState();464 }465466 const [markingNetwork, setMarkingNetwork] = createSignal(false);467468 async function handleMarkNetworkTrusted() {469 if (markingNetwork()) return;470 setMarkingNetwork(true);471 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });472 await refreshSyncState();473 setMarkingNetwork(false);474 }475476 const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);477 const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);478479 async function handleResetSyncIdentity() {480 setResetIdentityRunning(true);481 if (Capacitor.isNativePlatform()) {482 const { clearAndroidSyncIdentity } = await import("../../shared/android-sync-identity");483 await clearAndroidSyncIdentity();484 }485 const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });486 setResetIdentityRunning(false);487 if ((res as { ok: boolean }).ok) {488 setPairedDevices([]);489 setResetIdentityConfirm(false);490 }491 }492493 function formatRelativeTime(timestamp?: number): string {494 if (!timestamp) return "Never";495 const diff = Date.now() - timestamp;496 const minutes = Math.floor(diff / 60000);497 const hours = Math.floor(diff / 3600000);498 const days = Math.floor(diff / 86400000);499 if (minutes < 1) return "Just now";500 if (minutes < 60) return `${minutes}m ago`;501 if (hours < 24) return `${hours}h ago`;502 return `${days}d ago`;503 }504505 function getStatusColor(status: SyncStatus): string {506 switch (status) {507 case "synced": return "var(--peach-sync-ok)";508 case "syncing": return "var(--peach-sync-busy)";509 case "no_peers": return "var(--peach-sync-off)";510 case "not_trusted": return "var(--peach-sync-err)";511 case "offline": return "var(--peach-sync-off)";512 default: return "var(--peach-sync-off)";513 }514 }515516 function getStatusLabel(status: SyncStatus): string {517 switch (status) {518 case "synced": return "Synced";519 case "syncing": return "Syncing";520 case "no_peers": return "No peers visible";521 case "not_trusted": return "Not on a trusted network";522 case "offline": return "Offline";523 default: return "Unknown";524 }525 }526527 function isCurrentNetworkTrusted(): boolean {528 return trustedNetworks().length > 0;529 }530531 async function handleSetupDuress() {532 const mainPw = duressMainPassword().trim();533 const duressPw = duressPassword().trim();534 const confirmPw = duressConfirm().trim();535536 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }537 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }538 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }539 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }540541 setDuressSaving(true);542 setDuressError(null);543544 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };545 if (!verifyRes.ok || !verifyRes.data) {546 setDuressError("incorrect main password");547 setDuressSaving(false);548 return;549 }550551 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };552 setDuressSaving(false);553 if (res.ok) {554 setDuressEnabled(true);555 setShowDuressSetup(false);556 setDuressPassword("");557 setDuressConfirm("");558 setDuressMainPassword("");559 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");560 setTimeout(() => setDuressSuccess(null), 5000);561 } else {562 setDuressError(res.error ?? "failed to set duress password");563 }564 }565566 async function handleClearDeclinedCards() {567 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });568 setDeclinedCardCount(0);569 }570571 async function handleCountDuplicates() {572 setDedupRunning(true);573 setDedupDone(null);574 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };575 setDedupRunning(false);576 if (res.ok) setDedupCount(res.data ?? 0);577 }578579 async function handleDeduplicate() {580 setDedupRunning(true);581 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };582 setDedupRunning(false);583 if (res.ok) {584 setDedupDone(res.data ?? 0);585 setDedupCount(0);586 }587 }588589 async function handleDisableDuress() {590 const mainPw = duressMainPassword().trim();591 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }592593 setDuressSaving(true);594 setDuressError(null);595596 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };597 if (!verifyRes.ok || !verifyRes.data) {598 setDuressError("incorrect main password");599 setDuressSaving(false);600 return;601 }602603 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };604 setDuressSaving(false);605 if (res.ok) {606 setDuressEnabled(false);607 setShowDuressSetup(false);608 setDuressMainPassword("");609 setDuressSuccess("Duress password disabled.");610 setTimeout(() => setDuressSuccess(null), 4000);611 } else {612 setDuressError(res.error ?? "failed to disable duress password");613 }614 }615616 async function handleSubmitFeedback() {617 if (!fbMessage().trim() || fbSubmitting()) return;618 setFbSubmitting(true);619 setFbResult(null);620 const res = await sendMessage({621 type: "SUBMIT_FEEDBACK",622 payload: {623 category: fbCategory(),624 message: fbMessage().trim(),625 version,626 browser: navigator.userAgent,627 },628 }) as { ok: boolean; error?: string };629 setFbSubmitting(false);630 if (res.ok) {631 setFbResult("success");632 setFbMessage("");633 setTimeout(() => setFbResult(null), 4000);634 } else {635 setFbResult("error");636 }637 }638639 function handleHeaderBack() {640 if (activeSection() !== null) {641 setActiveSection(null);642 } else {643 props.onBack();644 }645 }646647 function currentTitle(): string {648 const s = activeSection();649 if (s === null) return "settings";650 if (s === "license") return "account";651 return s;652 }653654 function renderCatRow(opts: {655 section: SectionName;656 name: string;657 desc: string;658 icon: () => any;659 pro?: boolean;660 }) {661 return (662 <button663 class="peach-settings-cat-row"664 onClick={() => setActiveSection(opts.section)}665 >666 <div class="peach-settings-cat-icon">{opts.icon()}</div>667 <div class="peach-settings-cat-content">668 <span class="peach-settings-cat-name">669 {opts.name}670 <Show when={opts.pro && !isPro()}>671 <span class="peach-pro-mark" aria-label="Pro feature">✦</span>672 </Show>673 </span>674 <span class="peach-settings-cat-desc">{opts.desc}</span>675 </div>676 <svg677 class="peach-settings-cat-chevron"678 width="6" height="10" viewBox="0 0 6 10" fill="none"679 stroke="currentColor" stroke-width="1.1"680 stroke-linecap="round" stroke-linejoin="round"681 >682 <path d="M1 1l4 4-4 4" />683 </svg>684 </button>685 );686 }687688 const iconShield = () => (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="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>691 </svg>692 );693 const iconKey = () => (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 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>696 </svg>697 );698 const iconLock = () => (699 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">700 <rect x="3" y="7" width="10" height="7" rx="1.5"/>701 <path d="M6 7V5a3 3 0 016 0v2"/>702 </svg>703 );704 const iconSync = () => (705 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">706 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>707 <path d="M14 6l-2 2-2-2"/>708 <path d="M2 10l2-2 2 2"/>709 </svg>710 );711 const iconSun = () => (712 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">713 <circle cx="8" cy="8" r="3"/>714 <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"/>715 </svg>716 );717 const iconKeys = () => (718 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">719 <rect x="2" y="5" width="5" height="4" rx="1"/>720 <rect x="9" y="7" width="5" height="4" rx="1"/>721 <path d="M7 7h2m-2 2h2"/>722 </svg>723 );724 const iconBubble = () => (725 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">726 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>727 </svg>728 );729730 function renderHome() {731 return (732 <div class="peach-settings-body">733 <p class="peach-settings-nav-label">PROTECTION</p>734 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}735 {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}736737 <p class="peach-settings-nav-label">VAULT</p>738 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}739 {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}740741 <p class="peach-settings-nav-label">PREFERENCES</p>742 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}743 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}744 <Show when={isAndroid}>745 <p class="peach-settings-nav-label">MOBILE</p>746 {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}747 </Show>748749 <p class="peach-settings-nav-label">SUPPORT</p>750 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}751752 <div class="peach-settings-footer">753 <span>Peach {version}</span>754 <span class="peach-divider"></span>755 <span>Browser extension</span>756 </div>757 </div>758 );759 }760761 function renderSecurity() {762 return (763 <div class="peach-settings-body">764 <div class="peach-settings-group">765 <p class="peach-settings-sub-label">LOCKING</p>766 <div class="peach-settings-row">767 <span>Lock on close</span>768 <button769 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}770 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}771 aria-label="Toggle lock on close"772 >773 <span class="peach-toggle-knob"></span>774 </button>775 </div>776 <div class="peach-settings-row">777 <span>Auto-lock after</span>778 <select779 class="peach-settings-select"780 value={String(props.autoLockMinutes)}781 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}782 >783 <option value="1">1 minute</option>784 <option value="5">5 minutes</option>785 <option value="15">15 minutes</option>786 <option value="30">30 minutes</option>787 <option value="0">Never</option>788 </select>789 </div>790791 <p class="peach-settings-sub-label">BREACH PROTECTION</p>792 <div class="peach-settings-row">793 <span>Breach monitoring</span>794 <button795 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}796 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}797 aria-label="Toggle breach monitoring"798 >799 <span class="peach-toggle-knob"></span>800 </button>801 </div>802 <div class="peach-settings-row peach-settings-row--meta">803 <span>Check all passwords</span>804 <button805 class="peach-btn peach-btn--subtle peach-btn--sm"806 onClick={() => void handleCheckAllBreaches()}807 disabled={checkingBreaches()}808 >809 {checkingBreaches() ? "checking..." : "run"}810 </button>811 </div>812 <Show when={breachResult()}>813 <div class="peach-settings-row peach-settings-row--meta">814 <span>Last check</span>815 <span class="peach-settings-value">{breachResult()}</span>816 </div>817 </Show>818819 <p class="peach-settings-sub-label">MASTER PASSWORD</p>820 <div class="peach-settings-row peach-settings-row--meta">821 <span>Master password</span>822 <button823 class="peach-btn peach-btn--subtle peach-btn--sm"824 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}825 >826 {showChangePw() ? "cancel" : "change"}827 </button>828 </div>829 <Show when={showChangePw()}>830 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">831 <input832 class="peach-settings-input"833 type="password"834 placeholder="current password"835 value={changePwCurrent()}836 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}837 autocomplete="current-password"838 />839 <input840 class="peach-settings-input"841 type="password"842 placeholder="new password (min 12 chars)"843 value={changePwNew()}844 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}845 autocomplete="new-password"846 />847 <input848 class="peach-settings-input"849 type="password"850 placeholder="confirm new password"851 value={changePwConfirm()}852 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}853 autocomplete="new-password"854 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}855 />856 <Show when={changePwError()}>857 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>858 </Show>859 <Show when={changePwSuccess()}>860 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>861 </Show>862 <button863 class="peach-btn peach-btn--accent peach-btn--sm"864 style="align-self:flex-start"865 onClick={() => void handleChangePassword()}866 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}867 >868 {changePwSaving() ? "saving…" : "save new password"}869 </button>870 </div>871 </Show>872873 <p class="peach-settings-sub-label">DURESS PASSWORD</p>874 <div class="peach-settings-row peach-settings-row--top">875 <div class="peach-settings-row-main">876 <span>Duress password</span>877 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">878 If entered under coercion, shows a decoy empty vault instead of your real one.879 </span>880 </div>881 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>882 {duressEnabled() ? "on" : "off"}883 </div>884 </div>885886 <Show when={duressEnabled() && !showDuressSetup()}>887 <div class="peach-settings-row peach-settings-row--meta">888 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">889 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.890 </span>891 </div>892 </Show>893894 <Show when={duressSuccess()}>895 <div class="peach-settings-row peach-settings-row--meta">896 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>897 </div>898 </Show>899900 <Show when={!showDuressSetup()}>901 <div class="peach-settings-row peach-settings-row--meta">902 <button903 class="peach-btn peach-btn--subtle peach-btn--sm"904 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}905 >906 {duressEnabled() ? "Change / disable" : "Set up"}907 </button>908 </div>909 </Show>910911 <Show when={showDuressSetup()}>912 <div class="peach-duress-form">913 <label class="peach-label">main password (to confirm)</label>914 <input915 class="peach-settings-input"916 type="password"917 placeholder="your current master password"918 autocomplete="current-password"919 value={duressMainPassword()}920 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}921 />922 <Show when={!duressEnabled()}>923 <label class="peach-label">new duress password</label>924 <input925 class="peach-settings-input"926 type="password"927 placeholder="duress password (min 8 chars)"928 autocomplete="new-password"929 value={duressPassword()}930 onInput={(e) => setDuressPassword(e.currentTarget.value)}931 />932 <label class="peach-label">confirm duress password</label>933 <input934 class="peach-settings-input"935 type="password"936 placeholder="confirm duress password"937 autocomplete="new-password"938 value={duressConfirm()}939 onInput={(e) => setDuressConfirm(e.currentTarget.value)}940 />941 </Show>942943 <Show when={duressError()}>944 <p class="peach-hint peach-hint-error">{duressError()}</p>945 </Show>946947 <div class="peach-duress-actions">948 <button949 class="peach-btn peach-btn--subtle peach-btn--sm"950 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}951 >952 cancel953 </button>954 <Show when={duressEnabled()}>955 <button956 class="peach-btn peach-btn--ghost peach-btn--sm"957 style="color:var(--peach-breach)"958 onClick={() => void handleDisableDuress()}959 disabled={duressSaving()}960 >961 {duressSaving() ? "disabling..." : "disable duress"}962 </button>963 </Show>964 <Show when={!duressEnabled()}>965 <button966 class="peach-btn peach-btn--accent peach-btn--sm"967 onClick={() => void handleSetupDuress()}968 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}969 >970 {duressSaving() ? "saving..." : "save"}971 </button>972 </Show>973 </div>974 </div>975 </Show>976977 <p class="peach-settings-sub-label">CANARY ENTRIES</p>978 <div class="peach-settings-row peach-settings-row--top">979 <div class="peach-settings-row-main">980 <span>Canary entries</span>981 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">982 Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.983 </span>984 </div>985 <span class="peach-settings-value" style="flex-shrink:0">986 {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}987 </span>988 </div>989 <Show when={canaryEntries().length > 0}>990 <For each={canaryEntries()}>991 {(entry) => (992 <div class="peach-settings-row peach-settings-row--meta">993 <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>994 <button995 class="peach-btn peach-btn--subtle peach-btn--sm"996 onClick={() => void handleTestCanary(entry)}997 disabled={canaryTestingId() === entry.id}998 style="flex-shrink:0"999 >1000 {canaryTestingId() === entry.id1001 ? "sending…"1002 : canaryTestResult()?.id === entry.id1003 ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")1004 : "test"}1005 </button>1006 </div>1007 )}1008 </For>1009 </Show>10101011 <p class="peach-settings-sub-label">PASSKEYS</p>1012 <div class="peach-settings-row peach-settings-row--top">1013 <div class="peach-settings-row-main">1014 <span>Intercept passkey requests</span>1015 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">1016 Peach can handle passkey sign-in and creation on supported Chromium browsers.1017 </span>1018 </div>1019 <button1020 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}1021 onClick={() => void handleTogglePasskeys()}1022 aria-label="Toggle passkey interception"1023 disabled={!passkeyState()?.supported || passkeySaving()}1024 >1025 <span class="peach-toggle-knob"></span>1026 </button>1027 </div>1028 <div class="peach-settings-row peach-settings-row--meta">1029 <span>Status</span>1030 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>1031 </div>1032 <div class="peach-settings-row peach-settings-row--meta">1033 <span>Browser</span>1034 <span class="peach-settings-value">1035 {passkeyState()?.browserLabel ?? "Unknown"}1036 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1037 </span>1038 </div>1039 <div class="peach-info-notice" style="margin-top:8px">1040 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1041 </div>1042 <Show when={passkeyState()?.conflict}>1043 <div class="peach-settings-row peach-settings-row--meta">1044 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1045 </div>1046 </Show>1047 </div>1048 </div>1049 );1050 }10511052 function renderVault() {1053 return (1054 <div class="peach-settings-body">1055 <div class="peach-settings-group">1056 <p class="peach-settings-sub-label">ENTRIES</p>1057 <div class="peach-settings-row peach-settings-row--meta">1058 <span>Entries</span>1059 <span class="peach-settings-value">{props.entriesCount}</span>1060 </div>1061 <div class="peach-settings-row peach-settings-row--meta">1062 <span>Duplicates</span>1063 <Show when={dedupDone() !== null} fallback={1064 <button1065 class="peach-btn peach-btn--subtle peach-btn--sm"1066 onClick={() => void handleCountDuplicates()}1067 disabled={dedupRunning()}1068 >1069 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1070 </button>1071 }>1072 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1073 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1074 </span>1075 </Show>1076 </div>1077 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1078 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1079 <div class="peach-restore-warning">1080 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1081 </div>1082 <div style="display:flex;gap:6px">1083 <button1084 class="peach-btn peach-btn--subtle peach-btn--sm"1085 onClick={() => setDedupCount(null)}1086 >1087 cancel1088 </button>1089 <button1090 class="peach-btn peach-btn--accent peach-btn--sm"1091 onClick={() => void handleDeduplicate()}1092 disabled={dedupRunning()}1093 >1094 {dedupRunning() ? "removing..." : "remove duplicates"}1095 </button>1096 </div>1097 </div>1098 </Show>10991100 <p class="peach-settings-sub-label">MAINTENANCE</p>1101 <div class="peach-settings-row peach-settings-row--meta">1102 <span>Trash auto-delete</span>1103 <select1104 class="peach-settings-select"1105 value={String(props.trashRetentionDays)}1106 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1107 >1108 <option value="7">7 days</option>1109 <option value="30">30 days</option>1110 <option value="90">90 days</option>1111 <option value="0">Never</option>1112 </select>1113 </div>1114 <div class="peach-settings-row peach-settings-row--meta">1115 <span>Vault Health</span>1116 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1117 open1118 </button>1119 </div>1120 <Show when={declinedCardCount() > 0}>1121 <div class="peach-settings-row peach-settings-row--meta">1122 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1123 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1124 Clear1125 </button>1126 </div>1127 </Show>11281129 <p class="peach-settings-sub-label">DATA</p>1130 <div class="peach-settings-row peach-settings-row--meta">1131 <span>Import passwords</span>1132 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1133 open1134 </button>1135 </div>1136 <div class="peach-settings-row peach-settings-row--meta">1137 <span>Export backup</span>1138 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1139 open1140 </button>1141 </div>11421143 <p class="peach-settings-sub-label">VAULT HISTORY</p>1144 <Show when={historyLoading()}>1145 <p class="peach-settings-hint">Loading…</p>1146 </Show>1147 <Show when={!historyLoading() && historySnapshots().length === 0}>1148 <p class="peach-settings-hint">1149 No snapshots yet. Peach saves automatically before every change —1150 saves, syncs, and imports.1151 </p>1152 </Show>1153 <Show when={!historyLoading() && historySnapshots().length > 0}>1154 <p class="peach-settings-hint">1155 Restoring locks your vault. Unlock with the password that1156 encrypted that snapshot to confirm.1157 </p>1158 <div class="peach-history-list">1159 <For each={historySnapshots()}>1160 {(snap) => (1161 <div class="peach-history-row">1162 <div class="peach-history-meta">1163 <span class="peach-history-time">1164 {formatSnapshotTime(snap.savedAt)}1165 </span>1166 <span class="peach-history-sub">1167 {[1168 snap.entryCount !== null1169 ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1170 : null,1171 reasonLabel(snap.reason),1172 ]1173 .filter(Boolean)1174 .join(" · ")}1175 </span>1176 </div>1177 <button1178 class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1179 historyRestoring() === snap.id ? " is-busy" : ""1180 }`}1181 onClick={() => void handleRestoreVaultSnapshot(snap.id)}1182 disabled={historyRestoring() !== null}1183 >1184 <Show when={historyRestoring() === snap.id} fallback="Restore">1185 …1186 </Show>1187 </button>1188 </div>1189 )}1190 </For>1191 </div>1192 <p class="peach-history-footer">1193 {historySnapshots().length} of 10 snapshots1194 </p>1195 </Show>11961197 <button1198 class="peach-settings-advanced-toggle"1199 onClick={() => setShowVaultAdvanced((v) => !v)}1200 aria-expanded={showVaultAdvanced()}1201 >1202 <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1203 <span class="peach-settings-advanced-chevron">1204 {showVaultAdvanced() ? "▲" : "▼"}1205 </span>1206 </button>12071208 <Show when={showVaultAdvanced()}>1209 <div class="peach-danger-card">1210 <div class="peach-danger-card-head">1211 <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1212 <h3 class="peach-danger-card-title">Replace vault from backup</h3>1213 </div>1214 <p class="peach-danger-card-copy">1215 Wipes the current vault and restores from a backup file.1216 Recoverable only if your backup is good.1217 </p>1218 <button1219 class="peach-danger-card-button"1220 onClick={() => props.onOpenFileImport?.()}1221 >1222 Choose backup file1223 </button>1224 </div>12251226 <Show when={props.onOpenPanic}>1227 <div class="peach-danger-card peach-danger-card--hazard">1228 <div class="peach-danger-card-head">1229 <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1230 <h3 class="peach-danger-card-title">Destroy vault</h3>1231 </div>1232 <p class="peach-danger-card-copy">1233 Permanently wipes every entry, setting, and license on this1234 device. There is no recovery.1235 </p>1236 <button1237 class="peach-danger-card-button"1238 onClick={() => props.onOpenPanic?.()}1239 >1240 Begin destruction1241 </button>1242 </div>1243 </Show>1244 </Show>1245 </div>1246 </div>1247 );1248 }12491250 function renderSync() {1251 if (!isPro()) {1252 return (1253 <div class="peach-settings-body">1254 <div class="peach-pro-upgrade">1255 <div class="peach-pro-upgrade-hero">1256 <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1257 <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1258 <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1259 <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"/>1260 <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"/>1261 <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1262 <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1263 <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1264 </svg>1265 <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1266 <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1267 </div>12681269 <div class="peach-pro-features">1270 <div class="peach-pro-feature">1271 <span class="peach-pro-feature-mark">✦</span>1272 <div class="peach-pro-feature-text">1273 <span class="peach-pro-feature-name">Always in sync</span>1274 <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1275 </div>1276 </div>1277 <div class="peach-pro-feature">1278 <span class="peach-pro-feature-mark">✦</span>1279 <div class="peach-pro-feature-text">1280 <span class="peach-pro-feature-name">No cloud. No relay.</span>1281 <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1282 </div>1283 </div>1284 <div class="peach-pro-feature">1285 <span class="peach-pro-feature-mark">✦</span>1286 <div class="peach-pro-feature-text">1287 <span class="peach-pro-feature-name">Tailscale supported</span>1288 <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1289 </div>1290 </div>1291 <div class="peach-pro-feature">1292 <span class="peach-pro-feature-mark">✦</span>1293 <div class="peach-pro-feature-text">1294 <span class="peach-pro-feature-name">Zero friction after setup</span>1295 <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1296 </div>1297 </div>1298 </div>12991300 <button1301 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1302 onClick={() => setActiveSection("license")}1303 >1304 Upgrade to Pro1305 </button>1306 <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1307 Already have a key? Enter it →1308 </button>1309 </div>1310 </div>1311 );1312 }13131314 return (1315 <div class="peach-settings-body">1316 <div class="peach-settings-group">1317 <div class="peach-settings-row">1318 <span>Status</span>1319 <span class="peach-settings-value peach-settings-value--status">1320 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1321 {getStatusLabel(syncStatus())}1322 </span>1323 </div>1324 <div class="peach-settings-row peach-settings-row--meta">1325 <span>Last sync</span>1326 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1327 </div>1328 <div class="peach-settings-row peach-settings-row--meta">1329 <button1330 class="peach-btn peach-btn--subtle peach-btn--sm"1331 onClick={() => void handleSyncNow()}1332 disabled={syncStatus() === "not_trusted" || syncingNow()}1333 >1334 <Switch>1335 <Match when={syncingNow()}>Syncing…</Match>1336 <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1337 <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1338 <Match when={true}>Sync now</Match>1339 </Switch>1340 </button>1341 </div>1342 <p class="peach-settings-hint">1343 If you edit on multiple devices before syncing, the most recently1344 changed device wins. Use Vault History above to recover older1345 versions if needed.1346 </p>13471348 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1349 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1350 <Show when={pairedDevices().length === 0}>1351 <p class="peach-settings-hint">1352 No paired devices yet. Add a device to start syncing.1353 </p>1354 </Show>1355 <Show when={pairedDevices().length > 0}>1356 <div class="peach-history-list">1357 <For each={pairedDevices()}>1358 {(device) => (1359 <div class="peach-history-row">1360 <div class="peach-history-meta">1361 <span class="peach-history-time">{device.displayName}</span>1362 <span class="peach-history-sub">1363 {device.lastSyncAt1364 ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1365 : "Never synced"}1366 </span>1367 </div>1368 <button1369 class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1370 onClick={() => void handleRemoveDevice(device.deviceId)}1371 >1372 Remove1373 </button>1374 </div>1375 )}1376 </For>1377 </div>1378 </Show>1379 <div class="peach-settings-actions--row">1380 <button1381 class="peach-btn peach-btn--subtle peach-btn--sm"1382 onClick={() => props.onOpenSyncPairing?.()}1383 >1384 Add a device1385 </button>1386 </div>1387 </div>13881389 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1390 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1391 <For each={trustedNetworks()}>1392 {(network) => (1393 <div class="peach-sync-device-row">1394 <span class="peach-sync-device-name">1395 {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1396 </span>1397 <Show1398 when={!network.isTailscale}1399 fallback={<span class="peach-sync-device-meta">Always trusted</span>}1400 >1401 <button1402 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1403 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1404 >1405 Remove1406 </button>1407 </Show>1408 </div>1409 )}1410 </For>1411 <div class="peach-settings-actions--row">1412 <Show when={!isCurrentNetworkTrusted()}>1413 <button1414 class="peach-btn peach-btn--subtle peach-btn--sm"1415 onClick={() => void handleMarkNetworkTrusted()}1416 disabled={markingNetwork()}1417 >1418 {markingNetwork() ? "Marking…" : "Mark current network as trusted"}1419 </button>1420 </Show>1421 </div>1422 </div>14231424 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1425 <span class="peach-sync-section-label">Advanced</span>1426 <Show1427 when={resetIdentityConfirm()}1428 fallback={1429 <div class="peach-sync-advanced-row">1430 <div class="peach-sync-advanced-info">1431 <span class="peach-sync-advanced-name">Reset sync identity</span>1432 <span class="peach-sync-device-meta">1433 Generates a new keypair and unpairs every device.1434 </span>1435 </div>1436 <button1437 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1438 onClick={() => setResetIdentityConfirm(true)}1439 >1440 Reset1441 </button>1442 </div>1443 }1444 >1445 <div class="peach-sync-reset-confirm">1446 <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1447 <p class="peach-sync-reset-confirm-body">1448 This will unpair all devices. You will need to pair them again from scratch.1449 </p>1450 <div class="peach-settings-actions--row">1451 <button1452 class="peach-btn peach-btn--ghost peach-btn--sm"1453 onClick={() => setResetIdentityConfirm(false)}1454 disabled={resetIdentityRunning()}1455 >1456 Cancel1457 </button>1458 <button1459 class="peach-btn peach-btn--danger peach-btn--sm"1460 onClick={() => void handleResetSyncIdentity()}1461 disabled={resetIdentityRunning()}1462 >1463 {resetIdentityRunning() ? "Resetting..." : "Reset"}1464 </button>1465 </div>1466 </div>1467 </Show>1468 </div>1469 </div>1470 </div>1471 );1472 }14731474 function renderAppearance() {1475 const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1476 { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1477 { id: "cormorant", label: "Cormorant", preview: "Aa" },1478 { id: "inter", label: "Inter", preview: "Aa" },1479 { id: "system", label: "System", preview: "Aa" },1480 ];1481 const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1482 { id: "small", label: "Small" },1483 { id: "medium", label: "Default" },1484 { id: "large", label: "Large" },1485 ];1486 return (1487 <div class="peach-settings-body">1488 <div class="peach-settings-group">1489 <p class="peach-settings-sub-label">THEME</p>1490 <div class="peach-settings-row peach-settings-row--meta">1491 <span>Theme</span>1492 <button1493 class="peach-btn peach-btn--subtle peach-btn--sm"1494 onClick={() =>1495 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1496 }1497 >1498 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1499 </button>1500 </div>15011502 <p class="peach-settings-sub-label">ACCENT</p>1503 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1504 <span>Accent</span>1505 <div class="peach-accent-picker">1506 <For each={ACCENT_COLORS}>1507 {(color) => (1508 <button1509 class="peach-accent-swatch"1510 classList={{ "is-active": props.accentColor === color.name }}1511 style={{ background: color.hex }}1512 onClick={() => void props.onAccentChange(color.name)}1513 aria-label={color.name}1514 title={color.name}1515 />1516 )}1517 </For>1518 </div>1519 </div>15201521 <p class="peach-settings-sub-label">DISPLAY FONT</p>1522 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1523 <span>Font</span>1524 <div class="peach-font-picker">1525 <For each={fontOptions}>1526 {(opt) => (1527 <button1528 class="peach-font-swatch"1529 classList={{ "is-active": props.displayFont === opt.id }}1530 data-font-preview={opt.id}1531 onClick={() => void props.onDisplayFontChange(opt.id)}1532 aria-label={opt.label}1533 title={opt.label}1534 >1535 <span class="peach-font-swatch-glyph">{opt.preview}</span>1536 <span class="peach-font-swatch-name">{opt.label}</span>1537 </button>1538 )}1539 </For>1540 </div>1541 </div>15421543 <p class="peach-settings-sub-label">TEXT SIZE</p>1544 <div class="peach-settings-row peach-settings-row--meta">1545 <span>Size</span>1546 <div class="peach-pill-group">1547 <For each={sizeOptions}>1548 {(opt) => (1549 <button1550 class="peach-btn peach-btn--ghost peach-btn--sm"1551 classList={{ "is-active": props.textSize === opt.id }}1552 onClick={() => void props.onTextSizeChange(opt.id)}1553 >1554 {opt.label}1555 </button>1556 )}1557 </For>1558 </div>1559 </div>15601561 <p class="peach-settings-sub-label">MOTION</p>1562 <div class="peach-settings-row peach-settings-row--meta">1563 <span>Reduce motion</span>1564 <button1565 class="peach-btn peach-btn--subtle peach-btn--sm"1566 onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1567 >1568 {props.reduceMotion ? "On" : "Off"}1569 </button>1570 </div>1571 </div>1572 </div>1573 );1574 }15751576 function renderShortcuts() {1577 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1578 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15791580 function startCapture(action: ShortcutAction) {1581 setConflictMsg(null);1582 setCapturingAction(action);1583 }1584 function cancelCapture() {1585 setCapturingAction(null);1586 setConflictMsg(null);1587 }1588 function commitBinding(action: ShortcutAction, binding: Keybinding) {1589 const conflict = findConflict(props.shortcuts, binding, action);1590 if (conflict) {1591 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1592 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1593 return;1594 }1595 const next = { ...props.shortcuts, [action]: binding };1596 setCapturingAction(null);1597 setConflictMsg(null);1598 void props.onShortcutsChange(next);1599 }1600 function resetAction(action: ShortcutAction) {1601 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1602 void props.onShortcutsChange(next);1603 }1604 function resetAll() {1605 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1606 }16071608 1609 1610 function handleCaptureKey(e: KeyboardEvent) {1611 e.preventDefault();1612 e.stopPropagation();1613 if (e.key === "Escape") { cancelCapture(); return; }1614 const binding = keybindingFromEvent(e);1615 if (!binding) return; 1616 const action = capturingAction();1617 if (action) commitBinding(action, binding);1618 }16191620 return (1621 <div class="peach-settings-body">1622 <div class="peach-settings-group peach-shortcuts-group">1623 <p class="peach-settings-sub-label">ENTRY VIEW</p>1624 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1625 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1626 </For>1627 </div>16281629 <div class="peach-settings-group peach-shortcuts-group">1630 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1631 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1632 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1633 </For>1634 </div>16351636 <Show when={conflictMsg()}>1637 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1638 </Show>16391640 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1641 <div class="peach-settings-row peach-settings-row--meta">1642 <span>Inline autofill on page</span>1643 <a1644 class="peach-settings-link"1645 href="chrome://extensions/shortcuts"1646 onClick={(e) => {1647 e.preventDefault();1648 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1649 }}1650 >1651 ⌘⇧P — customize ↗1652 </a>1653 </div>1654 <button1655 class="peach-btn peach-btn--subtle peach-btn--sm"1656 style="align-self:flex-start"1657 onClick={resetAll}1658 >1659 Reset all to defaults1660 </button>1661 </div>1662 </div>1663 );16641665 function renderRow(1666 desc: typeof SHORTCUT_DESCRIPTORS[number],1667 capturing: () => ShortcutAction | null,1668 onStart: (a: ShortcutAction) => void,1669 onCancel: () => void,1670 onReset: (a: ShortcutAction) => void,1671 onKey: (e: KeyboardEvent) => void,1672 ) {1673 const isActive = () => capturing() === desc.action;1674 const binding = () => props.shortcuts[desc.action];1675 const isCustom = () => {1676 const b = binding();1677 const d = DEFAULT_BINDINGS[desc.action];1678 return b.key !== d.key1679 || Boolean(b.ctrl) !== Boolean(d.ctrl)1680 || Boolean(b.meta) !== Boolean(d.meta)1681 || Boolean(b.shift) !== Boolean(d.shift)1682 || Boolean(b.alt) !== Boolean(d.alt);1683 };1684 return (1685 <div class="peach-shortcut-row">1686 <span class="peach-shortcut-row-label">{desc.label}</span>1687 <button1688 class="peach-shortcut-chip"1689 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1690 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1691 onKeyDown={(e) => { if (isActive()) onKey(e); }}1692 ref={(el) => {1693 1694 if (el) {1695 queueMicrotask(() => { if (isActive()) el.focus(); });1696 }1697 }}1698 >1699 {isActive() ? "Press a key…" : formatKeybinding(binding())}1700 </button>1701 <Show when={isCustom() && !isActive()}>1702 <button1703 class="peach-shortcut-reset"1704 title="Reset to default"1705 onClick={() => onReset(desc.action)}1706 >1707 ↺1708 </button>1709 </Show>1710 </div>1711 );1712 }1713 }17141715 const PRO_FEATURES: Array<{1716 feature: ProFeature;1717 name: string;1718 desc: string;1719 }> = [1720 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1721 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1722 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1723 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1724 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1725 ];17261727 const [licenseKey, setLicenseKey] = createSignal<string | null>(null);17281729 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1730 const [deactivating, setDeactivating] = createSignal(false);17311732 async function handleDeactivateLicense() {1733 setDeactivating(true);1734 await sendMessage({ type: "DEACTIVATE_LICENSE" });1735 setLicenseInfo(null);1736 await props.onLicenseChange?.();1737 setDeactivating(false);1738 }17391740 async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1741 setCanaryTestingId(entry.id);1742 setCanaryTestResult(null);1743 const res = await sendMessage({1744 type: "FIRE_CANARY_PING",1745 payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1746 }) as { ok: boolean };1747 setCanaryTestResult({ id: entry.id, ok: res.ok });1748 setCanaryTestingId(null);1749 setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1750 }17511752 function renderLicense() {1753 async function onActivate() {1754 await handleActivateLicense();1755 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1756 await props.onLicenseChange?.();1757 }1758 }17591760 return (1761 <div class="peach-settings-body">1762 <Show1763 when={isPro()}1764 fallback={1765 <Show1766 when={showKeyEntry()}1767 fallback={1768 <div class="peach-pro-upgrade">1769 <div class="peach-pro-upgrade-hero">1770 <div class="peach-pro-upgrade-mark-wrap">1771 <PeachMark class="peach-pro-upgrade-mark" />1772 </div>1773 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1774 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1775 </div>17761777 <div class="peach-pro-features">1778 <For each={PRO_FEATURES}>1779 {(item) => (1780 <div1781 class="peach-pro-feature"1782 classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1783 >1784 <span class="peach-pro-feature-mark">✦</span>1785 <div class="peach-pro-feature-text">1786 <span class="peach-pro-feature-name">{item.name}</span>1787 <span class="peach-pro-feature-desc">{item.desc}</span>1788 </div>1789 </div>1790 )}1791 </For>1792 </div>17931794 <div class="peach-pro-upgrade-actions">1795 <p class="peach-pro-pricing">$39 · once · yours forever</p>1796 <a1797 href="https://peachpasswords.com/#pricing"1798 target="_blank"1799 rel="noopener noreferrer"1800 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1801 >1802 Get Peach Pro1803 </a>1804 <div class="peach-pro-key-divider"><span>or</span></div>1805 <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1806 Activate a license key1807 </button>1808 </div>1809 </div>1810 }1811 >1812 <div class="peach-pro-key-ceremony">1813 <LockRuneCanvas />1814 <p class="peach-pro-key-ceremony-label">Enter your license key</p>1815 <input1816 class="peach-underline-input peach-pro-key-input"1817 type="text"1818 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1819 value={licenseKeyInput()}1820 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1821 onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1822 autocomplete="off"1823 spellcheck={false}1824 />1825 <Show when={licenseError()}>1826 <p class="peach-pro-key-error">That key doesn't look right.</p>1827 </Show>1828 <button1829 class="peach-btn peach-btn--accent peach-pro-key-activate"1830 onClick={() => void onActivate()}1831 disabled={activatingLicense() || !licenseKeyInput().trim()}1832 >1833 {activatingLicense() ? "activating…" : "Activate"}1834 </button>1835 <button1836 class="peach-pro-key-cancel"1837 onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1838 >1839 cancel1840 </button>1841 </div>1842 </Show>1843 }1844 >1845 <div class="peach-pro-active">1846 <div class="peach-pro-active-hero">1847 <div class="peach-pro-active-mark-wrap">1848 <PeachMark class="peach-pro-active-mark" />1849 </div>1850 <h2 class="peach-pro-active-title">Peach Pro</h2>1851 <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1852 </div>18531854 <Show when={licenseKey()}>1855 <div class="peach-settings-license">1856 <span class="peach-settings-label">LICENSE KEY</span>1857 <div class="peach-settings-license-row">1858 <span class="peach-settings-license-key">{licenseKey()!}</span>1859 <button1860 class="peach-settings-license-copy"1861 onClick={() => copyLicenseKey(licenseKey()!)}1862 >1863 {copied() ? "✓" : "copy"}1864 </button>1865 </div>1866 </div>1867 </Show>18681869 <div class="peach-pro-active-features">1870 <For each={PRO_FEATURES}>1871 {(item) => (1872 <div class="peach-pro-active-feature">1873 <span class="peach-pro-active-feature-mark">✦</span>1874 <span class="peach-pro-active-feature-name">{item.name}</span>1875 </div>1876 )}1877 </For>1878 </div>18791880 <button1881 class="peach-pro-deactivate"1882 onClick={() => void handleDeactivateLicense()}1883 disabled={deactivating()}1884 >1885 {deactivating() ? "Deactivating…" : "Deactivate"}1886 </button>1887 </div>1888 </Show>1889 </div>1890 );1891 }18921893 function renderFeedback() {1894 return (1895 <div class="peach-settings-body">1896 <div class="peach-settings-group peach-feedback-group">1897 <p class="peach-feedback-intro">1898 Help us improve Peach. Every report goes directly to our inbox.1899 </p>19001901 <p class="peach-settings-sub-label">TYPE</p>1902 <div class="peach-feedback-chips">1903 <For each={FEEDBACK_CATEGORIES}>1904 {(cat) => (1905 <button1906 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1907 onClick={() => setFbCategory(cat)}1908 >1909 {cat}1910 </button>1911 )}1912 </For>1913 </div>19141915 <p class="peach-settings-sub-label">MESSAGE</p>1916 <textarea1917 class="peach-feedback-textarea"1918 placeholder="Describe the issue or idea..."1919 value={fbMessage()}1920 onInput={(e) => setFbMessage(e.currentTarget.value)}1921 />19221923 <p class="peach-feedback-meta">1924 Version {version} · will be included automatically1925 </p>19261927 <Show when={fbResult() === "success"}>1928 <p class="peach-feedback-result peach-feedback-result--ok">1929 Sent — thank you1930 </p>1931 </Show>1932 <Show when={fbResult() === "error"}>1933 <p class="peach-feedback-result peach-feedback-result--err">1934 Failed to send — try again1935 </p>1936 </Show>19371938 <button1939 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1940 onClick={() => void handleSubmitFeedback()}1941 disabled={fbSubmitting() || !fbMessage().trim()}1942 >1943 {fbSubmitting() ? "Sending..." : "Send feedback"}1944 </button>19451946 <p class="peach-feedback-anon">1947 Sent anonymously · no account required1948 </p>1949 </div>1950 </div>1951 );1952 }19531954 function renderMobile() {1955 return (1956 <div class="peach-settings-body">1957 <div class="peach-settings-group">1958 <p class="peach-settings-label">Autofill service</p>1959 <div class="peach-mobile-row">1960 <span class="peach-mobile-status">1961 {autofillEnabled() ? "Enabled" : "Disabled"}1962 </span>1963 <button1964 class="peach-btn peach-btn--subtle peach-btn--sm"1965 onClick={() => void AutofillPlugin?.openAutofillSettings()}1966 >1967 {autofillEnabled() ? "Change" : "Enable"}1968 </button>1969 </div>1970 </div>19711972 <div class="peach-settings-group">1973 <p class="peach-settings-label">Biometric unlock</p>1974 <div class="peach-mobile-row">1975 <span class="peach-mobile-status">1976 {biometricEnabled() ? "Available" : "Not available"}1977 </span>1978 </div>1979 </div>1980 </div>1981 );1982 }19831984 return (1985 <div class="peach-overlay-view ov-enter-right" style="display:flex">1986 <div class="peach-overlay-header">1987 <button1988 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1989 onClick={handleHeaderBack}1990 aria-label="Back"1991 >1992 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1993 <path d="M6 1L1 6L6 11" />1994 </svg>1995 </button>1996 <span class="peach-overlay-title">{currentTitle()}</span>1997 <div class="peach-overlay-spacer"></div>1998 </div>19992000 <Switch fallback={renderHome()}>2001 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>2002 <Match when={activeSection() === "vault"}>{renderVault()}</Match>2003 <Match when={activeSection() === "sync"}>{renderSync()}</Match>2004 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>2005 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>2006 <Match when={activeSection() === "license"}>{renderLicense()}</Match>2007 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>2008 <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>2009 </Switch>2010 </div>2011 );2012}