1import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2import { sendMessage } from "../App";3import { ACCENT_COLORS } from "../../shared/types";4import type { AccentColorName, PasskeyProviderState } from "../../shared/types";5import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types";6import {7 DEFAULT_BINDINGS,8 findConflict,9 formatKeybinding,10 keybindingFromEvent,11 SHORTCUT_DESCRIPTORS,12 type Keybinding,13 type ShortcutAction,14} from "../../shared/shortcuts";1516interface LicenseInfo {17 valid: boolean;18 plan: "free" | "pro";19 issued?: string;20}2122interface SettingsProps {23 entriesCount: number;24 breachMonitoring: boolean;25 lockOnClose: boolean;26 autoLockMinutes: number;27 trashRetentionDays: number;28 theme: "dark" | "light";29 accentColor: AccentColorName;30 onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;31 onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;32 onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;33 onTrashRetentionChange: (days: number) => void | Promise<void>;34 onThemeChange: (theme: "dark" | "light") => void;35 onAccentChange: (name: AccentColorName) => void | Promise<void>;36 shortcuts: Record<ShortcutAction, Keybinding>;37 onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;38 onBack: () => void;39 onImport: () => void;40 onExport: () => void;41 onBreachCheckDone: () => void;42 onOpenHealth?: () => void;43 onOpenSyncPairing?: (mode: "initiate" | "accept") => void;44 onOpenCJKImport?: () => void;45 onOpenFileImport?: () => void;46}4748type SectionName =49 | "security"50 | "vault"51 | "sync"52 | "appearance"53 | "shortcuts"54 | "license"55 | "feedback";5657const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];5859export function SettingsView(props: SettingsProps) {60 const version = chrome.runtime.getManifest().version;61 const [activeSection, setActiveSection] = createSignal<SectionName | null>(null);6263 const [checkingBreaches, setCheckingBreaches] = createSignal(false);64 const [breachResult, setBreachResult] = createSignal<string | null>(null);65 const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);66 const [licenseKeyInput, setLicenseKeyInput] = createSignal("");67 const [licenseError, setLicenseError] = createSignal<string | null>(null);68 const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);69 const [activatingLicense, setActivatingLicense] = createSignal(false);7071 const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);72 const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);73 const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");74 const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);75 const [isPro, setIsPro] = createSignal(false);76 const [syncingNow, setSyncingNow] = createSignal(false);77 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);78 const [passkeySaving, setPasskeySaving] = createSignal(false);7980 const [dedupCount, setDedupCount] = createSignal<number | null>(null);81 const [dedupRunning, setDedupRunning] = createSignal(false);82 const [dedupDone, setDedupDone] = createSignal<number | null>(null);8384 const [declinedCardCount, setDeclinedCardCount] = createSignal(0);8586 const [showChangePw, setShowChangePw] = createSignal(false);87 const [changePwCurrent, setChangePwCurrent] = createSignal("");88 const [changePwNew, setChangePwNew] = createSignal("");89 const [changePwConfirm, setChangePwConfirm] = createSignal("");90 const [changePwSaving, setChangePwSaving] = createSignal(false);91 const [changePwError, setChangePwError] = createSignal<string | null>(null);92 const [changePwSuccess, setChangePwSuccess] = createSignal(false);9394 const [duressEnabled, setDuressEnabled] = createSignal(false);95 const [showDuressSetup, setShowDuressSetup] = createSignal(false);96 const [duressPassword, setDuressPassword] = createSignal("");97 const [duressConfirm, setDuressConfirm] = createSignal("");98 const [duressMainPassword, setDuressMainPassword] = createSignal("");99 const [duressSaving, setDuressSaving] = createSignal(false);100 const [duressError, setDuressError] = createSignal<string | null>(null);101 const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);102103 const [fbCategory, setFbCategory] = createSignal("Bug report");104 const [fbMessage, setFbMessage] = createSignal("");105 const [fbSubmitting, setFbSubmitting] = createSignal(false);106 const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);107108 onMount(async () => {109 const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes] = await Promise.all([110 sendMessage({ type: "GET_LICENSE" }),111 sendMessage({ type: "GET_SYNC_STATE" }),112 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),113 sendMessage({ type: "IS_DURESS_ENABLED" }),114 sendMessage({ type: "GET_SETTINGS" }),115 ]);116117 if ((licenseRes as { ok: boolean }).ok) {118 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);119 }120121 if ((syncRes as { ok: boolean }).ok) {122 const data = syncRes as { ok: boolean; data?: {123 pairedDevices?: PairedDevice[];124 trustedNetworks?: TrustedNetwork[];125 status?: SyncStatus;126 lastSyncAt?: number;127 isPro?: boolean;128 }};129 if (data.data) {130 setPairedDevices(data.data.pairedDevices ?? []);131 setTrustedNetworks(data.data.trustedNetworks ?? []);132 setSyncStatus(data.data.status ?? "offline");133 setLastSyncAt(data.data.lastSyncAt);134 setIsPro(data.data.isPro ?? false);135 }136 }137138 if ((passkeyRes as { ok: boolean }).ok) {139 setPasskeyState(140 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null141 );142 }143144 if ((duressRes as { ok: boolean }).ok) {145 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);146 }147148 if ((settingsRes as { ok: boolean }).ok) {149 const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;150 setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);151 }152153 chrome.runtime.onMessage.addListener((msg) => {154 if (msg.type === "SYNC_STATE_UPDATE") {155 setPairedDevices(msg.pairedDevices ?? []);156 setTrustedNetworks(msg.trustedNetworks ?? []);157 setSyncStatus(msg.status ?? "offline");158 setLastSyncAt(msg.lastSyncAt);159 setIsPro(msg.isPro ?? false);160 }161 });162 });163164 async function handleChangePassword() {165 setChangePwError(null);166 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }167 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }168 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }169 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }170 setChangePwSaving(true);171 const res = await sendMessage({172 type: "CHANGE_PASSWORD",173 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },174 }) as { ok: boolean; error?: string };175 if (res.ok) {176 setChangePwSuccess(true);177 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");178 setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);179 } else {180 setChangePwError(res.error ?? "failed to change password");181 }182 setChangePwSaving(false);183 }184185 async function handleActivateLicense() {186 const key = licenseKeyInput().trim();187 if (!key) return;188 setActivatingLicense(true);189 setLicenseError(null);190 setLicenseSuccess(null);191 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });192 setActivatingLicense(false);193 if ((res as { ok: boolean }).ok) {194 const data = (res as { ok: boolean; data?: LicenseInfo }).data;195 if (data?.valid) {196 setLicenseInfo(data);197 setLicenseSuccess("License activated!");198 setLicenseKeyInput("");199 } else {200 setLicenseError("Invalid license key");201 }202 } else {203 setLicenseError("Activation failed");204 }205 }206207 async function handleTogglePasskeys() {208 const current = passkeyState();209 if (!current || !current.supported) return;210 setPasskeySaving(true);211 const res = await sendMessage({212 type: "SET_PASSKEY_PROVIDER_ENABLED",213 payload: { enabled: !current.enabled },214 });215 setPasskeySaving(false);216 if ((res as { ok: boolean }).ok) {217 setPasskeyState(218 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null219 );220 }221 }222223 async function handleCheckAllBreaches() {224 setCheckingBreaches(true);225 setBreachResult(null);226 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });227 setCheckingBreaches(false);228 if ((res as { ok: boolean }).ok) {229 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};230 const breached = Object.values(data).filter((count) => count > 0).length;231 setBreachResult(232 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"233 );234 props.onBreachCheckDone();235 }236 }237238 async function handleSyncNow() {239 if (syncStatus() === "not_trusted") return;240 setSyncingNow(true);241 await sendMessage({ type: "TRIGGER_SYNC" });242 setTimeout(() => setSyncingNow(false), 3000);243 }244245 async function handleRemoveDevice(deviceId: string) {246 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });247 }248249 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {250 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });251 }252253 async function handleMarkNetworkTrusted() {254 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });255 }256257 function formatRelativeTime(timestamp?: number): string {258 if (!timestamp) return "Never";259 const diff = Date.now() - timestamp;260 const minutes = Math.floor(diff / 60000);261 const hours = Math.floor(diff / 3600000);262 const days = Math.floor(diff / 86400000);263 if (minutes < 1) return "Just now";264 if (minutes < 60) return `${minutes}m ago`;265 if (hours < 24) return `${hours}h ago`;266 return `${days}d ago`;267 }268269 function getStatusColor(status: SyncStatus): string {270 switch (status) {271 case "synced": return "var(--peach-sync-ok)";272 case "syncing": return "var(--peach-sync-busy)";273 case "no_peers": return "var(--peach-sync-off)";274 case "not_trusted": return "var(--peach-sync-err)";275 case "offline": return "var(--peach-sync-off)";276 default: return "var(--peach-sync-off)";277 }278 }279280 function getStatusLabel(status: SyncStatus): string {281 switch (status) {282 case "synced": return "Synced";283 case "syncing": return "Syncing";284 case "no_peers": return "No peers visible";285 case "not_trusted": return "Not on a trusted network";286 case "offline": return "Offline";287 default: return "Unknown";288 }289 }290291 function isCurrentNetworkTrusted(): boolean {292 return trustedNetworks().length > 0;293 }294295 async function handleSetupDuress() {296 const mainPw = duressMainPassword().trim();297 const duressPw = duressPassword().trim();298 const confirmPw = duressConfirm().trim();299300 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }301 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }302 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }303 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }304305 setDuressSaving(true);306 setDuressError(null);307308 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };309 if (!verifyRes.ok || !verifyRes.data) {310 setDuressError("incorrect main password");311 setDuressSaving(false);312 return;313 }314315 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };316 setDuressSaving(false);317 if (res.ok) {318 setDuressEnabled(true);319 setShowDuressSetup(false);320 setDuressPassword("");321 setDuressConfirm("");322 setDuressMainPassword("");323 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");324 setTimeout(() => setDuressSuccess(null), 5000);325 } else {326 setDuressError(res.error ?? "failed to set duress password");327 }328 }329330 async function handleClearDeclinedCards() {331 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });332 setDeclinedCardCount(0);333 }334335 async function handleCountDuplicates() {336 setDedupRunning(true);337 setDedupDone(null);338 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };339 setDedupRunning(false);340 if (res.ok) setDedupCount(res.data ?? 0);341 }342343 async function handleDeduplicate() {344 setDedupRunning(true);345 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };346 setDedupRunning(false);347 if (res.ok) {348 setDedupDone(res.data ?? 0);349 setDedupCount(0);350 }351 }352353 async function handleDisableDuress() {354 const mainPw = duressMainPassword().trim();355 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }356357 setDuressSaving(true);358 setDuressError(null);359360 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };361 if (!verifyRes.ok || !verifyRes.data) {362 setDuressError("incorrect main password");363 setDuressSaving(false);364 return;365 }366367 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };368 setDuressSaving(false);369 if (res.ok) {370 setDuressEnabled(false);371 setShowDuressSetup(false);372 setDuressMainPassword("");373 setDuressSuccess("Duress password disabled.");374 setTimeout(() => setDuressSuccess(null), 4000);375 } else {376 setDuressError(res.error ?? "failed to disable duress password");377 }378 }379380 async function handleSubmitFeedback() {381 if (!fbMessage().trim() || fbSubmitting()) return;382 setFbSubmitting(true);383 setFbResult(null);384 const res = await sendMessage({385 type: "SUBMIT_FEEDBACK",386 payload: {387 category: fbCategory(),388 message: fbMessage().trim(),389 version,390 browser: navigator.userAgent,391 },392 }) as { ok: boolean; error?: string };393 setFbSubmitting(false);394 if (res.ok) {395 setFbResult("success");396 setFbMessage("");397 setTimeout(() => setFbResult(null), 4000);398 } else {399 setFbResult("error");400 }401 }402403 function handleHeaderBack() {404 if (activeSection() !== null) {405 setActiveSection(null);406 } else {407 props.onBack();408 }409 }410411 function currentTitle(): string {412 const s = activeSection();413 if (s === null) return "settings";414 if (s === "license") return "account";415 return s;416 }417418 function renderCatRow(opts: {419 section: SectionName;420 name: string;421 desc: string;422 icon: () => any;423 pro?: boolean;424 }) {425 return (426 <button427 class="peach-settings-cat-row"428 onClick={() => setActiveSection(opts.section)}429 >430 <div class="peach-settings-cat-icon">{opts.icon()}</div>431 <div class="peach-settings-cat-content">432 <span class="peach-settings-cat-name">433 {opts.name}434 <Show when={opts.pro && !isPro()}>435 <span class="peach-sync-pro-badge" style="margin-left:6px">Pro</span>436 </Show>437 </span>438 <span class="peach-settings-cat-desc">{opts.desc}</span>439 </div>440 <svg441 class="peach-settings-cat-chevron"442 width="6" height="10" viewBox="0 0 6 10" fill="none"443 stroke="currentColor" stroke-width="1.1"444 stroke-linecap="round" stroke-linejoin="round"445 >446 <path d="M1 1l4 4-4 4" />447 </svg>448 </button>449 );450 }451452 const iconShield = () => (453 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">454 <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>455 </svg>456 );457 const iconKey = () => (458 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">459 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>460 </svg>461 );462 const iconLock = () => (463 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">464 <rect x="3" y="7" width="10" height="7" rx="1.5"/>465 <path d="M6 7V5a3 3 0 016 0v2"/>466 </svg>467 );468 const iconSync = () => (469 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">470 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>471 <path d="M14 6l-2 2-2-2"/>472 <path d="M2 10l2-2 2 2"/>473 </svg>474 );475 const iconSun = () => (476 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">477 <circle cx="8" cy="8" r="3"/>478 <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"/>479 </svg>480 );481 const iconKeys = () => (482 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">483 <rect x="2" y="5" width="5" height="4" rx="1"/>484 <rect x="9" y="7" width="5" height="4" rx="1"/>485 <path d="M7 7h2m-2 2h2"/>486 </svg>487 );488 const iconBubble = () => (489 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">490 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>491 </svg>492 );493494 function renderHome() {495 return (496 <div class="peach-settings-body">497 <p class="peach-settings-nav-label">PROTECTION</p>498 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}499 {renderCatRow({ section: "license", name: "Account", desc: "Plan & license key", icon: iconKey })}500501 <p class="peach-settings-nav-label">VAULT</p>502 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}503 {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}504505 <p class="peach-settings-nav-label">PREFERENCES</p>506 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}507 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}508509 <p class="peach-settings-nav-label">SUPPORT</p>510 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}511512 <div class="peach-settings-footer">513 <span>Peach {version}</span>514 <span class="peach-divider"></span>515 <span>Browser extension</span>516 </div>517 </div>518 );519 }520521 function renderSecurity() {522 return (523 <div class="peach-settings-body">524 <div class="peach-settings-group">525 <p class="peach-settings-sub-label">LOCKING</p>526 <div class="peach-settings-row">527 <span>Lock on close</span>528 <button529 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}530 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}531 aria-label="Toggle lock on close"532 >533 <span class="peach-toggle-knob"></span>534 </button>535 </div>536 <div class="peach-settings-row">537 <span>Auto-lock after</span>538 <select539 class="peach-settings-select"540 value={String(props.autoLockMinutes)}541 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}542 >543 <option value="1">1 minute</option>544 <option value="5">5 minutes</option>545 <option value="15">15 minutes</option>546 <option value="30">30 minutes</option>547 <option value="0">Never</option>548 </select>549 </div>550551 <p class="peach-settings-sub-label">BREACH PROTECTION</p>552 <div class="peach-settings-row">553 <span>Breach monitoring</span>554 <button555 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}556 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}557 aria-label="Toggle breach monitoring"558 >559 <span class="peach-toggle-knob"></span>560 </button>561 </div>562 <div class="peach-settings-row peach-settings-row--meta">563 <span>Check all passwords</span>564 <button565 class="peach-btn peach-btn--subtle peach-btn--sm"566 onClick={() => void handleCheckAllBreaches()}567 disabled={checkingBreaches()}568 >569 {checkingBreaches() ? "checking..." : "run"}570 </button>571 </div>572 <Show when={breachResult()}>573 <div class="peach-settings-row peach-settings-row--meta">574 <span>Last check</span>575 <span class="peach-settings-value">{breachResult()}</span>576 </div>577 </Show>578579 <p class="peach-settings-sub-label">MASTER PASSWORD</p>580 <div class="peach-settings-row peach-settings-row--meta">581 <span>Master password</span>582 <button583 class="peach-btn peach-btn--subtle peach-btn--sm"584 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}585 >586 {showChangePw() ? "cancel" : "change"}587 </button>588 </div>589 <Show when={showChangePw()}>590 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">591 <input592 class="peach-settings-input"593 type="password"594 placeholder="current password"595 value={changePwCurrent()}596 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}597 autocomplete="current-password"598 />599 <input600 class="peach-settings-input"601 type="password"602 placeholder="new password (min 12 chars)"603 value={changePwNew()}604 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}605 autocomplete="new-password"606 />607 <input608 class="peach-settings-input"609 type="password"610 placeholder="confirm new password"611 value={changePwConfirm()}612 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}613 autocomplete="new-password"614 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}615 />616 <Show when={changePwError()}>617 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>618 </Show>619 <Show when={changePwSuccess()}>620 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>621 </Show>622 <button623 class="peach-btn peach-btn--accent peach-btn--sm"624 style="align-self:flex-start"625 onClick={() => void handleChangePassword()}626 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}627 >628 {changePwSaving() ? "saving…" : "save new password"}629 </button>630 </div>631 </Show>632633 <p class="peach-settings-sub-label">DURESS PASSWORD</p>634 <div class="peach-settings-row peach-settings-row--top">635 <div class="peach-settings-row-main">636 <span>Duress password</span>637 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">638 If entered under coercion, shows a decoy empty vault instead of your real one.639 </span>640 </div>641 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>642 {duressEnabled() ? "on" : "off"}643 </div>644 </div>645646 <Show when={duressEnabled() && !showDuressSetup()}>647 <div class="peach-settings-row peach-settings-row--meta">648 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">649 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.650 </span>651 </div>652 </Show>653654 <Show when={duressSuccess()}>655 <div class="peach-settings-row peach-settings-row--meta">656 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>657 </div>658 </Show>659660 <Show when={!showDuressSetup()}>661 <div class="peach-settings-row peach-settings-row--meta">662 <button663 class="peach-btn peach-btn--subtle peach-btn--sm"664 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}665 >666 {duressEnabled() ? "Change / disable" : "Set up"}667 </button>668 </div>669 </Show>670671 <Show when={showDuressSetup()}>672 <div class="peach-duress-form">673 <label class="peach-label">main password (to confirm)</label>674 <input675 class="peach-settings-input"676 type="password"677 placeholder="your current master password"678 autocomplete="current-password"679 value={duressMainPassword()}680 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}681 />682 <Show when={!duressEnabled()}>683 <label class="peach-label">new duress password</label>684 <input685 class="peach-settings-input"686 type="password"687 placeholder="duress password (min 8 chars)"688 autocomplete="new-password"689 value={duressPassword()}690 onInput={(e) => setDuressPassword(e.currentTarget.value)}691 />692 <label class="peach-label">confirm duress password</label>693 <input694 class="peach-settings-input"695 type="password"696 placeholder="confirm duress password"697 autocomplete="new-password"698 value={duressConfirm()}699 onInput={(e) => setDuressConfirm(e.currentTarget.value)}700 />701 </Show>702703 <Show when={duressError()}>704 <p class="peach-hint peach-hint-error">{duressError()}</p>705 </Show>706707 <div class="peach-duress-actions">708 <button709 class="peach-btn peach-btn--subtle peach-btn--sm"710 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}711 >712 cancel713 </button>714 <Show when={duressEnabled()}>715 <button716 class="peach-btn peach-btn--ghost peach-btn--sm"717 style="color:var(--peach-breach)"718 onClick={() => void handleDisableDuress()}719 disabled={duressSaving()}720 >721 {duressSaving() ? "disabling..." : "disable duress"}722 </button>723 </Show>724 <Show when={!duressEnabled()}>725 <button726 class="peach-btn peach-btn--accent peach-btn--sm"727 onClick={() => void handleSetupDuress()}728 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}729 >730 {duressSaving() ? "saving..." : "save"}731 </button>732 </Show>733 </div>734 </div>735 </Show>736737 <p class="peach-settings-sub-label">PASSKEYS</p>738 <div class="peach-settings-row peach-settings-row--top">739 <div class="peach-settings-row-main">740 <span>Intercept passkey requests</span>741 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">742 Peach can handle passkey sign-in and creation on supported Chromium browsers.743 </span>744 </div>745 <button746 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}747 onClick={() => void handleTogglePasskeys()}748 aria-label="Toggle passkey interception"749 disabled={!passkeyState()?.supported || passkeySaving()}750 >751 <span class="peach-toggle-knob"></span>752 </button>753 </div>754 <div class="peach-settings-row peach-settings-row--meta">755 <span>Status</span>756 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>757 </div>758 <div class="peach-settings-row peach-settings-row--meta">759 <span>Browser</span>760 <span class="peach-settings-value">761 {passkeyState()?.browserLabel ?? "Unknown"}762 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}763 </span>764 </div>765 <div class="peach-info-notice" style="margin-top:8px">766 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.767 </div>768 <Show when={passkeyState()?.conflict}>769 <div class="peach-settings-row peach-settings-row--meta">770 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>771 </div>772 </Show>773 </div>774 </div>775 );776 }777778 function renderVault() {779 return (780 <div class="peach-settings-body">781 <div class="peach-settings-group">782 <p class="peach-settings-sub-label">ENTRIES</p>783 <div class="peach-settings-row peach-settings-row--meta">784 <span>Entries</span>785 <span class="peach-settings-value">{props.entriesCount}</span>786 </div>787 <div class="peach-settings-row peach-settings-row--meta">788 <span>Duplicates</span>789 <Show when={dedupDone() !== null} fallback={790 <button791 class="peach-btn peach-btn--subtle peach-btn--sm"792 onClick={() => void handleCountDuplicates()}793 disabled={dedupRunning()}794 >795 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}796 </button>797 }>798 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">799 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}800 </span>801 </Show>802 </div>803 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>804 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">805 <div class="peach-restore-warning">806 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.807 </div>808 <div style="display:flex;gap:6px">809 <button810 class="peach-btn peach-btn--subtle peach-btn--sm"811 onClick={() => setDedupCount(null)}812 >813 cancel814 </button>815 <button816 class="peach-btn peach-btn--accent peach-btn--sm"817 onClick={() => void handleDeduplicate()}818 disabled={dedupRunning()}819 >820 {dedupRunning() ? "removing..." : "remove duplicates"}821 </button>822 </div>823 </div>824 </Show>825826 <p class="peach-settings-sub-label">MAINTENANCE</p>827 <div class="peach-settings-row peach-settings-row--meta">828 <span>Trash auto-delete</span>829 <select830 class="peach-settings-select"831 value={String(props.trashRetentionDays)}832 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}833 >834 <option value="7">7 days</option>835 <option value="30">30 days</option>836 <option value="90">90 days</option>837 <option value="0">Never</option>838 </select>839 </div>840 <div class="peach-settings-row peach-settings-row--meta">841 <span>Vault Health</span>842 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>843 open844 </button>845 </div>846 <Show when={declinedCardCount() > 0}>847 <div class="peach-settings-row peach-settings-row--meta">848 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>849 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>850 Clear851 </button>852 </div>853 </Show>854855 <p class="peach-settings-sub-label">DATA</p>856 <div class="peach-settings-row peach-settings-row--meta">857 <span>Import passwords</span>858 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>859 open860 </button>861 </div>862 <div class="peach-settings-row peach-settings-row--meta">863 <span>Export backup</span>864 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>865 open866 </button>867 </div>868 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">869 <span>Replace vault from backup</span>870 <div class="peach-restore-warning" style="margin-top:2px">871 This will permanently replace your current vault with the backup.872 </div>873 <div style="display:flex;gap:6px">874 <button875 class="peach-btn peach-btn--subtle peach-btn--sm"876 onClick={() => props.onOpenCJKImport?.()}877 >878 CJK phrase879 </button>880 <button881 class="peach-btn peach-btn--subtle peach-btn--sm"882 onClick={() => props.onOpenFileImport?.()}883 >884 backup file885 </button>886 </div>887 </div>888 </div>889 </div>890 );891 }892893 function renderSync() {894 if (!isPro()) {895 return (896 <div class="peach-settings-body">897 <div class="peach-settings-group" style="position:relative">898 <div class="peach-sync-pro-gate">899 <div class="peach-sync-pro-gate-blur">900 <div class="peach-settings-row">901 <span>Status</span>902 <span class="peach-settings-value peach-settings-value--status">903 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>904 {getStatusLabel(syncStatus())}905 </span>906 </div>907 <div class="peach-settings-row peach-settings-row--meta">908 <span>Last sync</span>909 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>910 </div>911 </div>912 <div class="peach-sync-pro-gate-overlay">913 <span class="peach-sync-pro-badge">Pro</span>914 <a href="https://peachpasswords.com/#pricing" target="_blank" rel="noopener noreferrer" class="peach-link" style="color:var(--peach-accent);font-size:13px;font-weight:500">915 Upgrade to Pro →916 </a>917 </div>918 </div>919 </div>920 </div>921 );922 }923924 return (925 <div class="peach-settings-body">926 <div class="peach-settings-group">927 <div class="peach-settings-row">928 <span>Status</span>929 <span class="peach-settings-value peach-settings-value--status">930 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>931 {getStatusLabel(syncStatus())}932 </span>933 </div>934 <div class="peach-settings-row peach-settings-row--meta">935 <span>Last sync</span>936 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>937 </div>938 <div class="peach-settings-row peach-settings-row--meta">939 <button940 class="peach-btn peach-btn--subtle peach-btn--sm"941 onClick={() => void handleSyncNow()}942 disabled={syncStatus() === "not_trusted" || syncStatus() === "syncing"}943 >944 {syncingNow() ? "Syncing..." : "Sync now"}945 </button>946 </div>947948 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">949 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>950 {pairedDevices().map((device) => (951 <div class="peach-sync-device-row">952 <div class="peach-sync-device-info">953 <span class="peach-sync-device-name">{device.displayName}</span>954 <span class="peach-sync-device-meta">955 Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}956 </span>957 </div>958 <button959 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"960 onClick={() => void handleRemoveDevice(device.deviceId)}961 >962 Remove963 </button>964 </div>965 ))}966 <div class="peach-settings-actions--row">967 <button968 class="peach-btn peach-btn--subtle peach-btn--sm"969 onClick={() => props.onOpenSyncPairing?.("initiate")}970 >971 Pair a new device972 </button>973 <button974 class="peach-btn peach-btn--subtle peach-btn--sm"975 onClick={() => props.onOpenSyncPairing?.("accept")}976 >977 Pair with another device978 </button>979 </div>980 </div>981982 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">983 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>984 {trustedNetworks().filter(n => !n.isTailscale).map((network) => (985 <div class="peach-sync-device-row">986 <span class="peach-sync-device-name">{network.ssid ?? "Unknown network"}</span>987 <button988 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"989 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}990 >991 Remove992 </button>993 </div>994 ))}995 <div class="peach-settings-actions--row">996 <Show when={!isCurrentNetworkTrusted()}>997 <button998 class="peach-btn peach-btn--subtle peach-btn--sm"999 onClick={() => void handleMarkNetworkTrusted()}1000 >1001 Mark current network as trusted1002 </button>1003 </Show>1004 </div>1005 <span class="peach-sync-device-meta">1006 Tailscale is always trusted1007 </span>1008 </div>1009 </div>1010 </div>1011 );1012 }10131014 function renderAppearance() {1015 return (1016 <div class="peach-settings-body">1017 <div class="peach-settings-group">1018 <p class="peach-settings-sub-label">THEME</p>1019 <div class="peach-settings-row peach-settings-row--meta">1020 <span>Theme</span>1021 <button1022 class="peach-btn peach-btn--subtle peach-btn--sm"1023 onClick={() =>1024 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1025 }1026 >1027 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1028 </button>1029 </div>10301031 <p class="peach-settings-sub-label">ACCENT</p>1032 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1033 <span>Accent</span>1034 <div class="peach-accent-picker">1035 <For each={ACCENT_COLORS}>1036 {(color) => (1037 <button1038 class="peach-accent-swatch"1039 classList={{ "is-active": props.accentColor === color.name }}1040 style={{ background: color.hex }}1041 onClick={() => void props.onAccentChange(color.name)}1042 aria-label={color.name}1043 title={color.name}1044 />1045 )}1046 </For>1047 </div>1048 </div>1049 </div>1050 </div>1051 );1052 }10531054 function renderShortcuts() {1055 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1056 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);10571058 function startCapture(action: ShortcutAction) {1059 setConflictMsg(null);1060 setCapturingAction(action);1061 }1062 function cancelCapture() {1063 setCapturingAction(null);1064 setConflictMsg(null);1065 }1066 function commitBinding(action: ShortcutAction, binding: Keybinding) {1067 const conflict = findConflict(props.shortcuts, binding, action);1068 if (conflict) {1069 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1070 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1071 return;1072 }1073 const next = { ...props.shortcuts, [action]: binding };1074 setCapturingAction(null);1075 setConflictMsg(null);1076 void props.onShortcutsChange(next);1077 }1078 function resetAction(action: ShortcutAction) {1079 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1080 void props.onShortcutsChange(next);1081 }1082 function resetAll() {1083 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1084 }10851086 1087 1088 function handleCaptureKey(e: KeyboardEvent) {1089 e.preventDefault();1090 e.stopPropagation();1091 if (e.key === "Escape") { cancelCapture(); return; }1092 const binding = keybindingFromEvent(e);1093 if (!binding) return; 1094 const action = capturingAction();1095 if (action) commitBinding(action, binding);1096 }10971098 return (1099 <div class="peach-settings-body">1100 <div class="peach-settings-group peach-shortcuts-group">1101 <p class="peach-settings-sub-label">ENTRY VIEW</p>1102 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1103 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1104 </For>1105 </div>11061107 <div class="peach-settings-group peach-shortcuts-group">1108 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1109 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1110 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1111 </For>1112 </div>11131114 <Show when={conflictMsg()}>1115 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1116 </Show>11171118 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1119 <div class="peach-settings-row peach-settings-row--meta">1120 <span>Inline autofill on page</span>1121 <a1122 class="peach-settings-link"1123 href="chrome://extensions/shortcuts"1124 onClick={(e) => {1125 e.preventDefault();1126 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1127 }}1128 >1129 ⌘⇧P — customize ↗1130 </a>1131 </div>1132 <button1133 class="peach-btn peach-btn--subtle peach-btn--sm"1134 style="align-self:flex-start"1135 onClick={resetAll}1136 >1137 Reset all to defaults1138 </button>1139 </div>1140 </div>1141 );11421143 function renderRow(1144 desc: typeof SHORTCUT_DESCRIPTORS[number],1145 capturing: () => ShortcutAction | null,1146 onStart: (a: ShortcutAction) => void,1147 onCancel: () => void,1148 onReset: (a: ShortcutAction) => void,1149 onKey: (e: KeyboardEvent) => void,1150 ) {1151 const isActive = () => capturing() === desc.action;1152 const binding = () => props.shortcuts[desc.action];1153 const isCustom = () => {1154 const b = binding();1155 const d = DEFAULT_BINDINGS[desc.action];1156 return b.key !== d.key1157 || Boolean(b.ctrl) !== Boolean(d.ctrl)1158 || Boolean(b.meta) !== Boolean(d.meta)1159 || Boolean(b.shift) !== Boolean(d.shift)1160 || Boolean(b.alt) !== Boolean(d.alt);1161 };1162 return (1163 <div class="peach-shortcut-row">1164 <span class="peach-shortcut-row-label">{desc.label}</span>1165 <button1166 class="peach-shortcut-chip"1167 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1168 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1169 onKeyDown={(e) => { if (isActive()) onKey(e); }}1170 ref={(el) => {1171 1172 if (el) {1173 queueMicrotask(() => { if (isActive()) el.focus(); });1174 }1175 }}1176 >1177 {isActive() ? "Press a key…" : formatKeybinding(binding())}1178 </button>1179 <Show when={isCustom() && !isActive()}>1180 <button1181 class="peach-shortcut-reset"1182 title="Reset to default"1183 onClick={() => onReset(desc.action)}1184 >1185 ↺1186 </button>1187 </Show>1188 </div>1189 );1190 }1191 }11921193 function renderLicense() {1194 return (1195 <div class="peach-settings-body">1196 <div class="peach-settings-group">1197 <p class="peach-settings-sub-label">PLAN</p>1198 <Show1199 when={licenseInfo()?.valid && licenseInfo()?.plan === "pro"}1200 fallback={1201 <>1202 <div class="peach-pro-promo">1203 <div class="peach-pro-promo-glow" aria-hidden="true"></div>1204 <div class="peach-pro-promo-inner">1205 <h3 class="peach-pro-promo-title">Peach Pro</h3>1206 <p class="peach-pro-promo-tagline">One-time purchase. Yours forever.</p>1207 <ul class="peach-pro-promo-list">1208 <li>1209 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">1210 <path d="M3 8a5 5 0 0 1 9-3" />1211 <path d="M13 8a5 5 0 0 1-9 3" />1212 <path d="M12 2v3h-3" />1213 <path d="M4 14v-3h3" />1214 </svg>1215 <span>Sync across every device, end-to-end encrypted</span>1216 </li>1217 <li>1218 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">1219 <circle cx="6" cy="9" r="3" />1220 <path d="M9 9h5" />1221 <path d="M12 9v2.5" />1222 <path d="M14 9v2" />1223 </svg>1224 <span>Create and store passkeys directly in Peach</span>1225 </li>1226 <li>1227 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">1228 <path d="M8 2.5l1.6 3.4 3.7.4-2.8 2.5.8 3.7L8 10.7l-3.3 1.8.8-3.7L2.7 6.3l3.7-.4z" />1229 </svg>1230 <span>Every future Pro feature, no extra cost</span>1231 </li>1232 </ul>1233 <a1234 href="https://peachpasswords.com/#pricing"1235 target="_blank"1236 rel="noopener noreferrer"1237 class="peach-btn peach-pro-promo-cta"1238 >1239 Upgrade to Pro →1240 </a>1241 </div>1242 </div>1243 <p class="peach-pro-or-divider"><span>or activate a key</span></p>1244 <div class="peach-settings-row" style="flex-direction:column;align-items:stretch;gap:8px">1245 <input1246 class="peach-underline-input"1247 type="text"1248 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1249 value={licenseKeyInput()}1250 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1251 style="width:100%;box-sizing:border-box"1252 />1253 <button1254 class="peach-btn peach-btn--subtle peach-btn--sm"1255 onClick={() => void handleActivateLicense()}1256 disabled={activatingLicense() || !licenseKeyInput().trim()}1257 >1258 {activatingLicense() ? "activating..." : "Activate"}1259 </button>1260 </div>1261 <Show when={licenseError()}>1262 <div class="peach-settings-row peach-settings-row--meta">1263 <span class="peach-settings-value" style="color:#e55">{licenseError()}</span>1264 </div>1265 </Show>1266 <Show when={licenseSuccess()}>1267 <div class="peach-settings-row peach-settings-row--meta">1268 <span class="peach-settings-value" style="color:#5e5">{licenseSuccess()}</span>1269 </div>1270 </Show>1271 </>1272 }1273 >1274 <div class="peach-settings-row peach-settings-row--meta">1275 <span>Peach Pro</span>1276 <span class="peach-settings-value">1277 activated {licenseInfo()?.issued ? new Date(licenseInfo()!.issued!).toLocaleDateString() : ""}1278 </span>1279 </div>1280 </Show>1281 </div>1282 </div>1283 );1284 }12851286 function renderFeedback() {1287 return (1288 <div class="peach-settings-body">1289 <div class="peach-settings-group peach-feedback-group">1290 <p class="peach-feedback-intro">1291 Help us improve Peach. Every report goes directly to our inbox.1292 </p>12931294 <p class="peach-settings-sub-label">TYPE</p>1295 <div class="peach-feedback-chips">1296 <For each={FEEDBACK_CATEGORIES}>1297 {(cat) => (1298 <button1299 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1300 onClick={() => setFbCategory(cat)}1301 >1302 {cat}1303 </button>1304 )}1305 </For>1306 </div>13071308 <p class="peach-settings-sub-label">MESSAGE</p>1309 <textarea1310 class="peach-feedback-textarea"1311 placeholder="Describe the issue or idea..."1312 value={fbMessage()}1313 onInput={(e) => setFbMessage(e.currentTarget.value)}1314 />13151316 <p class="peach-feedback-meta">1317 Version {version} · will be included automatically1318 </p>13191320 <Show when={fbResult() === "success"}>1321 <p class="peach-feedback-result peach-feedback-result--ok">1322 Sent — thank you1323 </p>1324 </Show>1325 <Show when={fbResult() === "error"}>1326 <p class="peach-feedback-result peach-feedback-result--err">1327 Failed to send — try again1328 </p>1329 </Show>13301331 <button1332 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1333 onClick={() => void handleSubmitFeedback()}1334 disabled={fbSubmitting() || !fbMessage().trim()}1335 >1336 {fbSubmitting() ? "Sending..." : "Send feedback"}1337 </button>13381339 <p class="peach-feedback-anon">1340 Sent anonymously · no account required1341 </p>1342 </div>1343 </div>1344 );1345 }13461347 return (1348 <div class="peach-overlay-view ov-enter-right" style="display:flex">1349 <div class="peach-overlay-header">1350 <button1351 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1352 onClick={handleHeaderBack}1353 aria-label="Back"1354 >1355 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1356 <path d="M6 1L1 6L6 11" />1357 </svg>1358 </button>1359 <span class="peach-overlay-title">{currentTitle()}</span>1360 <div class="peach-overlay-spacer"></div>1361 </div>13621363 <Switch fallback={renderHome()}>1364 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1365 <Match when={activeSection() === "vault"}>{renderVault()}</Match>1366 <Match when={activeSection() === "sync"}>{renderSync()}</Match>1367 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1368 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1369 <Match when={activeSection() === "license"}>{renderLicense()}</Match>1370 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1371 </Switch>1372 </div>1373 );1374}