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?: (mode: "initiate" | "accept") => 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 formatHistoryTime(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 [showChangePw, setShowChangePw] = createSignal(false);200 const [changePwCurrent, setChangePwCurrent] = createSignal("");201 const [changePwNew, setChangePwNew] = createSignal("");202 const [changePwConfirm, setChangePwConfirm] = createSignal("");203 const [changePwSaving, setChangePwSaving] = createSignal(false);204 const [changePwError, setChangePwError] = createSignal<string | null>(null);205 const [changePwSuccess, setChangePwSuccess] = createSignal(false);206207 const [duressEnabled, setDuressEnabled] = createSignal(false);208 const [showDuressSetup, setShowDuressSetup] = createSignal(false);209 const [duressPassword, setDuressPassword] = createSignal("");210 const [duressConfirm, setDuressConfirm] = createSignal("");211 const [duressMainPassword, setDuressMainPassword] = createSignal("");212 const [duressSaving, setDuressSaving] = createSignal(false);213 const [duressError, setDuressError] = createSignal<string | null>(null);214 const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);215216 const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);217218 219 function getAutofillPlugin() {220 try {221 return registerPlugin<{222 isEnabledAsAutofillService(): Promise<{ enabled: boolean }>;223 openAutofillSettings(): Promise<void>;224 }>("Autofill");225 } catch {226 return null;227 }228 }229 const AutofillPlugin = getAutofillPlugin();230 const [autofillEnabled, setAutofillEnabled] = createSignal(false);231 const [biometricEnabled, setBiometricEnabled] = createSignal(false);232 const isAndroid = Capacitor.getPlatform() === "android";233 const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);234 const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);235236 const [fbCategory, setFbCategory] = createSignal("Bug report");237 const [fbMessage, setFbMessage] = createSignal("");238 const [fbSubmitting, setFbSubmitting] = createSignal(false);239 const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);240241 onMount(async () => {242 const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([243 sendMessage({ type: "GET_LICENSE" }),244 sendMessage({ type: "GET_SYNC_STATE" }),245 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),246 sendMessage({ type: "IS_DURESS_ENABLED" }),247 sendMessage({ type: "GET_SETTINGS" }),248 sendMessage({ type: "GET_LICENSE_KEY" }),249 ]);250251 252 253 void loadVaultHistory();254255 if ((licenseRes as { ok: boolean }).ok) {256 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);257 }258259 if ((syncRes as { ok: boolean }).ok) {260 const data = syncRes as { ok: boolean; data?: {261 pairedDevices?: PairedDevice[];262 trustedNetworks?: TrustedNetwork[];263 status?: SyncStatus;264 lastSyncAt?: number;265 isPro?: boolean;266 }};267 if (data.data) {268 setPairedDevices(data.data.pairedDevices ?? []);269 setTrustedNetworks(data.data.trustedNetworks ?? []);270 setSyncStatus(data.data.status ?? "offline");271 setLastSyncAt(data.data.lastSyncAt);272 setSyncProStatus(data.data.isPro ?? false);273 }274 }275276 if ((passkeyRes as { ok: boolean }).ok) {277 setPasskeyState(278 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null279 );280 }281282 if ((duressRes as { ok: boolean }).ok) {283 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);284 }285286 if ((settingsRes as { ok: boolean }).ok) {287 const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;288 setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);289 }290291 if ((keyRes as { ok: boolean }).ok) {292 setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null);293 }294295 const entriesRes = await sendMessage({ type: "GET_ENTRIES" });296 if ((entriesRes as { ok: boolean }).ok) {297 const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? [];298 setCanaryEntries(299 entries300 .filter((e) => e.canary?.webhookUrl)301 .map((e) => ({ id: e.id, title: e.title, canary: e.canary! }))302 );303 }304305 306 if (isAndroid) {307 if (AutofillPlugin) {308 const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();309 setAutofillEnabled(enabled);310 }311 const bio = await biometricAvailable();312 setBiometricEnabled(bio);313 }314315 chrome.runtime.onMessage.addListener((msg) => {316 if (msg.type === "SYNC_STATE_UPDATE") {317 setPairedDevices(msg.pairedDevices ?? []);318 setTrustedNetworks(msg.trustedNetworks ?? []);319 setSyncStatus(msg.status ?? "offline");320 setLastSyncAt(msg.lastSyncAt);321 setSyncProStatus(msg.isPro ?? false);322 }323 });324 });325326 async function handleChangePassword() {327 setChangePwError(null);328 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }329 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }330 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }331 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }332 setChangePwSaving(true);333 const res = await sendMessage({334 type: "CHANGE_PASSWORD",335 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },336 }) as { ok: boolean; error?: string };337 if (res.ok) {338 setChangePwSuccess(true);339 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");340 setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);341 } else {342 setChangePwError(res.error ?? "failed to change password");343 }344 setChangePwSaving(false);345 }346347 const [copied, setCopied] = createSignal(false);348349 async function copyLicenseKey(key: string) {350 await navigator.clipboard.writeText(key);351 setCopied(true);352 setTimeout(() => setCopied(false), 2000);353 }354355 async function handleActivateLicense() {356 const key = licenseKeyInput().trim();357 if (!key) return;358 setActivatingLicense(true);359 setLicenseError(null);360 setLicenseSuccess(null);361 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });362 setActivatingLicense(false);363 if ((res as { ok: boolean }).ok) {364 const data = (res as { ok: boolean; data?: LicenseInfo }).data;365 if (data?.valid) {366 setLicenseInfo(data);367 setLicenseKey(key);368 setLicenseSuccess("License activated!");369 setLicenseKeyInput("");370 } else {371 setLicenseError("Invalid license key");372 }373 } else {374 setLicenseError("Activation failed");375 }376 }377378 async function handleTogglePasskeys() {379 const current = passkeyState();380 if (!current || !current.supported) return;381 setPasskeySaving(true);382 const res = await sendMessage({383 type: "SET_PASSKEY_PROVIDER_ENABLED",384 payload: { enabled: !current.enabled },385 });386 setPasskeySaving(false);387 if ((res as { ok: boolean }).ok) {388 setPasskeyState(389 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null390 );391 }392 }393394 async function handleCheckAllBreaches() {395 setCheckingBreaches(true);396 setBreachResult(null);397 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });398 setCheckingBreaches(false);399 if ((res as { ok: boolean }).ok) {400 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};401 const breached = Object.values(data).filter((count) => count > 0).length;402 setBreachResult(403 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"404 );405 props.onBreachCheckDone();406 }407 }408409 async function handleSyncNow() {410 if (syncStatus() === "not_trusted") return;411 setSyncingNow(true);412 setSyncFeedback(null);413 const res = await sendMessage({ type: "TRIGGER_SYNC" });414 const ok = (res as { ok: boolean }).ok;415 if (ok) {416 const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;417 const count = data?.syncedCount ?? 0;418 setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });419 setLastSyncAt(Date.now());420 } else {421 const error = (res as { error?: string }).error;422 const map: Record<string, string> = {423 no_paired_devices: "No paired devices",424 no_devices_reachable: "No devices found",425 vault_locked: "Vault is locked",426 pro_required: "Sync requires Pro",427 };428 setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });429 }430 setTimeout(() => {431 setSyncingNow(false);432 setSyncFeedback(null);433 }, 3500);434 }435436 async function handleRemoveDevice(deviceId: string) {437 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });438 }439440 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {441 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });442 }443444 async function handleMarkNetworkTrusted() {445 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });446 }447448 const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);449 const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);450451 async function handleResetSyncIdentity() {452 setResetIdentityRunning(true);453 const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });454 setResetIdentityRunning(false);455 if ((res as { ok: boolean }).ok) {456 setPairedDevices([]);457 setResetIdentityConfirm(false);458 }459 }460461 function formatRelativeTime(timestamp?: number): string {462 if (!timestamp) return "Never";463 const diff = Date.now() - timestamp;464 const minutes = Math.floor(diff / 60000);465 const hours = Math.floor(diff / 3600000);466 const days = Math.floor(diff / 86400000);467 if (minutes < 1) return "Just now";468 if (minutes < 60) return `${minutes}m ago`;469 if (hours < 24) return `${hours}h ago`;470 return `${days}d ago`;471 }472473 function getStatusColor(status: SyncStatus): string {474 switch (status) {475 case "synced": return "var(--peach-sync-ok)";476 case "syncing": return "var(--peach-sync-busy)";477 case "no_peers": return "var(--peach-sync-off)";478 case "not_trusted": return "var(--peach-sync-err)";479 case "offline": return "var(--peach-sync-off)";480 default: return "var(--peach-sync-off)";481 }482 }483484 function getStatusLabel(status: SyncStatus): string {485 switch (status) {486 case "synced": return "Synced";487 case "syncing": return "Syncing";488 case "no_peers": return "No peers visible";489 case "not_trusted": return "Not on a trusted network";490 case "offline": return "Offline";491 default: return "Unknown";492 }493 }494495 function isCurrentNetworkTrusted(): boolean {496 return trustedNetworks().length > 0;497 }498499 async function handleSetupDuress() {500 const mainPw = duressMainPassword().trim();501 const duressPw = duressPassword().trim();502 const confirmPw = duressConfirm().trim();503504 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }505 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }506 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }507 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }508509 setDuressSaving(true);510 setDuressError(null);511512 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };513 if (!verifyRes.ok || !verifyRes.data) {514 setDuressError("incorrect main password");515 setDuressSaving(false);516 return;517 }518519 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };520 setDuressSaving(false);521 if (res.ok) {522 setDuressEnabled(true);523 setShowDuressSetup(false);524 setDuressPassword("");525 setDuressConfirm("");526 setDuressMainPassword("");527 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");528 setTimeout(() => setDuressSuccess(null), 5000);529 } else {530 setDuressError(res.error ?? "failed to set duress password");531 }532 }533534 async function handleClearDeclinedCards() {535 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });536 setDeclinedCardCount(0);537 }538539 async function handleCountDuplicates() {540 setDedupRunning(true);541 setDedupDone(null);542 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };543 setDedupRunning(false);544 if (res.ok) setDedupCount(res.data ?? 0);545 }546547 async function handleDeduplicate() {548 setDedupRunning(true);549 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };550 setDedupRunning(false);551 if (res.ok) {552 setDedupDone(res.data ?? 0);553 setDedupCount(0);554 }555 }556557 async function handleDisableDuress() {558 const mainPw = duressMainPassword().trim();559 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }560561 setDuressSaving(true);562 setDuressError(null);563564 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };565 if (!verifyRes.ok || !verifyRes.data) {566 setDuressError("incorrect main password");567 setDuressSaving(false);568 return;569 }570571 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };572 setDuressSaving(false);573 if (res.ok) {574 setDuressEnabled(false);575 setShowDuressSetup(false);576 setDuressMainPassword("");577 setDuressSuccess("Duress password disabled.");578 setTimeout(() => setDuressSuccess(null), 4000);579 } else {580 setDuressError(res.error ?? "failed to disable duress password");581 }582 }583584 async function handleSubmitFeedback() {585 if (!fbMessage().trim() || fbSubmitting()) return;586 setFbSubmitting(true);587 setFbResult(null);588 const res = await sendMessage({589 type: "SUBMIT_FEEDBACK",590 payload: {591 category: fbCategory(),592 message: fbMessage().trim(),593 version,594 browser: navigator.userAgent,595 },596 }) as { ok: boolean; error?: string };597 setFbSubmitting(false);598 if (res.ok) {599 setFbResult("success");600 setFbMessage("");601 setTimeout(() => setFbResult(null), 4000);602 } else {603 setFbResult("error");604 }605 }606607 function handleHeaderBack() {608 if (activeSection() !== null) {609 setActiveSection(null);610 } else {611 props.onBack();612 }613 }614615 function currentTitle(): string {616 const s = activeSection();617 if (s === null) return "settings";618 if (s === "license") return "account";619 return s;620 }621622 function renderCatRow(opts: {623 section: SectionName;624 name: string;625 desc: string;626 icon: () => any;627 pro?: boolean;628 }) {629 return (630 <button631 class="peach-settings-cat-row"632 onClick={() => setActiveSection(opts.section)}633 >634 <div class="peach-settings-cat-icon">{opts.icon()}</div>635 <div class="peach-settings-cat-content">636 <span class="peach-settings-cat-name">637 {opts.name}638 <Show when={opts.pro && !isPro()}>639 <span class="peach-pro-mark" aria-label="Pro feature">✦</span>640 </Show>641 </span>642 <span class="peach-settings-cat-desc">{opts.desc}</span>643 </div>644 <svg645 class="peach-settings-cat-chevron"646 width="6" height="10" viewBox="0 0 6 10" fill="none"647 stroke="currentColor" stroke-width="1.1"648 stroke-linecap="round" stroke-linejoin="round"649 >650 <path d="M1 1l4 4-4 4" />651 </svg>652 </button>653 );654 }655656 const iconShield = () => (657 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">658 <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>659 </svg>660 );661 const iconKey = () => (662 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">663 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>664 </svg>665 );666 const iconLock = () => (667 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">668 <rect x="3" y="7" width="10" height="7" rx="1.5"/>669 <path d="M6 7V5a3 3 0 016 0v2"/>670 </svg>671 );672 const iconSync = () => (673 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">674 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>675 <path d="M14 6l-2 2-2-2"/>676 <path d="M2 10l2-2 2 2"/>677 </svg>678 );679 const iconSun = () => (680 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">681 <circle cx="8" cy="8" r="3"/>682 <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"/>683 </svg>684 );685 const iconKeys = () => (686 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">687 <rect x="2" y="5" width="5" height="4" rx="1"/>688 <rect x="9" y="7" width="5" height="4" rx="1"/>689 <path d="M7 7h2m-2 2h2"/>690 </svg>691 );692 const iconBubble = () => (693 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">694 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>695 </svg>696 );697698 function renderHome() {699 return (700 <div class="peach-settings-body">701 <p class="peach-settings-nav-label">PROTECTION</p>702 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}703 {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}704705 <p class="peach-settings-nav-label">VAULT</p>706 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}707 {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}708709 <p class="peach-settings-nav-label">PREFERENCES</p>710 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}711 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}712 <Show when={isAndroid}>713 <p class="peach-settings-nav-label">MOBILE</p>714 {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}715 </Show>716717 <p class="peach-settings-nav-label">SUPPORT</p>718 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}719720 <div class="peach-settings-footer">721 <span>Peach {version}</span>722 <span class="peach-divider"></span>723 <span>Browser extension</span>724 </div>725 </div>726 );727 }728729 function renderSecurity() {730 return (731 <div class="peach-settings-body">732 <div class="peach-settings-group">733 <p class="peach-settings-sub-label">LOCKING</p>734 <div class="peach-settings-row">735 <span>Lock on close</span>736 <button737 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}738 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}739 aria-label="Toggle lock on close"740 >741 <span class="peach-toggle-knob"></span>742 </button>743 </div>744 <div class="peach-settings-row">745 <span>Auto-lock after</span>746 <select747 class="peach-settings-select"748 value={String(props.autoLockMinutes)}749 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}750 >751 <option value="1">1 minute</option>752 <option value="5">5 minutes</option>753 <option value="15">15 minutes</option>754 <option value="30">30 minutes</option>755 <option value="0">Never</option>756 </select>757 </div>758759 <p class="peach-settings-sub-label">BREACH PROTECTION</p>760 <div class="peach-settings-row">761 <span>Breach monitoring</span>762 <button763 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}764 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}765 aria-label="Toggle breach monitoring"766 >767 <span class="peach-toggle-knob"></span>768 </button>769 </div>770 <div class="peach-settings-row peach-settings-row--meta">771 <span>Check all passwords</span>772 <button773 class="peach-btn peach-btn--subtle peach-btn--sm"774 onClick={() => void handleCheckAllBreaches()}775 disabled={checkingBreaches()}776 >777 {checkingBreaches() ? "checking..." : "run"}778 </button>779 </div>780 <Show when={breachResult()}>781 <div class="peach-settings-row peach-settings-row--meta">782 <span>Last check</span>783 <span class="peach-settings-value">{breachResult()}</span>784 </div>785 </Show>786787 <p class="peach-settings-sub-label">MASTER PASSWORD</p>788 <div class="peach-settings-row peach-settings-row--meta">789 <span>Master password</span>790 <button791 class="peach-btn peach-btn--subtle peach-btn--sm"792 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}793 >794 {showChangePw() ? "cancel" : "change"}795 </button>796 </div>797 <Show when={showChangePw()}>798 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">799 <input800 class="peach-settings-input"801 type="password"802 placeholder="current password"803 value={changePwCurrent()}804 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}805 autocomplete="current-password"806 />807 <input808 class="peach-settings-input"809 type="password"810 placeholder="new password (min 12 chars)"811 value={changePwNew()}812 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}813 autocomplete="new-password"814 />815 <input816 class="peach-settings-input"817 type="password"818 placeholder="confirm new password"819 value={changePwConfirm()}820 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}821 autocomplete="new-password"822 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}823 />824 <Show when={changePwError()}>825 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>826 </Show>827 <Show when={changePwSuccess()}>828 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>829 </Show>830 <button831 class="peach-btn peach-btn--accent peach-btn--sm"832 style="align-self:flex-start"833 onClick={() => void handleChangePassword()}834 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}835 >836 {changePwSaving() ? "saving…" : "save new password"}837 </button>838 </div>839 </Show>840841 <p class="peach-settings-sub-label">DURESS PASSWORD</p>842 <div class="peach-settings-row peach-settings-row--top">843 <div class="peach-settings-row-main">844 <span>Duress password</span>845 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">846 If entered under coercion, shows a decoy empty vault instead of your real one.847 </span>848 </div>849 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>850 {duressEnabled() ? "on" : "off"}851 </div>852 </div>853854 <Show when={duressEnabled() && !showDuressSetup()}>855 <div class="peach-settings-row peach-settings-row--meta">856 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">857 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.858 </span>859 </div>860 </Show>861862 <Show when={duressSuccess()}>863 <div class="peach-settings-row peach-settings-row--meta">864 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>865 </div>866 </Show>867868 <Show when={!showDuressSetup()}>869 <div class="peach-settings-row peach-settings-row--meta">870 <button871 class="peach-btn peach-btn--subtle peach-btn--sm"872 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}873 >874 {duressEnabled() ? "Change / disable" : "Set up"}875 </button>876 </div>877 </Show>878879 <Show when={showDuressSetup()}>880 <div class="peach-duress-form">881 <label class="peach-label">main password (to confirm)</label>882 <input883 class="peach-settings-input"884 type="password"885 placeholder="your current master password"886 autocomplete="current-password"887 value={duressMainPassword()}888 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}889 />890 <Show when={!duressEnabled()}>891 <label class="peach-label">new duress password</label>892 <input893 class="peach-settings-input"894 type="password"895 placeholder="duress password (min 8 chars)"896 autocomplete="new-password"897 value={duressPassword()}898 onInput={(e) => setDuressPassword(e.currentTarget.value)}899 />900 <label class="peach-label">confirm duress password</label>901 <input902 class="peach-settings-input"903 type="password"904 placeholder="confirm duress password"905 autocomplete="new-password"906 value={duressConfirm()}907 onInput={(e) => setDuressConfirm(e.currentTarget.value)}908 />909 </Show>910911 <Show when={duressError()}>912 <p class="peach-hint peach-hint-error">{duressError()}</p>913 </Show>914915 <div class="peach-duress-actions">916 <button917 class="peach-btn peach-btn--subtle peach-btn--sm"918 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}919 >920 cancel921 </button>922 <Show when={duressEnabled()}>923 <button924 class="peach-btn peach-btn--ghost peach-btn--sm"925 style="color:var(--peach-breach)"926 onClick={() => void handleDisableDuress()}927 disabled={duressSaving()}928 >929 {duressSaving() ? "disabling..." : "disable duress"}930 </button>931 </Show>932 <Show when={!duressEnabled()}>933 <button934 class="peach-btn peach-btn--accent peach-btn--sm"935 onClick={() => void handleSetupDuress()}936 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}937 >938 {duressSaving() ? "saving..." : "save"}939 </button>940 </Show>941 </div>942 </div>943 </Show>944945 <p class="peach-settings-sub-label">CANARY ENTRIES</p>946 <div class="peach-settings-row peach-settings-row--top">947 <div class="peach-settings-row-main">948 <span>Canary entries</span>949 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">950 Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.951 </span>952 </div>953 <span class="peach-settings-value" style="flex-shrink:0">954 {canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}955 </span>956 </div>957 <Show when={canaryEntries().length > 0}>958 <For each={canaryEntries()}>959 {(entry) => (960 <div class="peach-settings-row peach-settings-row--meta">961 <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{entry.title}</span>962 <button963 class="peach-btn peach-btn--subtle peach-btn--sm"964 onClick={() => void handleTestCanary(entry)}965 disabled={canaryTestingId() === entry.id}966 style="flex-shrink:0"967 >968 {canaryTestingId() === entry.id969 ? "sending…"970 : canaryTestResult()?.id === entry.id971 ? (canaryTestResult()!.ok ? "sent ✓" : "failed ✗")972 : "test"}973 </button>974 </div>975 )}976 </For>977 </Show>978979 <p class="peach-settings-sub-label">PASSKEYS</p>980 <div class="peach-settings-row peach-settings-row--top">981 <div class="peach-settings-row-main">982 <span>Intercept passkey requests</span>983 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">984 Peach can handle passkey sign-in and creation on supported Chromium browsers.985 </span>986 </div>987 <button988 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}989 onClick={() => void handleTogglePasskeys()}990 aria-label="Toggle passkey interception"991 disabled={!passkeyState()?.supported || passkeySaving()}992 >993 <span class="peach-toggle-knob"></span>994 </button>995 </div>996 <div class="peach-settings-row peach-settings-row--meta">997 <span>Status</span>998 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>999 </div>1000 <div class="peach-settings-row peach-settings-row--meta">1001 <span>Browser</span>1002 <span class="peach-settings-value">1003 {passkeyState()?.browserLabel ?? "Unknown"}1004 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}1005 </span>1006 </div>1007 <div class="peach-info-notice" style="margin-top:8px">1008 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.1009 </div>1010 <Show when={passkeyState()?.conflict}>1011 <div class="peach-settings-row peach-settings-row--meta">1012 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>1013 </div>1014 </Show>1015 </div>1016 </div>1017 );1018 }10191020 function renderVault() {1021 return (1022 <div class="peach-settings-body">1023 <div class="peach-settings-group">1024 <p class="peach-settings-sub-label">ENTRIES</p>1025 <div class="peach-settings-row peach-settings-row--meta">1026 <span>Entries</span>1027 <span class="peach-settings-value">{props.entriesCount}</span>1028 </div>1029 <div class="peach-settings-row peach-settings-row--meta">1030 <span>Duplicates</span>1031 <Show when={dedupDone() !== null} fallback={1032 <button1033 class="peach-btn peach-btn--subtle peach-btn--sm"1034 onClick={() => void handleCountDuplicates()}1035 disabled={dedupRunning()}1036 >1037 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}1038 </button>1039 }>1040 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">1041 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}1042 </span>1043 </Show>1044 </div>1045 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>1046 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">1047 <div class="peach-restore-warning">1048 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.1049 </div>1050 <div style="display:flex;gap:6px">1051 <button1052 class="peach-btn peach-btn--subtle peach-btn--sm"1053 onClick={() => setDedupCount(null)}1054 >1055 cancel1056 </button>1057 <button1058 class="peach-btn peach-btn--accent peach-btn--sm"1059 onClick={() => void handleDeduplicate()}1060 disabled={dedupRunning()}1061 >1062 {dedupRunning() ? "removing..." : "remove duplicates"}1063 </button>1064 </div>1065 </div>1066 </Show>10671068 <p class="peach-settings-sub-label">MAINTENANCE</p>1069 <div class="peach-settings-row peach-settings-row--meta">1070 <span>Trash auto-delete</span>1071 <select1072 class="peach-settings-select"1073 value={String(props.trashRetentionDays)}1074 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}1075 >1076 <option value="7">7 days</option>1077 <option value="30">30 days</option>1078 <option value="90">90 days</option>1079 <option value="0">Never</option>1080 </select>1081 </div>1082 <div class="peach-settings-row peach-settings-row--meta">1083 <span>Vault Health</span>1084 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>1085 open1086 </button>1087 </div>1088 <Show when={declinedCardCount() > 0}>1089 <div class="peach-settings-row peach-settings-row--meta">1090 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>1091 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>1092 Clear1093 </button>1094 </div>1095 </Show>10961097 <p class="peach-settings-sub-label">DATA</p>1098 <div class="peach-settings-row peach-settings-row--meta">1099 <span>Import passwords</span>1100 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>1101 open1102 </button>1103 </div>1104 <div class="peach-settings-row peach-settings-row--meta">1105 <span>Export backup</span>1106 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>1107 open1108 </button>1109 </div>11101111 <p class="peach-settings-sub-label">VAULT HISTORY</p>1112 <Show when={historyLoading()}>1113 <p class="peach-settings-hint">Loading…</p>1114 </Show>1115 <Show when={!historyLoading() && historySnapshots().length === 0}>1116 <p class="peach-settings-hint">1117 No snapshots yet. Peach saves automatically before every change —1118 saves, syncs, and imports.1119 </p>1120 </Show>1121 <Show when={!historyLoading() && historySnapshots().length > 0}>1122 <p class="peach-settings-hint">1123 Restoring locks your vault. Unlock with the password that1124 encrypted that snapshot to confirm.1125 </p>1126 <div class="peach-history-list">1127 <For each={historySnapshots()}>1128 {(snap) => (1129 <div class="peach-history-row">1130 <div class="peach-history-meta">1131 <span class="peach-history-time">1132 {formatHistoryTime(snap.savedAt)}1133 </span>1134 <span class="peach-history-sub">1135 {[1136 snap.entryCount !== null1137 ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1138 : null,1139 reasonLabel(snap.reason),1140 ]1141 .filter(Boolean)1142 .join(" · ")}1143 </span>1144 </div>1145 <button1146 class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1147 historyRestoring() === snap.id ? " is-busy" : ""1148 }`}1149 onClick={() => void handleRestoreVaultSnapshot(snap.id)}1150 disabled={historyRestoring() !== null}1151 >1152 <Show when={historyRestoring() === snap.id} fallback="Restore">1153 …1154 </Show>1155 </button>1156 </div>1157 )}1158 </For>1159 </div>1160 <p class="peach-history-footer">1161 {historySnapshots().length} of 10 snapshots1162 </p>1163 </Show>11641165 <div class="peach-danger-card">1166 <div class="peach-danger-card-head">1167 <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1168 <h3 class="peach-danger-card-title">Replace vault from backup</h3>1169 </div>1170 <p class="peach-danger-card-copy">1171 Wipes the current vault and restores from a backup file.1172 Recoverable only if your backup is good.1173 </p>1174 <button1175 class="peach-danger-card-button"1176 onClick={() => props.onOpenFileImport?.()}1177 >1178 Choose backup file1179 </button>1180 </div>11811182 <Show when={props.onOpenPanic}>1183 <div class="peach-danger-card peach-danger-card--hazard">1184 <div class="peach-danger-card-head">1185 <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1186 <h3 class="peach-danger-card-title">Destroy vault</h3>1187 </div>1188 <p class="peach-danger-card-copy">1189 Permanently wipes every entry, setting, and license on this1190 device. There is no recovery.1191 </p>1192 <button1193 class="peach-danger-card-button"1194 onClick={() => props.onOpenPanic?.()}1195 >1196 Begin destruction1197 </button>1198 </div>1199 </Show>1200 </div>1201 </div>1202 );1203 }12041205 function renderSync() {1206 if (!isPro()) {1207 return (1208 <div class="peach-settings-body">1209 <div class="peach-pro-upgrade">1210 <div class="peach-pro-upgrade-hero">1211 <svg class="peach-sync-hero-icon" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">1212 <circle cx="9" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1213 <circle cx="27" cy="18" r="4.5" stroke="currentColor" stroke-width="1.2" opacity="0.9"/>1214 <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"/>1215 <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"/>1216 <path d="M13.5 18H22.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.35"/>1217 <path d="M20 16L22.5 18L20 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1218 <path d="M16 16L13.5 18L16 20" stroke="var(--peach-accent)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>1219 </svg>1220 <h2 class="peach-pro-upgrade-title">Peach Sync</h2>1221 <p class="peach-pro-upgrade-tagline">Encrypted · peer-to-peer · local</p>1222 </div>12231224 <div class="peach-pro-features">1225 <div class="peach-pro-feature">1226 <span class="peach-pro-feature-mark">✦</span>1227 <div class="peach-pro-feature-text">1228 <span class="peach-pro-feature-name">Always in sync</span>1229 <span class="peach-pro-feature-desc">Your vault stays current across all your devices automatically</span>1230 </div>1231 </div>1232 <div class="peach-pro-feature">1233 <span class="peach-pro-feature-mark">✦</span>1234 <div class="peach-pro-feature-text">1235 <span class="peach-pro-feature-name">No cloud. No relay.</span>1236 <span class="peach-pro-feature-desc">Direct device-to-device on your local network — nothing leaves your hands</span>1237 </div>1238 </div>1239 <div class="peach-pro-feature">1240 <span class="peach-pro-feature-mark">✦</span>1241 <div class="peach-pro-feature-text">1242 <span class="peach-pro-feature-name">Tailscale supported</span>1243 <span class="peach-pro-feature-desc">Sync securely from anywhere over your private Tailscale network</span>1244 </div>1245 </div>1246 <div class="peach-pro-feature">1247 <span class="peach-pro-feature-mark">✦</span>1248 <div class="peach-pro-feature-text">1249 <span class="peach-pro-feature-name">Zero friction after setup</span>1250 <span class="peach-pro-feature-desc">Mark a network as trusted once — sync happens in the background</span>1251 </div>1252 </div>1253 </div>12541255 <button1256 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1257 onClick={() => setActiveSection("license")}1258 >1259 Upgrade to Pro1260 </button>1261 <button class="peach-pro-key-link" onClick={() => setActiveSection("license")}>1262 Already have a key? Enter it →1263 </button>1264 </div>1265 </div>1266 );1267 }12681269 return (1270 <div class="peach-settings-body">1271 <div class="peach-settings-group">1272 <div class="peach-settings-row">1273 <span>Status</span>1274 <span class="peach-settings-value peach-settings-value--status">1275 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>1276 {getStatusLabel(syncStatus())}1277 </span>1278 </div>1279 <div class="peach-settings-row peach-settings-row--meta">1280 <span>Last sync</span>1281 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>1282 </div>1283 <div class="peach-settings-row peach-settings-row--meta">1284 <button1285 class="peach-btn peach-btn--subtle peach-btn--sm"1286 onClick={() => void handleSyncNow()}1287 disabled={syncStatus() === "not_trusted" || syncingNow()}1288 >1289 <Switch>1290 <Match when={syncingNow()}>Syncing…</Match>1291 <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1292 <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1293 <Match when={true}>Sync now</Match>1294 </Switch>1295 </button>1296 </div>1297 <p class="peach-settings-hint">1298 If you edit on multiple devices before syncing, the most recently1299 changed device wins. Use Vault History above to recover older1300 versions if needed.1301 </p>13021303 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1304 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1305 {pairedDevices().map((device) => (1306 <div class="peach-sync-device-row">1307 <div class="peach-sync-device-info">1308 <span class="peach-sync-device-name">{device.displayName}</span>1309 <span class="peach-sync-device-meta">1310 Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}1311 </span>1312 </div>1313 <button1314 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1315 onClick={() => void handleRemoveDevice(device.deviceId)}1316 >1317 Remove1318 </button>1319 </div>1320 ))}1321 <div class="peach-settings-actions--row">1322 <button1323 class="peach-btn peach-btn--subtle peach-btn--sm"1324 onClick={() => props.onOpenSyncPairing?.("initiate")}1325 >1326 Pair a new device1327 </button>1328 <button1329 class="peach-btn peach-btn--subtle peach-btn--sm"1330 onClick={() => props.onOpenSyncPairing?.("accept")}1331 >1332 Pair with another device1333 </button>1334 </div>1335 </div>13361337 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1338 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1339 {trustedNetworks().filter(n => !n.isTailscale).map((network) => (1340 <div class="peach-sync-device-row">1341 <span class="peach-sync-device-name">{network.ssid ?? "Unknown network"}</span>1342 <button1343 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1344 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1345 >1346 Remove1347 </button>1348 </div>1349 ))}1350 <div class="peach-settings-actions--row">1351 <Show when={!isCurrentNetworkTrusted()}>1352 <button1353 class="peach-btn peach-btn--subtle peach-btn--sm"1354 onClick={() => void handleMarkNetworkTrusted()}1355 >1356 Mark current network as trusted1357 </button>1358 </Show>1359 </div>1360 <span class="peach-sync-device-meta">1361 Tailscale is always trusted1362 </span>1363 </div>13641365 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1366 <span class="peach-sync-section-label">Advanced</span>1367 <Show1368 when={resetIdentityConfirm()}1369 fallback={1370 <div class="peach-sync-advanced-row">1371 <div class="peach-sync-advanced-info">1372 <span class="peach-sync-advanced-name">Reset sync identity</span>1373 <span class="peach-sync-device-meta">1374 Generates a new keypair and unpairs every device.1375 </span>1376 </div>1377 <button1378 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1379 onClick={() => setResetIdentityConfirm(true)}1380 >1381 Reset1382 </button>1383 </div>1384 }1385 >1386 <div class="peach-sync-reset-confirm">1387 <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1388 <p class="peach-sync-reset-confirm-body">1389 This will unpair all devices. You will need to pair them again from scratch.1390 </p>1391 <div class="peach-settings-actions--row">1392 <button1393 class="peach-btn peach-btn--ghost peach-btn--sm"1394 onClick={() => setResetIdentityConfirm(false)}1395 disabled={resetIdentityRunning()}1396 >1397 Cancel1398 </button>1399 <button1400 class="peach-btn peach-btn--danger peach-btn--sm"1401 onClick={() => void handleResetSyncIdentity()}1402 disabled={resetIdentityRunning()}1403 >1404 {resetIdentityRunning() ? "Resetting..." : "Reset"}1405 </button>1406 </div>1407 </div>1408 </Show>1409 </div>1410 </div>1411 </div>1412 );1413 }14141415 function renderAppearance() {1416 const fontOptions: Array<{ id: "cormorant-italic" | "cormorant" | "inter" | "system"; label: string; preview: string }> = [1417 { id: "cormorant-italic", label: "Cormorant Italic", preview: "Aa" },1418 { id: "cormorant", label: "Cormorant", preview: "Aa" },1419 { id: "inter", label: "Inter", preview: "Aa" },1420 { id: "system", label: "System", preview: "Aa" },1421 ];1422 const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [1423 { id: "small", label: "Small" },1424 { id: "medium", label: "Default" },1425 { id: "large", label: "Large" },1426 ];1427 return (1428 <div class="peach-settings-body">1429 <div class="peach-settings-group">1430 <p class="peach-settings-sub-label">THEME</p>1431 <div class="peach-settings-row peach-settings-row--meta">1432 <span>Theme</span>1433 <button1434 class="peach-btn peach-btn--subtle peach-btn--sm"1435 onClick={() =>1436 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1437 }1438 >1439 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1440 </button>1441 </div>14421443 <p class="peach-settings-sub-label">ACCENT</p>1444 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1445 <span>Accent</span>1446 <div class="peach-accent-picker">1447 <For each={ACCENT_COLORS}>1448 {(color) => (1449 <button1450 class="peach-accent-swatch"1451 classList={{ "is-active": props.accentColor === color.name }}1452 style={{ background: color.hex }}1453 onClick={() => void props.onAccentChange(color.name)}1454 aria-label={color.name}1455 title={color.name}1456 />1457 )}1458 </For>1459 </div>1460 </div>14611462 <p class="peach-settings-sub-label">DISPLAY FONT</p>1463 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--font">1464 <span>Font</span>1465 <div class="peach-font-picker">1466 <For each={fontOptions}>1467 {(opt) => (1468 <button1469 class="peach-font-swatch"1470 classList={{ "is-active": props.displayFont === opt.id }}1471 data-font-preview={opt.id}1472 onClick={() => void props.onDisplayFontChange(opt.id)}1473 aria-label={opt.label}1474 title={opt.label}1475 >1476 <span class="peach-font-swatch-glyph">{opt.preview}</span>1477 <span class="peach-font-swatch-name">{opt.label}</span>1478 </button>1479 )}1480 </For>1481 </div>1482 </div>14831484 <p class="peach-settings-sub-label">TEXT SIZE</p>1485 <div class="peach-settings-row peach-settings-row--meta">1486 <span>Size</span>1487 <div class="peach-pill-group">1488 <For each={sizeOptions}>1489 {(opt) => (1490 <button1491 class="peach-btn peach-btn--ghost peach-btn--sm"1492 classList={{ "is-active": props.textSize === opt.id }}1493 onClick={() => void props.onTextSizeChange(opt.id)}1494 >1495 {opt.label}1496 </button>1497 )}1498 </For>1499 </div>1500 </div>15011502 <p class="peach-settings-sub-label">MOTION</p>1503 <div class="peach-settings-row peach-settings-row--meta">1504 <span>Reduce motion</span>1505 <button1506 class="peach-btn peach-btn--subtle peach-btn--sm"1507 onClick={() => void props.onReduceMotionChange(!props.reduceMotion)}1508 >1509 {props.reduceMotion ? "On" : "Off"}1510 </button>1511 </div>1512 </div>1513 </div>1514 );1515 }15161517 function renderShortcuts() {1518 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1519 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);15201521 function startCapture(action: ShortcutAction) {1522 setConflictMsg(null);1523 setCapturingAction(action);1524 }1525 function cancelCapture() {1526 setCapturingAction(null);1527 setConflictMsg(null);1528 }1529 function commitBinding(action: ShortcutAction, binding: Keybinding) {1530 const conflict = findConflict(props.shortcuts, binding, action);1531 if (conflict) {1532 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1533 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1534 return;1535 }1536 const next = { ...props.shortcuts, [action]: binding };1537 setCapturingAction(null);1538 setConflictMsg(null);1539 void props.onShortcutsChange(next);1540 }1541 function resetAction(action: ShortcutAction) {1542 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1543 void props.onShortcutsChange(next);1544 }1545 function resetAll() {1546 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1547 }15481549 1550 1551 function handleCaptureKey(e: KeyboardEvent) {1552 e.preventDefault();1553 e.stopPropagation();1554 if (e.key === "Escape") { cancelCapture(); return; }1555 const binding = keybindingFromEvent(e);1556 if (!binding) return; 1557 const action = capturingAction();1558 if (action) commitBinding(action, binding);1559 }15601561 return (1562 <div class="peach-settings-body">1563 <div class="peach-settings-group peach-shortcuts-group">1564 <p class="peach-settings-sub-label">ENTRY VIEW</p>1565 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1566 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1567 </For>1568 </div>15691570 <div class="peach-settings-group peach-shortcuts-group">1571 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1572 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1573 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1574 </For>1575 </div>15761577 <Show when={conflictMsg()}>1578 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1579 </Show>15801581 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1582 <div class="peach-settings-row peach-settings-row--meta">1583 <span>Inline autofill on page</span>1584 <a1585 class="peach-settings-link"1586 href="chrome://extensions/shortcuts"1587 onClick={(e) => {1588 e.preventDefault();1589 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1590 }}1591 >1592 ⌘⇧P — customize ↗1593 </a>1594 </div>1595 <button1596 class="peach-btn peach-btn--subtle peach-btn--sm"1597 style="align-self:flex-start"1598 onClick={resetAll}1599 >1600 Reset all to defaults1601 </button>1602 </div>1603 </div>1604 );16051606 function renderRow(1607 desc: typeof SHORTCUT_DESCRIPTORS[number],1608 capturing: () => ShortcutAction | null,1609 onStart: (a: ShortcutAction) => void,1610 onCancel: () => void,1611 onReset: (a: ShortcutAction) => void,1612 onKey: (e: KeyboardEvent) => void,1613 ) {1614 const isActive = () => capturing() === desc.action;1615 const binding = () => props.shortcuts[desc.action];1616 const isCustom = () => {1617 const b = binding();1618 const d = DEFAULT_BINDINGS[desc.action];1619 return b.key !== d.key1620 || Boolean(b.ctrl) !== Boolean(d.ctrl)1621 || Boolean(b.meta) !== Boolean(d.meta)1622 || Boolean(b.shift) !== Boolean(d.shift)1623 || Boolean(b.alt) !== Boolean(d.alt);1624 };1625 return (1626 <div class="peach-shortcut-row">1627 <span class="peach-shortcut-row-label">{desc.label}</span>1628 <button1629 class="peach-shortcut-chip"1630 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1631 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1632 onKeyDown={(e) => { if (isActive()) onKey(e); }}1633 ref={(el) => {1634 1635 if (el) {1636 queueMicrotask(() => { if (isActive()) el.focus(); });1637 }1638 }}1639 >1640 {isActive() ? "Press a key…" : formatKeybinding(binding())}1641 </button>1642 <Show when={isCustom() && !isActive()}>1643 <button1644 class="peach-shortcut-reset"1645 title="Reset to default"1646 onClick={() => onReset(desc.action)}1647 >1648 ↺1649 </button>1650 </Show>1651 </div>1652 );1653 }1654 }16551656 const PRO_FEATURES: Array<{1657 feature: ProFeature;1658 name: string;1659 desc: string;1660 }> = [1661 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1662 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1663 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1664 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1665 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1666 ];16671668 const [licenseKey, setLicenseKey] = createSignal<string | null>(null);16691670 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1671 const [deactivating, setDeactivating] = createSignal(false);16721673 async function handleDeactivateLicense() {1674 setDeactivating(true);1675 await sendMessage({ type: "DEACTIVATE_LICENSE" });1676 setLicenseInfo(null);1677 await props.onLicenseChange?.();1678 setDeactivating(false);1679 }16801681 async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) {1682 setCanaryTestingId(entry.id);1683 setCanaryTestResult(null);1684 const res = await sendMessage({1685 type: "FIRE_CANARY_PING",1686 payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" },1687 }) as { ok: boolean };1688 setCanaryTestResult({ id: entry.id, ok: res.ok });1689 setCanaryTestingId(null);1690 setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000);1691 }16921693 function renderLicense() {1694 async function onActivate() {1695 await handleActivateLicense();1696 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1697 await props.onLicenseChange?.();1698 }1699 }17001701 return (1702 <div class="peach-settings-body">1703 <Show1704 when={isPro()}1705 fallback={1706 <Show1707 when={showKeyEntry()}1708 fallback={1709 <div class="peach-pro-upgrade">1710 <div class="peach-pro-upgrade-hero">1711 <div class="peach-pro-upgrade-mark-wrap">1712 <PeachMark class="peach-pro-upgrade-mark" />1713 </div>1714 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1715 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1716 </div>17171718 <div class="peach-pro-features">1719 <For each={PRO_FEATURES}>1720 {(item) => (1721 <div1722 class="peach-pro-feature"1723 classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}1724 >1725 <span class="peach-pro-feature-mark">✦</span>1726 <div class="peach-pro-feature-text">1727 <span class="peach-pro-feature-name">{item.name}</span>1728 <span class="peach-pro-feature-desc">{item.desc}</span>1729 </div>1730 </div>1731 )}1732 </For>1733 </div>17341735 <div class="peach-pro-upgrade-actions">1736 <p class="peach-pro-pricing">$39 · once · yours forever</p>1737 <a1738 href="https://peachpasswords.com/#pricing"1739 target="_blank"1740 rel="noopener noreferrer"1741 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1742 >1743 Get Peach Pro1744 </a>1745 <div class="peach-pro-key-divider"><span>or</span></div>1746 <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1747 Activate a license key1748 </button>1749 </div>1750 </div>1751 }1752 >1753 <div class="peach-pro-key-ceremony">1754 <LockRuneCanvas />1755 <p class="peach-pro-key-ceremony-label">Enter your license key</p>1756 <input1757 class="peach-underline-input peach-pro-key-input"1758 type="text"1759 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1760 value={licenseKeyInput()}1761 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1762 onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1763 autocomplete="off"1764 spellcheck={false}1765 />1766 <Show when={licenseError()}>1767 <p class="peach-pro-key-error">That key doesn't look right.</p>1768 </Show>1769 <button1770 class="peach-btn peach-btn--accent peach-pro-key-activate"1771 onClick={() => void onActivate()}1772 disabled={activatingLicense() || !licenseKeyInput().trim()}1773 >1774 {activatingLicense() ? "activating…" : "Activate"}1775 </button>1776 <button1777 class="peach-pro-key-cancel"1778 onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1779 >1780 cancel1781 </button>1782 </div>1783 </Show>1784 }1785 >1786 <div class="peach-pro-active">1787 <div class="peach-pro-active-hero">1788 <div class="peach-pro-active-mark-wrap">1789 <PeachMark class="peach-pro-active-mark" />1790 </div>1791 <h2 class="peach-pro-active-title">Peach Pro</h2>1792 <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1793 </div>17941795 <Show when={licenseKey()}>1796 <div class="peach-settings-license">1797 <span class="peach-settings-label">LICENSE KEY</span>1798 <div class="peach-settings-license-row">1799 <span class="peach-settings-license-key">{licenseKey()!}</span>1800 <button1801 class="peach-settings-license-copy"1802 onClick={() => copyLicenseKey(licenseKey()!)}1803 >1804 {copied() ? "✓" : "copy"}1805 </button>1806 </div>1807 </div>1808 </Show>18091810 <div class="peach-pro-active-features">1811 <For each={PRO_FEATURES}>1812 {(item) => (1813 <div class="peach-pro-active-feature">1814 <span class="peach-pro-active-feature-mark">✦</span>1815 <span class="peach-pro-active-feature-name">{item.name}</span>1816 </div>1817 )}1818 </For>1819 </div>18201821 <button1822 class="peach-pro-deactivate"1823 onClick={() => void handleDeactivateLicense()}1824 disabled={deactivating()}1825 >1826 {deactivating() ? "Deactivating…" : "Deactivate"}1827 </button>1828 </div>1829 </Show>1830 </div>1831 );1832 }18331834 function renderFeedback() {1835 return (1836 <div class="peach-settings-body">1837 <div class="peach-settings-group peach-feedback-group">1838 <p class="peach-feedback-intro">1839 Help us improve Peach. Every report goes directly to our inbox.1840 </p>18411842 <p class="peach-settings-sub-label">TYPE</p>1843 <div class="peach-feedback-chips">1844 <For each={FEEDBACK_CATEGORIES}>1845 {(cat) => (1846 <button1847 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1848 onClick={() => setFbCategory(cat)}1849 >1850 {cat}1851 </button>1852 )}1853 </For>1854 </div>18551856 <p class="peach-settings-sub-label">MESSAGE</p>1857 <textarea1858 class="peach-feedback-textarea"1859 placeholder="Describe the issue or idea..."1860 value={fbMessage()}1861 onInput={(e) => setFbMessage(e.currentTarget.value)}1862 />18631864 <p class="peach-feedback-meta">1865 Version {version} · will be included automatically1866 </p>18671868 <Show when={fbResult() === "success"}>1869 <p class="peach-feedback-result peach-feedback-result--ok">1870 Sent — thank you1871 </p>1872 </Show>1873 <Show when={fbResult() === "error"}>1874 <p class="peach-feedback-result peach-feedback-result--err">1875 Failed to send — try again1876 </p>1877 </Show>18781879 <button1880 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1881 onClick={() => void handleSubmitFeedback()}1882 disabled={fbSubmitting() || !fbMessage().trim()}1883 >1884 {fbSubmitting() ? "Sending..." : "Send feedback"}1885 </button>18861887 <p class="peach-feedback-anon">1888 Sent anonymously · no account required1889 </p>1890 </div>1891 </div>1892 );1893 }18941895 function renderMobile() {1896 return (1897 <div class="peach-settings-body">1898 <div class="peach-settings-group">1899 <p class="peach-settings-label">Autofill service</p>1900 <div class="peach-mobile-row">1901 <span class="peach-mobile-status">1902 {autofillEnabled() ? "Enabled" : "Disabled"}1903 </span>1904 <button1905 class="peach-btn peach-btn--subtle peach-btn--sm"1906 onClick={() => void AutofillPlugin?.openAutofillSettings()}1907 >1908 {autofillEnabled() ? "Change" : "Enable"}1909 </button>1910 </div>1911 </div>19121913 <div class="peach-settings-group">1914 <p class="peach-settings-label">Biometric unlock</p>1915 <div class="peach-mobile-row">1916 <span class="peach-mobile-status">1917 {biometricEnabled() ? "Available" : "Not available"}1918 </span>1919 </div>1920 </div>1921 </div>1922 );1923 }19241925 return (1926 <div class="peach-overlay-view ov-enter-right" style="display:flex">1927 <div class="peach-overlay-header">1928 <button1929 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1930 onClick={handleHeaderBack}1931 aria-label="Back"1932 >1933 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1934 <path d="M6 1L1 6L6 11" />1935 </svg>1936 </button>1937 <span class="peach-overlay-title">{currentTitle()}</span>1938 <div class="peach-overlay-spacer"></div>1939 </div>19401941 <Switch fallback={renderHome()}>1942 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1943 <Match when={activeSection() === "vault"}>{renderVault()}</Match>1944 <Match when={activeSection() === "sync"}>{renderSync()}</Match>1945 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1946 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1947 <Match when={activeSection() === "license"}>{renderLicense()}</Match>1948 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1949 <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>1950 </Switch>1951 </div>1952 );1953}