1import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2import { sendMessage } from "../App";3import { ACCENT_COLORS } from "../../shared/types";4import type { AccentColorName, PasskeyProviderState, ProFeature } 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 isPro?: boolean;31 initialSection?: string | null;32 upgradeHighlight?: ProFeature | null;33 onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;34 onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;35 onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;36 onTrashRetentionChange: (days: number) => void | Promise<void>;37 onThemeChange: (theme: "dark" | "light") => void;38 onAccentChange: (name: AccentColorName) => void | Promise<void>;39 shortcuts: Record<ShortcutAction, Keybinding>;40 onShortcutsChange: (active: Record<ShortcutAction, Keybinding>) => void | Promise<void>;41 onLicenseChange?: () => void | Promise<void>;42 onBack: () => void;43 onImport: () => void;44 onExport: () => void;45 onBreachCheckDone: () => void;46 onOpenHealth?: () => void;47 onOpenSyncPairing?: (mode: "initiate" | "accept") => void;48 onOpenCJKImport?: () => void;49 onOpenFileImport?: () => void;50}5152type SectionName =53 | "security"54 | "vault"55 | "sync"56 | "appearance"57 | "shortcuts"58 | "license"59 | "feedback";6061const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];6263function formatActivatedSince(issued: string | undefined): string {64 if (!issued) return "Active";65 try {66 const d = new Date(issued);67 if (Number.isNaN(d.getTime())) return "Active";68 return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`;69 } catch {70 return "Active";71 }72}7374function isSectionName(value: string): value is SectionName {75 return (76 value === "security" ||77 value === "vault" ||78 value === "sync" ||79 value === "appearance" ||80 value === "shortcuts" ||81 value === "license" ||82 value === "feedback"83 );84}8586export function SettingsView(props: SettingsProps) {87 const version = chrome.runtime.getManifest().version;88 const initial = props.initialSection;89 const [activeSection, setActiveSection] = createSignal<SectionName | null>(90 initial && isSectionName(initial) ? (initial as SectionName) : null91 );9293 const [checkingBreaches, setCheckingBreaches] = createSignal(false);94 const [breachResult, setBreachResult] = createSignal<string | null>(null);95 const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);96 const [licenseKeyInput, setLicenseKeyInput] = createSignal("");97 const [licenseError, setLicenseError] = createSignal<string | null>(null);98 const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);99 const [activatingLicense, setActivatingLicense] = createSignal(false);100101 const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);102 const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);103 const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");104 const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);105 106 107 108 const [syncProStatus, setSyncProStatus] = createSignal(false);109 const isPro = () => props.isPro ?? syncProStatus();110 const [syncingNow, setSyncingNow] = createSignal(false);111 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);112 const [passkeySaving, setPasskeySaving] = createSignal(false);113114 const [dedupCount, setDedupCount] = createSignal<number | null>(null);115 const [dedupRunning, setDedupRunning] = createSignal(false);116 const [dedupDone, setDedupDone] = createSignal<number | null>(null);117118 const [declinedCardCount, setDeclinedCardCount] = createSignal(0);119120 const [showChangePw, setShowChangePw] = createSignal(false);121 const [changePwCurrent, setChangePwCurrent] = createSignal("");122 const [changePwNew, setChangePwNew] = createSignal("");123 const [changePwConfirm, setChangePwConfirm] = createSignal("");124 const [changePwSaving, setChangePwSaving] = createSignal(false);125 const [changePwError, setChangePwError] = createSignal<string | null>(null);126 const [changePwSuccess, setChangePwSuccess] = createSignal(false);127128 const [duressEnabled, setDuressEnabled] = createSignal(false);129 const [showDuressSetup, setShowDuressSetup] = createSignal(false);130 const [duressPassword, setDuressPassword] = createSignal("");131 const [duressConfirm, setDuressConfirm] = createSignal("");132 const [duressMainPassword, setDuressMainPassword] = createSignal("");133 const [duressSaving, setDuressSaving] = createSignal(false);134 const [duressError, setDuressError] = createSignal<string | null>(null);135 const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);136137 const [fbCategory, setFbCategory] = createSignal("Bug report");138 const [fbMessage, setFbMessage] = createSignal("");139 const [fbSubmitting, setFbSubmitting] = createSignal(false);140 const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);141142 onMount(async () => {143 const [licenseRes, syncRes, passkeyRes, duressRes, settingsRes] = await Promise.all([144 sendMessage({ type: "GET_LICENSE" }),145 sendMessage({ type: "GET_SYNC_STATE" }),146 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),147 sendMessage({ type: "IS_DURESS_ENABLED" }),148 sendMessage({ type: "GET_SETTINGS" }),149 ]);150151 if ((licenseRes as { ok: boolean }).ok) {152 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);153 }154155 if ((syncRes as { ok: boolean }).ok) {156 const data = syncRes as { ok: boolean; data?: {157 pairedDevices?: PairedDevice[];158 trustedNetworks?: TrustedNetwork[];159 status?: SyncStatus;160 lastSyncAt?: number;161 isPro?: boolean;162 }};163 if (data.data) {164 setPairedDevices(data.data.pairedDevices ?? []);165 setTrustedNetworks(data.data.trustedNetworks ?? []);166 setSyncStatus(data.data.status ?? "offline");167 setLastSyncAt(data.data.lastSyncAt);168 setSyncProStatus(data.data.isPro ?? false);169 }170 }171172 if ((passkeyRes as { ok: boolean }).ok) {173 setPasskeyState(174 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null175 );176 }177178 if ((duressRes as { ok: boolean }).ok) {179 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);180 }181182 if ((settingsRes as { ok: boolean }).ok) {183 const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data;184 setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0);185 }186187 chrome.runtime.onMessage.addListener((msg) => {188 if (msg.type === "SYNC_STATE_UPDATE") {189 setPairedDevices(msg.pairedDevices ?? []);190 setTrustedNetworks(msg.trustedNetworks ?? []);191 setSyncStatus(msg.status ?? "offline");192 setLastSyncAt(msg.lastSyncAt);193 setSyncProStatus(msg.isPro ?? false);194 }195 });196 });197198 async function handleChangePassword() {199 setChangePwError(null);200 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }201 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }202 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }203 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }204 setChangePwSaving(true);205 const res = await sendMessage({206 type: "CHANGE_PASSWORD",207 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },208 }) as { ok: boolean; error?: string };209 if (res.ok) {210 setChangePwSuccess(true);211 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");212 setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);213 } else {214 setChangePwError(res.error ?? "failed to change password");215 }216 setChangePwSaving(false);217 }218219 async function handleActivateLicense() {220 const key = licenseKeyInput().trim();221 if (!key) return;222 setActivatingLicense(true);223 setLicenseError(null);224 setLicenseSuccess(null);225 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });226 setActivatingLicense(false);227 if ((res as { ok: boolean }).ok) {228 const data = (res as { ok: boolean; data?: LicenseInfo }).data;229 if (data?.valid) {230 setLicenseInfo(data);231 setLicenseSuccess("License activated!");232 setLicenseKeyInput("");233 } else {234 setLicenseError("Invalid license key");235 }236 } else {237 setLicenseError("Activation failed");238 }239 }240241 async function handleTogglePasskeys() {242 const current = passkeyState();243 if (!current || !current.supported) return;244 setPasskeySaving(true);245 const res = await sendMessage({246 type: "SET_PASSKEY_PROVIDER_ENABLED",247 payload: { enabled: !current.enabled },248 });249 setPasskeySaving(false);250 if ((res as { ok: boolean }).ok) {251 setPasskeyState(252 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null253 );254 }255 }256257 async function handleCheckAllBreaches() {258 setCheckingBreaches(true);259 setBreachResult(null);260 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });261 setCheckingBreaches(false);262 if ((res as { ok: boolean }).ok) {263 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};264 const breached = Object.values(data).filter((count) => count > 0).length;265 setBreachResult(266 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"267 );268 props.onBreachCheckDone();269 }270 }271272 async function handleSyncNow() {273 if (syncStatus() === "not_trusted") return;274 setSyncingNow(true);275 await sendMessage({ type: "TRIGGER_SYNC" });276 setTimeout(() => setSyncingNow(false), 3000);277 }278279 async function handleRemoveDevice(deviceId: string) {280 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });281 }282283 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {284 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });285 }286287 async function handleMarkNetworkTrusted() {288 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });289 }290291 function formatRelativeTime(timestamp?: number): string {292 if (!timestamp) return "Never";293 const diff = Date.now() - timestamp;294 const minutes = Math.floor(diff / 60000);295 const hours = Math.floor(diff / 3600000);296 const days = Math.floor(diff / 86400000);297 if (minutes < 1) return "Just now";298 if (minutes < 60) return `${minutes}m ago`;299 if (hours < 24) return `${hours}h ago`;300 return `${days}d ago`;301 }302303 function getStatusColor(status: SyncStatus): string {304 switch (status) {305 case "synced": return "var(--peach-sync-ok)";306 case "syncing": return "var(--peach-sync-busy)";307 case "no_peers": return "var(--peach-sync-off)";308 case "not_trusted": return "var(--peach-sync-err)";309 case "offline": return "var(--peach-sync-off)";310 default: return "var(--peach-sync-off)";311 }312 }313314 function getStatusLabel(status: SyncStatus): string {315 switch (status) {316 case "synced": return "Synced";317 case "syncing": return "Syncing";318 case "no_peers": return "No peers visible";319 case "not_trusted": return "Not on a trusted network";320 case "offline": return "Offline";321 default: return "Unknown";322 }323 }324325 function isCurrentNetworkTrusted(): boolean {326 return trustedNetworks().length > 0;327 }328329 async function handleSetupDuress() {330 const mainPw = duressMainPassword().trim();331 const duressPw = duressPassword().trim();332 const confirmPw = duressConfirm().trim();333334 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }335 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }336 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }337 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }338339 setDuressSaving(true);340 setDuressError(null);341342 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };343 if (!verifyRes.ok || !verifyRes.data) {344 setDuressError("incorrect main password");345 setDuressSaving(false);346 return;347 }348349 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };350 setDuressSaving(false);351 if (res.ok) {352 setDuressEnabled(true);353 setShowDuressSetup(false);354 setDuressPassword("");355 setDuressConfirm("");356 setDuressMainPassword("");357 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");358 setTimeout(() => setDuressSuccess(null), 5000);359 } else {360 setDuressError(res.error ?? "failed to set duress password");361 }362 }363364 async function handleClearDeclinedCards() {365 await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } });366 setDeclinedCardCount(0);367 }368369 async function handleCountDuplicates() {370 setDedupRunning(true);371 setDedupDone(null);372 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };373 setDedupRunning(false);374 if (res.ok) setDedupCount(res.data ?? 0);375 }376377 async function handleDeduplicate() {378 setDedupRunning(true);379 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };380 setDedupRunning(false);381 if (res.ok) {382 setDedupDone(res.data ?? 0);383 setDedupCount(0);384 }385 }386387 async function handleDisableDuress() {388 const mainPw = duressMainPassword().trim();389 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }390391 setDuressSaving(true);392 setDuressError(null);393394 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };395 if (!verifyRes.ok || !verifyRes.data) {396 setDuressError("incorrect main password");397 setDuressSaving(false);398 return;399 }400401 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };402 setDuressSaving(false);403 if (res.ok) {404 setDuressEnabled(false);405 setShowDuressSetup(false);406 setDuressMainPassword("");407 setDuressSuccess("Duress password disabled.");408 setTimeout(() => setDuressSuccess(null), 4000);409 } else {410 setDuressError(res.error ?? "failed to disable duress password");411 }412 }413414 async function handleSubmitFeedback() {415 if (!fbMessage().trim() || fbSubmitting()) return;416 setFbSubmitting(true);417 setFbResult(null);418 const res = await sendMessage({419 type: "SUBMIT_FEEDBACK",420 payload: {421 category: fbCategory(),422 message: fbMessage().trim(),423 version,424 browser: navigator.userAgent,425 },426 }) as { ok: boolean; error?: string };427 setFbSubmitting(false);428 if (res.ok) {429 setFbResult("success");430 setFbMessage("");431 setTimeout(() => setFbResult(null), 4000);432 } else {433 setFbResult("error");434 }435 }436437 function handleHeaderBack() {438 if (activeSection() !== null) {439 setActiveSection(null);440 } else {441 props.onBack();442 }443 }444445 function currentTitle(): string {446 const s = activeSection();447 if (s === null) return "settings";448 if (s === "license") return "account";449 return s;450 }451452 function renderCatRow(opts: {453 section: SectionName;454 name: string;455 desc: string;456 icon: () => any;457 pro?: boolean;458 }) {459 return (460 <button461 class="peach-settings-cat-row"462 onClick={() => setActiveSection(opts.section)}463 >464 <div class="peach-settings-cat-icon">{opts.icon()}</div>465 <div class="peach-settings-cat-content">466 <span class="peach-settings-cat-name">467 {opts.name}468 <Show when={opts.pro && !isPro()}>469 <span class="peach-pro-mark" aria-label="Pro feature">✦</span>470 </Show>471 </span>472 <span class="peach-settings-cat-desc">{opts.desc}</span>473 </div>474 <svg475 class="peach-settings-cat-chevron"476 width="6" height="10" viewBox="0 0 6 10" fill="none"477 stroke="currentColor" stroke-width="1.1"478 stroke-linecap="round" stroke-linejoin="round"479 >480 <path d="M1 1l4 4-4 4" />481 </svg>482 </button>483 );484 }485486 const iconShield = () => (487 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">488 <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>489 </svg>490 );491 const iconKey = () => (492 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">493 <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>494 </svg>495 );496 const iconLock = () => (497 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">498 <rect x="3" y="7" width="10" height="7" rx="1.5"/>499 <path d="M6 7V5a3 3 0 016 0v2"/>500 </svg>501 );502 const iconSync = () => (503 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">504 <path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/>505 <path d="M14 6l-2 2-2-2"/>506 <path d="M2 10l2-2 2 2"/>507 </svg>508 );509 const iconSun = () => (510 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">511 <circle cx="8" cy="8" r="3"/>512 <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"/>513 </svg>514 );515 const iconKeys = () => (516 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">517 <rect x="2" y="5" width="5" height="4" rx="1"/>518 <rect x="9" y="7" width="5" height="4" rx="1"/>519 <path d="M7 7h2m-2 2h2"/>520 </svg>521 );522 const iconBubble = () => (523 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">524 <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>525 </svg>526 );527528 function renderHome() {529 return (530 <div class="peach-settings-body">531 <p class="peach-settings-nav-label">PROTECTION</p>532 {renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })}533 {renderCatRow({ section: "license", name: "Account", desc: "Plan & license key", icon: iconKey })}534535 <p class="peach-settings-nav-label">VAULT</p>536 {renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}537 {renderCatRow({ section: "sync", name: "Sync", desc: "Paired devices & trusted networks", icon: iconSync, pro: true })}538539 <p class="peach-settings-nav-label">PREFERENCES</p>540 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}541 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}542543 <p class="peach-settings-nav-label">SUPPORT</p>544 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}545546 <div class="peach-settings-footer">547 <span>Peach {version}</span>548 <span class="peach-divider"></span>549 <span>Browser extension</span>550 </div>551 </div>552 );553 }554555 function renderSecurity() {556 return (557 <div class="peach-settings-body">558 <div class="peach-settings-group">559 <p class="peach-settings-sub-label">LOCKING</p>560 <div class="peach-settings-row">561 <span>Lock on close</span>562 <button563 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}564 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}565 aria-label="Toggle lock on close"566 >567 <span class="peach-toggle-knob"></span>568 </button>569 </div>570 <div class="peach-settings-row">571 <span>Auto-lock after</span>572 <select573 class="peach-settings-select"574 value={String(props.autoLockMinutes)}575 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}576 >577 <option value="1">1 minute</option>578 <option value="5">5 minutes</option>579 <option value="15">15 minutes</option>580 <option value="30">30 minutes</option>581 <option value="0">Never</option>582 </select>583 </div>584585 <p class="peach-settings-sub-label">BREACH PROTECTION</p>586 <div class="peach-settings-row">587 <span>Breach monitoring</span>588 <button589 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}590 onClick={() => void props.onBreachMonitoringChange(!props.breachMonitoring)}591 aria-label="Toggle breach monitoring"592 >593 <span class="peach-toggle-knob"></span>594 </button>595 </div>596 <div class="peach-settings-row peach-settings-row--meta">597 <span>Check all passwords</span>598 <button599 class="peach-btn peach-btn--subtle peach-btn--sm"600 onClick={() => void handleCheckAllBreaches()}601 disabled={checkingBreaches()}602 >603 {checkingBreaches() ? "checking..." : "run"}604 </button>605 </div>606 <Show when={breachResult()}>607 <div class="peach-settings-row peach-settings-row--meta">608 <span>Last check</span>609 <span class="peach-settings-value">{breachResult()}</span>610 </div>611 </Show>612613 <p class="peach-settings-sub-label">MASTER PASSWORD</p>614 <div class="peach-settings-row peach-settings-row--meta">615 <span>Master password</span>616 <button617 class="peach-btn peach-btn--subtle peach-btn--sm"618 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}619 >620 {showChangePw() ? "cancel" : "change"}621 </button>622 </div>623 <Show when={showChangePw()}>624 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">625 <input626 class="peach-settings-input"627 type="password"628 placeholder="current password"629 value={changePwCurrent()}630 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}631 autocomplete="current-password"632 />633 <input634 class="peach-settings-input"635 type="password"636 placeholder="new password (min 12 chars)"637 value={changePwNew()}638 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}639 autocomplete="new-password"640 />641 <input642 class="peach-settings-input"643 type="password"644 placeholder="confirm new password"645 value={changePwConfirm()}646 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}647 autocomplete="new-password"648 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}649 />650 <Show when={changePwError()}>651 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>652 </Show>653 <Show when={changePwSuccess()}>654 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>655 </Show>656 <button657 class="peach-btn peach-btn--accent peach-btn--sm"658 style="align-self:flex-start"659 onClick={() => void handleChangePassword()}660 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}661 >662 {changePwSaving() ? "saving…" : "save new password"}663 </button>664 </div>665 </Show>666667 <p class="peach-settings-sub-label">DURESS PASSWORD</p>668 <div class="peach-settings-row peach-settings-row--top">669 <div class="peach-settings-row-main">670 <span>Duress password</span>671 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">672 If entered under coercion, shows a decoy empty vault instead of your real one.673 </span>674 </div>675 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>676 {duressEnabled() ? "on" : "off"}677 </div>678 </div>679680 <Show when={duressEnabled() && !showDuressSetup()}>681 <div class="peach-settings-row peach-settings-row--meta">682 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">683 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.684 </span>685 </div>686 </Show>687688 <Show when={duressSuccess()}>689 <div class="peach-settings-row peach-settings-row--meta">690 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>691 </div>692 </Show>693694 <Show when={!showDuressSetup()}>695 <div class="peach-settings-row peach-settings-row--meta">696 <button697 class="peach-btn peach-btn--subtle peach-btn--sm"698 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}699 >700 {duressEnabled() ? "Change / disable" : "Set up"}701 </button>702 </div>703 </Show>704705 <Show when={showDuressSetup()}>706 <div class="peach-duress-form">707 <label class="peach-label">main password (to confirm)</label>708 <input709 class="peach-settings-input"710 type="password"711 placeholder="your current master password"712 autocomplete="current-password"713 value={duressMainPassword()}714 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}715 />716 <Show when={!duressEnabled()}>717 <label class="peach-label">new duress password</label>718 <input719 class="peach-settings-input"720 type="password"721 placeholder="duress password (min 8 chars)"722 autocomplete="new-password"723 value={duressPassword()}724 onInput={(e) => setDuressPassword(e.currentTarget.value)}725 />726 <label class="peach-label">confirm duress password</label>727 <input728 class="peach-settings-input"729 type="password"730 placeholder="confirm duress password"731 autocomplete="new-password"732 value={duressConfirm()}733 onInput={(e) => setDuressConfirm(e.currentTarget.value)}734 />735 </Show>736737 <Show when={duressError()}>738 <p class="peach-hint peach-hint-error">{duressError()}</p>739 </Show>740741 <div class="peach-duress-actions">742 <button743 class="peach-btn peach-btn--subtle peach-btn--sm"744 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}745 >746 cancel747 </button>748 <Show when={duressEnabled()}>749 <button750 class="peach-btn peach-btn--ghost peach-btn--sm"751 style="color:var(--peach-breach)"752 onClick={() => void handleDisableDuress()}753 disabled={duressSaving()}754 >755 {duressSaving() ? "disabling..." : "disable duress"}756 </button>757 </Show>758 <Show when={!duressEnabled()}>759 <button760 class="peach-btn peach-btn--accent peach-btn--sm"761 onClick={() => void handleSetupDuress()}762 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}763 >764 {duressSaving() ? "saving..." : "save"}765 </button>766 </Show>767 </div>768 </div>769 </Show>770771 <p class="peach-settings-sub-label">PASSKEYS</p>772 <div class="peach-settings-row peach-settings-row--top">773 <div class="peach-settings-row-main">774 <span>Intercept passkey requests</span>775 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">776 Peach can handle passkey sign-in and creation on supported Chromium browsers.777 </span>778 </div>779 <button780 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}781 onClick={() => void handleTogglePasskeys()}782 aria-label="Toggle passkey interception"783 disabled={!passkeyState()?.supported || passkeySaving()}784 >785 <span class="peach-toggle-knob"></span>786 </button>787 </div>788 <div class="peach-settings-row peach-settings-row--meta">789 <span>Status</span>790 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>791 </div>792 <div class="peach-settings-row peach-settings-row--meta">793 <span>Browser</span>794 <span class="peach-settings-value">795 {passkeyState()?.browserLabel ?? "Unknown"}796 {passkeyState()?.supported ? " ✓" : " (interception unavailable)"}797 </span>798 </div>799 <div class="peach-info-notice" style="margin-top:8px">800 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.801 </div>802 <Show when={passkeyState()?.conflict}>803 <div class="peach-settings-row peach-settings-row--meta">804 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>805 </div>806 </Show>807 </div>808 </div>809 );810 }811812 function renderVault() {813 return (814 <div class="peach-settings-body">815 <div class="peach-settings-group">816 <p class="peach-settings-sub-label">ENTRIES</p>817 <div class="peach-settings-row peach-settings-row--meta">818 <span>Entries</span>819 <span class="peach-settings-value">{props.entriesCount}</span>820 </div>821 <div class="peach-settings-row peach-settings-row--meta">822 <span>Duplicates</span>823 <Show when={dedupDone() !== null} fallback={824 <button825 class="peach-btn peach-btn--subtle peach-btn--sm"826 onClick={() => void handleCountDuplicates()}827 disabled={dedupRunning()}828 >829 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}830 </button>831 }>832 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">833 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}834 </span>835 </Show>836 </div>837 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>838 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">839 <div class="peach-restore-warning">840 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.841 </div>842 <div style="display:flex;gap:6px">843 <button844 class="peach-btn peach-btn--subtle peach-btn--sm"845 onClick={() => setDedupCount(null)}846 >847 cancel848 </button>849 <button850 class="peach-btn peach-btn--accent peach-btn--sm"851 onClick={() => void handleDeduplicate()}852 disabled={dedupRunning()}853 >854 {dedupRunning() ? "removing..." : "remove duplicates"}855 </button>856 </div>857 </div>858 </Show>859860 <p class="peach-settings-sub-label">MAINTENANCE</p>861 <div class="peach-settings-row peach-settings-row--meta">862 <span>Trash auto-delete</span>863 <select864 class="peach-settings-select"865 value={String(props.trashRetentionDays)}866 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}867 >868 <option value="7">7 days</option>869 <option value="30">30 days</option>870 <option value="90">90 days</option>871 <option value="0">Never</option>872 </select>873 </div>874 <div class="peach-settings-row peach-settings-row--meta">875 <span>Vault Health</span>876 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>877 open878 </button>879 </div>880 <Show when={declinedCardCount() > 0}>881 <div class="peach-settings-row peach-settings-row--meta">882 <span>{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save</span>883 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={() => void handleClearDeclinedCards()}>884 Clear885 </button>886 </div>887 </Show>888889 <p class="peach-settings-sub-label">DATA</p>890 <div class="peach-settings-row peach-settings-row--meta">891 <span>Import passwords</span>892 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>893 open894 </button>895 </div>896 <div class="peach-settings-row peach-settings-row--meta">897 <span>Export backup</span>898 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>899 open900 </button>901 </div>902 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">903 <span>Replace vault from backup</span>904 <div class="peach-restore-warning" style="margin-top:2px">905 This will permanently replace your current vault with the backup.906 </div>907 <div style="display:flex;gap:6px">908 <button909 class="peach-btn peach-btn--subtle peach-btn--sm"910 onClick={() => props.onOpenCJKImport?.()}911 >912 CJK phrase913 </button>914 <button915 class="peach-btn peach-btn--subtle peach-btn--sm"916 onClick={() => props.onOpenFileImport?.()}917 >918 backup file919 </button>920 </div>921 </div>922 </div>923 </div>924 );925 }926927 function renderSync() {928 if (!isPro()) {929 return (930 <div class="peach-settings-body">931 <div class="peach-settings-group" style="position:relative">932 <div class="peach-sync-pro-gate">933 <div class="peach-sync-pro-gate-blur">934 <div class="peach-settings-row">935 <span>Status</span>936 <span class="peach-settings-value peach-settings-value--status">937 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>938 {getStatusLabel(syncStatus())}939 </span>940 </div>941 <div class="peach-settings-row peach-settings-row--meta">942 <span>Last sync</span>943 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>944 </div>945 </div>946 <div class="peach-sync-pro-gate-overlay">947 <span class="peach-sync-pro-badge">Pro</span>948 <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">949 Upgrade to Pro →950 </a>951 </div>952 </div>953 </div>954 </div>955 );956 }957958 return (959 <div class="peach-settings-body">960 <div class="peach-settings-group">961 <div class="peach-settings-row">962 <span>Status</span>963 <span class="peach-settings-value peach-settings-value--status">964 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>965 {getStatusLabel(syncStatus())}966 </span>967 </div>968 <div class="peach-settings-row peach-settings-row--meta">969 <span>Last sync</span>970 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>971 </div>972 <div class="peach-settings-row peach-settings-row--meta">973 <button974 class="peach-btn peach-btn--subtle peach-btn--sm"975 onClick={() => void handleSyncNow()}976 disabled={syncStatus() === "not_trusted" || syncStatus() === "syncing"}977 >978 {syncingNow() ? "Syncing..." : "Sync now"}979 </button>980 </div>981982 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">983 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>984 {pairedDevices().map((device) => (985 <div class="peach-sync-device-row">986 <div class="peach-sync-device-info">987 <span class="peach-sync-device-name">{device.displayName}</span>988 <span class="peach-sync-device-meta">989 Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}990 </span>991 </div>992 <button993 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"994 onClick={() => void handleRemoveDevice(device.deviceId)}995 >996 Remove997 </button>998 </div>999 ))}1000 <div class="peach-settings-actions--row">1001 <button1002 class="peach-btn peach-btn--subtle peach-btn--sm"1003 onClick={() => props.onOpenSyncPairing?.("initiate")}1004 >1005 Pair a new device1006 </button>1007 <button1008 class="peach-btn peach-btn--subtle peach-btn--sm"1009 onClick={() => props.onOpenSyncPairing?.("accept")}1010 >1011 Pair with another device1012 </button>1013 </div>1014 </div>10151016 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1017 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1018 {trustedNetworks().filter(n => !n.isTailscale).map((network) => (1019 <div class="peach-sync-device-row">1020 <span class="peach-sync-device-name">{network.ssid ?? "Unknown network"}</span>1021 <button1022 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1023 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1024 >1025 Remove1026 </button>1027 </div>1028 ))}1029 <div class="peach-settings-actions--row">1030 <Show when={!isCurrentNetworkTrusted()}>1031 <button1032 class="peach-btn peach-btn--subtle peach-btn--sm"1033 onClick={() => void handleMarkNetworkTrusted()}1034 >1035 Mark current network as trusted1036 </button>1037 </Show>1038 </div>1039 <span class="peach-sync-device-meta">1040 Tailscale is always trusted1041 </span>1042 </div>1043 </div>1044 </div>1045 );1046 }10471048 function renderAppearance() {1049 return (1050 <div class="peach-settings-body">1051 <div class="peach-settings-group">1052 <p class="peach-settings-sub-label">THEME</p>1053 <div class="peach-settings-row peach-settings-row--meta">1054 <span>Theme</span>1055 <button1056 class="peach-btn peach-btn--subtle peach-btn--sm"1057 onClick={() =>1058 props.onThemeChange(props.theme === "dark" ? "light" : "dark")1059 }1060 >1061 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}1062 </button>1063 </div>10641065 <p class="peach-settings-sub-label">ACCENT</p>1066 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">1067 <span>Accent</span>1068 <div class="peach-accent-picker">1069 <For each={ACCENT_COLORS}>1070 {(color) => (1071 <button1072 class="peach-accent-swatch"1073 classList={{ "is-active": props.accentColor === color.name }}1074 style={{ background: color.hex }}1075 onClick={() => void props.onAccentChange(color.name)}1076 aria-label={color.name}1077 title={color.name}1078 />1079 )}1080 </For>1081 </div>1082 </div>1083 </div>1084 </div>1085 );1086 }10871088 function renderShortcuts() {1089 const [capturingAction, setCapturingAction] = createSignal<ShortcutAction | null>(null);1090 const [conflictMsg, setConflictMsg] = createSignal<string | null>(null);10911092 function startCapture(action: ShortcutAction) {1093 setConflictMsg(null);1094 setCapturingAction(action);1095 }1096 function cancelCapture() {1097 setCapturingAction(null);1098 setConflictMsg(null);1099 }1100 function commitBinding(action: ShortcutAction, binding: Keybinding) {1101 const conflict = findConflict(props.shortcuts, binding, action);1102 if (conflict) {1103 const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict);1104 setConflictMsg(`Already bound to "${other?.label ?? conflict}"`);1105 return;1106 }1107 const next = { ...props.shortcuts, [action]: binding };1108 setCapturingAction(null);1109 setConflictMsg(null);1110 void props.onShortcutsChange(next);1111 }1112 function resetAction(action: ShortcutAction) {1113 const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] };1114 void props.onShortcutsChange(next);1115 }1116 function resetAll() {1117 void props.onShortcutsChange({ ...DEFAULT_BINDINGS });1118 }11191120 1121 1122 function handleCaptureKey(e: KeyboardEvent) {1123 e.preventDefault();1124 e.stopPropagation();1125 if (e.key === "Escape") { cancelCapture(); return; }1126 const binding = keybindingFromEvent(e);1127 if (!binding) return; 1128 const action = capturingAction();1129 if (action) commitBinding(action, binding);1130 }11311132 return (1133 <div class="peach-settings-body">1134 <div class="peach-settings-group peach-shortcuts-group">1135 <p class="peach-settings-sub-label">ENTRY VIEW</p>1136 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "entry")}>1137 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1138 </For>1139 </div>11401141 <div class="peach-settings-group peach-shortcuts-group">1142 <p class="peach-settings-sub-label">ANYWHERE IN POPUP</p>1143 <For each={SHORTCUT_DESCRIPTORS.filter((d) => d.scope === "global")}>1144 {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}1145 </For>1146 </div>11471148 <Show when={conflictMsg()}>1149 <div class="peach-shortcuts-conflict">{conflictMsg()}</div>1150 </Show>11511152 <div class="peach-settings-group" style="display:flex;flex-direction:column;gap:8px">1153 <div class="peach-settings-row peach-settings-row--meta">1154 <span>Inline autofill on page</span>1155 <a1156 class="peach-settings-link"1157 href="chrome://extensions/shortcuts"1158 onClick={(e) => {1159 e.preventDefault();1160 chrome.tabs.create({ url: "chrome://extensions/shortcuts" });1161 }}1162 >1163 ⌘⇧P — customize ↗1164 </a>1165 </div>1166 <button1167 class="peach-btn peach-btn--subtle peach-btn--sm"1168 style="align-self:flex-start"1169 onClick={resetAll}1170 >1171 Reset all to defaults1172 </button>1173 </div>1174 </div>1175 );11761177 function renderRow(1178 desc: typeof SHORTCUT_DESCRIPTORS[number],1179 capturing: () => ShortcutAction | null,1180 onStart: (a: ShortcutAction) => void,1181 onCancel: () => void,1182 onReset: (a: ShortcutAction) => void,1183 onKey: (e: KeyboardEvent) => void,1184 ) {1185 const isActive = () => capturing() === desc.action;1186 const binding = () => props.shortcuts[desc.action];1187 const isCustom = () => {1188 const b = binding();1189 const d = DEFAULT_BINDINGS[desc.action];1190 return b.key !== d.key1191 || Boolean(b.ctrl) !== Boolean(d.ctrl)1192 || Boolean(b.meta) !== Boolean(d.meta)1193 || Boolean(b.shift) !== Boolean(d.shift)1194 || Boolean(b.alt) !== Boolean(d.alt);1195 };1196 return (1197 <div class="peach-shortcut-row">1198 <span class="peach-shortcut-row-label">{desc.label}</span>1199 <button1200 class="peach-shortcut-chip"1201 classList={{ "is-capturing": isActive(), "is-custom": isCustom() }}1202 onClick={() => isActive() ? onCancel() : onStart(desc.action)}1203 onKeyDown={(e) => { if (isActive()) onKey(e); }}1204 ref={(el) => {1205 1206 if (el) {1207 queueMicrotask(() => { if (isActive()) el.focus(); });1208 }1209 }}1210 >1211 {isActive() ? "Press a key…" : formatKeybinding(binding())}1212 </button>1213 <Show when={isCustom() && !isActive()}>1214 <button1215 class="peach-shortcut-reset"1216 title="Reset to default"1217 onClick={() => onReset(desc.action)}1218 >1219 ↺1220 </button>1221 </Show>1222 </div>1223 );1224 }1225 }12261227 const PRO_FEATURES: Array<{1228 feature: ProFeature;1229 name: string;1230 desc: string;1231 }> = [1232 { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer · local" },1233 { feature: "ios_app", name: "Android & iOS", desc: "Your vault everywhere" },1234 { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },1235 { feature: "advanced_health", name: "Advanced Vault Health", desc: "Reuse · age · exposure scoring" },1236 { feature: "smart_password_change", name: "Smart Password Change", desc: "Guided reset · pre-filled · vault updated" },1237 ];12381239 const [showKeyEntry, setShowKeyEntry] = createSignal(false);1240 const [deactivating, setDeactivating] = createSignal(false);12411242 async function handleDeactivateLicense() {1243 setDeactivating(true);1244 await sendMessage({ type: "DEACTIVATE_LICENSE" });1245 setLicenseInfo(null);1246 await props.onLicenseChange?.();1247 setDeactivating(false);1248 }12491250 function renderLicense() {1251 return (1252 <div class="peach-settings-body">1253 <Show1254 when={isPro()}1255 fallback={1256 <div class="peach-pro-upgrade">1257 <div class="peach-pro-upgrade-hero">1258 <span class="peach-pro-upgrade-mark" aria-hidden="true">🍑</span>1259 <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1260 <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1261 </div>12621263 <div class="peach-pro-features">1264 <For each={PRO_FEATURES}>1265 {(item) => (1266 <div1267 class="peach-pro-feature"1268 classList={{1269 "is-highlighted": props.upgradeHighlight === item.feature,1270 }}1271 >1272 <span class="peach-pro-feature-mark">✦</span>1273 <div class="peach-pro-feature-text">1274 <span class="peach-pro-feature-name">{item.name}</span>1275 <span class="peach-pro-feature-desc">{item.desc}</span>1276 </div>1277 </div>1278 )}1279 </For>1280 </div>12811282 <p class="peach-pro-pricing">$39 · once · yours forever</p>12831284 <a1285 href="https://peachpasswords.com/#pricing"1286 target="_blank"1287 rel="noopener noreferrer"1288 class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1289 >1290 Get Peach Pro1291 </a>12921293 <Show1294 when={showKeyEntry()}1295 fallback={1296 <button1297 class="peach-pro-key-link"1298 onClick={() => setShowKeyEntry(true)}1299 >1300 Already have a key? Enter it →1301 </button>1302 }1303 >1304 <div class="peach-pro-key-entry">1305 <input1306 class="peach-underline-input peach-pro-key-input"1307 type="text"1308 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1309 value={licenseKeyInput()}1310 onInput={(e) =>1311 setLicenseKeyInput(e.currentTarget.value.toUpperCase())1312 }1313 onKeyDown={(e) => {1314 if (e.key === "Enter") void onActivate();1315 }}1316 autocomplete="off"1317 spellcheck={false}1318 />1319 <button1320 class="peach-btn peach-btn--accent peach-btn--sm"1321 onClick={() => void onActivate()}1322 disabled={activatingLicense() || !licenseKeyInput().trim()}1323 >1324 {activatingLicense() ? "activating…" : "Activate"}1325 </button>1326 <Show when={licenseError()}>1327 <p class="peach-pro-key-error">That key doesn't look right.</p>1328 </Show>1329 </div>1330 </Show>1331 </div>1332 }1333 >1334 <div class="peach-pro-active">1335 <div class="peach-pro-active-head">1336 <span class="peach-pro-active-mark" aria-hidden="true">🍑</span>1337 <h3 class="peach-pro-active-title">Peach Pro</h3>1338 </div>1339 <p class="peach-pro-active-since">1340 {formatActivatedSince(licenseInfo()?.issued)}1341 </p>1342 <div class="peach-pro-active-divider"></div>1343 <p class="peach-pro-active-thanks">Thank you.</p>1344 <button1345 class="peach-pro-deactivate"1346 onClick={() => void handleDeactivateLicense()}1347 disabled={deactivating()}1348 >1349 {deactivating() ? "Deactivating…" : "Deactivate →"}1350 </button>1351 </div>1352 </Show>1353 </div>1354 );13551356 async function onActivate() {1357 await handleActivateLicense();1358 if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {1359 await props.onLicenseChange?.();1360 }1361 }1362 }13631364 function renderFeedback() {1365 return (1366 <div class="peach-settings-body">1367 <div class="peach-settings-group peach-feedback-group">1368 <p class="peach-feedback-intro">1369 Help us improve Peach. Every report goes directly to our inbox.1370 </p>13711372 <p class="peach-settings-sub-label">TYPE</p>1373 <div class="peach-feedback-chips">1374 <For each={FEEDBACK_CATEGORIES}>1375 {(cat) => (1376 <button1377 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}1378 onClick={() => setFbCategory(cat)}1379 >1380 {cat}1381 </button>1382 )}1383 </For>1384 </div>13851386 <p class="peach-settings-sub-label">MESSAGE</p>1387 <textarea1388 class="peach-feedback-textarea"1389 placeholder="Describe the issue or idea..."1390 value={fbMessage()}1391 onInput={(e) => setFbMessage(e.currentTarget.value)}1392 />13931394 <p class="peach-feedback-meta">1395 Version {version} · will be included automatically1396 </p>13971398 <Show when={fbResult() === "success"}>1399 <p class="peach-feedback-result peach-feedback-result--ok">1400 Sent — thank you1401 </p>1402 </Show>1403 <Show when={fbResult() === "error"}>1404 <p class="peach-feedback-result peach-feedback-result--err">1405 Failed to send — try again1406 </p>1407 </Show>14081409 <button1410 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"1411 onClick={() => void handleSubmitFeedback()}1412 disabled={fbSubmitting() || !fbMessage().trim()}1413 >1414 {fbSubmitting() ? "Sending..." : "Send feedback"}1415 </button>14161417 <p class="peach-feedback-anon">1418 Sent anonymously · no account required1419 </p>1420 </div>1421 </div>1422 );1423 }14241425 return (1426 <div class="peach-overlay-view ov-enter-right" style="display:flex">1427 <div class="peach-overlay-header">1428 <button1429 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1430 onClick={handleHeaderBack}1431 aria-label="Back"1432 >1433 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1434 <path d="M6 1L1 6L6 11" />1435 </svg>1436 </button>1437 <span class="peach-overlay-title">{currentTitle()}</span>1438 <div class="peach-overlay-spacer"></div>1439 </div>14401441 <Switch fallback={renderHome()}>1442 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1443 <Match when={activeSection() === "vault"}>{renderVault()}</Match>1444 <Match when={activeSection() === "sync"}>{renderSync()}</Match>1445 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1446 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1447 <Match when={activeSection() === "license"}>{renderLicense()}</Match>1448 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1449 </Switch>1450 </div>1451 );1452}