1import { createSignal, For, onMount, Show } 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";67interface LicenseInfo {8 valid: boolean;9 plan: "free" | "pro";10 issued?: string;11}1213interface SettingsProps {14 entriesCount: number;15 breachMonitoring: boolean;16 lockOnClose: boolean;17 autoLockMinutes: number;18 trashRetentionDays: number;19 theme: "dark" | "light";20 accentColor: AccentColorName;21 onBreachMonitoringChange: (enabled: boolean) => void | Promise<void>;22 onLockOnCloseChange: (enabled: boolean) => void | Promise<void>;23 onAutoLockMinutesChange: (minutes: number) => void | Promise<void>;24 onTrashRetentionChange: (days: number) => void | Promise<void>;25 onThemeChange: (theme: "dark" | "light") => void;26 onAccentChange: (name: AccentColorName) => void | Promise<void>;27 onBack: () => void;28 onImport: () => void;29 onExport: () => void;30 onBreachCheckDone: () => void;31 onOpenHealth?: () => void;32 onOpenSyncPairing?: (mode: "initiate" | "accept") => void;33 onOpenCJKImport?: () => void;34 onOpenFileImport?: () => void;35}3637export function SettingsView(props: SettingsProps) {38 const version = chrome.runtime.getManifest().version;39 const [checkingBreaches, setCheckingBreaches] = createSignal(false);40 const [breachResult, setBreachResult] = createSignal<string | null>(null);41 const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);42 const [licenseKeyInput, setLicenseKeyInput] = createSignal("");43 const [licenseError, setLicenseError] = createSignal<string | null>(null);44 const [licenseSuccess, setLicenseSuccess] = createSignal<string | null>(null);45 const [activatingLicense, setActivatingLicense] = createSignal(false);4647 const [pairedDevices, setPairedDevices] = createSignal<PairedDevice[]>([]);48 const [trustedNetworks, setTrustedNetworks] = createSignal<TrustedNetwork[]>([]);49 const [syncStatus, setSyncStatus] = createSignal<SyncStatus>("offline");50 const [lastSyncAt, setLastSyncAt] = createSignal<number | undefined>(undefined);51 const [isPro, setIsPro] = createSignal(false);52 const [syncingNow, setSyncingNow] = createSignal(false);53 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);54 const [passkeySaving, setPasskeySaving] = createSignal(false);5556 57 const [dedupCount, setDedupCount] = createSignal<number | null>(null);58 const [dedupRunning, setDedupRunning] = createSignal(false);59 const [dedupDone, setDedupDone] = createSignal<number | null>(null);6061 62 const [showChangePw, setShowChangePw] = createSignal(false);63 const [changePwCurrent, setChangePwCurrent] = createSignal("");64 const [changePwNew, setChangePwNew] = createSignal("");65 const [changePwConfirm, setChangePwConfirm] = createSignal("");66 const [changePwSaving, setChangePwSaving] = createSignal(false);67 const [changePwError, setChangePwError] = createSignal<string | null>(null);68 const [changePwSuccess, setChangePwSuccess] = createSignal(false);6970 async function handleChangePassword() {71 setChangePwError(null);72 if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }73 if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; }74 if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; }75 if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; }76 setChangePwSaving(true);77 const res = await sendMessage({78 type: "CHANGE_PASSWORD",79 payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },80 }) as { ok: boolean; error?: string };81 if (res.ok) {82 setChangePwSuccess(true);83 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");84 setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);85 } else {86 setChangePwError(res.error ?? "failed to change password");87 }88 setChangePwSaving(false);89 }9091 92 const [duressEnabled, setDuressEnabled] = createSignal(false);93 const [showDuressSetup, setShowDuressSetup] = createSignal(false);94 const [duressPassword, setDuressPassword] = createSignal("");95 const [duressConfirm, setDuressConfirm] = createSignal("");96 const [duressMainPassword, setDuressMainPassword] = createSignal("");97 const [duressSaving, setDuressSaving] = createSignal(false);98 const [duressError, setDuressError] = createSignal<string | null>(null);99 const [duressSuccess, setDuressSuccess] = createSignal<string | null>(null);100101 onMount(async () => {102 const [licenseRes, syncRes, passkeyRes, duressRes] = await Promise.all([103 sendMessage({ type: "GET_LICENSE" }),104 sendMessage({ type: "GET_SYNC_STATE" }),105 sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }),106 sendMessage({ type: "IS_DURESS_ENABLED" }),107 ]);108109 if ((licenseRes as { ok: boolean }).ok) {110 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);111 }112113 if ((syncRes as { ok: boolean }).ok) {114 const data = syncRes as { ok: boolean; data?: {115 pairedDevices?: PairedDevice[];116 trustedNetworks?: TrustedNetwork[];117 status?: SyncStatus;118 lastSyncAt?: number;119 isPro?: boolean;120 }};121 if (data.data) {122 setPairedDevices(data.data.pairedDevices ?? []);123 setTrustedNetworks(data.data.trustedNetworks ?? []);124 setSyncStatus(data.data.status ?? "offline");125 setLastSyncAt(data.data.lastSyncAt);126 setIsPro(data.data.isPro ?? false);127 }128 }129130 if ((passkeyRes as { ok: boolean }).ok) {131 setPasskeyState(132 (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null133 );134 }135136 if ((duressRes as { ok: boolean }).ok) {137 setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false);138 }139140 chrome.runtime.onMessage.addListener((msg) => {141 if (msg.type === "SYNC_STATE_UPDATE") {142 setPairedDevices(msg.pairedDevices ?? []);143 setTrustedNetworks(msg.trustedNetworks ?? []);144 setSyncStatus(msg.status ?? "offline");145 setLastSyncAt(msg.lastSyncAt);146 setIsPro(msg.isPro ?? false);147 }148 });149 });150151 async function handleActivateLicense() {152 const key = licenseKeyInput().trim();153 if (!key) return;154 setActivatingLicense(true);155 setLicenseError(null);156 setLicenseSuccess(null);157 const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } });158 setActivatingLicense(false);159 if ((res as { ok: boolean }).ok) {160 const data = (res as { ok: boolean; data?: LicenseInfo }).data;161 if (data?.valid) {162 setLicenseInfo(data);163 setLicenseSuccess("License activated!");164 setLicenseKeyInput("");165 } else {166 setLicenseError("Invalid license key");167 }168 } else {169 setLicenseError("Activation failed");170 }171 }172173 async function handleTogglePasskeys() {174 const current = passkeyState();175 if (!current || !current.supported) return;176 setPasskeySaving(true);177 const res = await sendMessage({178 type: "SET_PASSKEY_PROVIDER_ENABLED",179 payload: { enabled: !current.enabled },180 });181 setPasskeySaving(false);182 if ((res as { ok: boolean }).ok) {183 setPasskeyState(184 (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null185 );186 }187 }188189 function maskLicenseKey(key: string): string {190 if (key.length < 10) return key.slice(0, 4) + "****";191 return key.slice(0, 6) + "****" + key.slice(-4);192 }193194 async function handleCheckAllBreaches() {195 setCheckingBreaches(true);196 setBreachResult(null);197 const res = await sendMessage({ type: "CHECK_ALL_BREACHES" });198 setCheckingBreaches(false);199 if ((res as { ok: boolean }).ok) {200 const data = (res as { ok: boolean; data?: Record<string, number> }).data ?? {};201 const breached = Object.values(data).filter((count) => count > 0).length;202 setBreachResult(203 breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean"204 );205 props.onBreachCheckDone();206 }207 }208209 async function handleSyncNow() {210 if (syncStatus() === "not_trusted") return;211 setSyncingNow(true);212 await sendMessage({ type: "TRIGGER_SYNC" });213 setTimeout(() => setSyncingNow(false), 3000);214 }215216 async function handleRemoveDevice(deviceId: string) {217 await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });218 }219220 async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {221 await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });222 }223224 async function handleMarkNetworkTrusted() {225 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });226 }227228 function formatRelativeTime(timestamp?: number): string {229 if (!timestamp) return "Never";230 const diff = Date.now() - timestamp;231 const minutes = Math.floor(diff / 60000);232 const hours = Math.floor(diff / 3600000);233 const days = Math.floor(diff / 86400000);234 if (minutes < 1) return "Just now";235 if (minutes < 60) return `${minutes}m ago`;236 if (hours < 24) return `${hours}h ago`;237 return `${days}d ago`;238 }239240 function getStatusColor(status: SyncStatus): string {241 switch (status) {242 case "synced": return "var(--peach-sync-ok)";243 case "syncing": return "var(--peach-sync-busy)";244 case "no_peers": return "var(--peach-sync-off)";245 case "not_trusted": return "var(--peach-sync-err)";246 case "offline": return "var(--peach-sync-off)";247 default: return "var(--peach-sync-off)";248 }249 }250251 function getStatusLabel(status: SyncStatus): string {252 switch (status) {253 case "synced": return "Synced";254 case "syncing": return "Syncing";255 case "no_peers": return "No peers visible";256 case "not_trusted": return "Not on a trusted network";257 case "offline": return "Offline";258 default: return "Unknown";259 }260 }261262 function isCurrentNetworkTrusted(): boolean {263 264 265 return trustedNetworks().length > 0;266 }267268 async function handleSetupDuress() {269 const mainPw = duressMainPassword().trim();270 const duressPw = duressPassword().trim();271 const confirmPw = duressConfirm().trim();272273 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }274 if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; }275 if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; }276 if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; }277278 setDuressSaving(true);279 setDuressError(null);280281 282 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };283 if (!verifyRes.ok || !verifyRes.data) {284 setDuressError("incorrect main password");285 setDuressSaving(false);286 return;287 }288289 const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string };290 setDuressSaving(false);291 if (res.ok) {292 setDuressEnabled(true);293 setShowDuressSetup(false);294 setDuressPassword("");295 setDuressConfirm("");296 setDuressMainPassword("");297 setDuressSuccess("Duress password set. If entered, a decoy vault will be shown.");298 setTimeout(() => setDuressSuccess(null), 5000);299 } else {300 setDuressError(res.error ?? "failed to set duress password");301 }302 }303304 async function handleCountDuplicates() {305 setDedupRunning(true);306 setDedupDone(null);307 const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number };308 setDedupRunning(false);309 if (res.ok) setDedupCount(res.data ?? 0);310 }311312 async function handleDeduplicate() {313 setDedupRunning(true);314 const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number };315 setDedupRunning(false);316 if (res.ok) {317 setDedupDone(res.data ?? 0);318 setDedupCount(0);319 }320 }321322 async function handleDisableDuress() {323 const mainPw = duressMainPassword().trim();324 if (!mainPw) { setDuressError("enter your main password to confirm"); return; }325326 setDuressSaving(true);327 setDuressError(null);328329 const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean };330 if (!verifyRes.ok || !verifyRes.data) {331 setDuressError("incorrect main password");332 setDuressSaving(false);333 return;334 }335336 const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string };337 setDuressSaving(false);338 if (res.ok) {339 setDuressEnabled(false);340 setShowDuressSetup(false);341 setDuressMainPassword("");342 setDuressSuccess("Duress password disabled.");343 setTimeout(() => setDuressSuccess(null), 4000);344 } else {345 setDuressError(res.error ?? "failed to disable duress password");346 }347 }348349 function renderSyncSection() {350 if (!isPro()) {351 return (352 <div class="peach-settings-group" style="position:relative">353 <p class="peach-settings-label">sync</p>354 <div class="peach-sync-pro-gate">355 <div class="peach-sync-pro-gate-blur">356 <div class="peach-settings-row">357 <span>Status</span>358 <span class="peach-settings-value peach-settings-value--status">359 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>360 {getStatusLabel(syncStatus())}361 </span>362 </div>363 <div class="peach-settings-row peach-settings-row--meta">364 <span>Last sync</span>365 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>366 </div>367 </div>368 <div class="peach-sync-pro-gate-overlay">369 <span class="peach-sync-pro-badge">Pro</span>370 <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">371 Upgrade to Pro →372 </a>373 </div>374 </div>375 </div>376 );377 }378379 return (380 <div class="peach-settings-group">381 <p class="peach-settings-label">sync</p>382 <div class="peach-settings-row">383 <span>Status</span>384 <span class="peach-settings-value peach-settings-value--status">385 <span class="peach-sync-status-dot" style={{ background: getStatusColor(syncStatus()) }}></span>386 {getStatusLabel(syncStatus())}387 </span>388 </div>389 <div class="peach-settings-row peach-settings-row--meta">390 <span>Last sync</span>391 <span class="peach-settings-value">{lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}</span>392 </div>393 <div class="peach-settings-row peach-settings-row--meta">394 <button395 class="peach-btn peach-btn--subtle peach-btn--sm"396 onClick={() => void handleSyncNow()}397 disabled={syncStatus() === "not_trusted" || syncStatus() === "syncing"}398 >399 {syncingNow() ? "Syncing..." : "Sync now"}400 </button>401 </div>402403 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">404 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>405 {pairedDevices().map((device) => (406 <div class="peach-sync-device-row">407 <div class="peach-sync-device-info">408 <span class="peach-sync-device-name">{device.displayName}</span>409 <span class="peach-sync-device-meta">410 Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}411 </span>412 </div>413 <button414 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"415 onClick={() => void handleRemoveDevice(device.deviceId)}416 >417 Remove418 </button>419 </div>420 ))}421 <div class="peach-settings-actions--row">422 <button423 class="peach-btn peach-btn--subtle peach-btn--sm"424 onClick={() => props.onOpenSyncPairing?.("initiate")}425 >426 Pair a new device427 </button>428 <button429 class="peach-btn peach-btn--subtle peach-btn--sm"430 onClick={() => props.onOpenSyncPairing?.("accept")}431 >432 Pair with another device433 </button>434 </div>435 </div>436437 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">438 <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>439 {trustedNetworks().filter(n => !n.isTailscale).map((network) => (440 <div class="peach-sync-device-row">441 <span class="peach-sync-device-name">{network.ssid ?? "Unknown network"}</span>442 <button443 class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"444 onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}445 >446 Remove447 </button>448 </div>449 ))}450 <div class="peach-settings-actions--row">451 {!isCurrentNetworkTrusted() && (452 <button453 class="peach-btn peach-btn--subtle peach-btn--sm"454 onClick={() => void handleMarkNetworkTrusted()}455 >456 Mark current network as trusted457 </button>458 )}459 </div>460 <span class="peach-sync-device-meta">461 Tailscale is always trusted462 </span>463 </div>464 </div>465 );466 }467468 return (469 <div class="peach-overlay-view ov-enter-right" style="display:flex">470 <div class="peach-overlay-header">471 <button472 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"473 onClick={props.onBack}474 aria-label="Back"475 >476 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">477 <path d="M6 1L1 6L6 11" />478 </svg>479 </button>480 <span class="peach-overlay-title">settings</span>481 <div class="peach-overlay-spacer"></div>482 </div>483484 <div class="peach-settings-body">485 <div class="peach-settings-group">486 <p class="peach-settings-label">security</p>487 <div class="peach-settings-row">488 <span>Lock on close</span>489 <button490 class={`peach-toggle ${props.lockOnClose ? "is-on" : ""}`}491 onClick={() => void props.onLockOnCloseChange(!props.lockOnClose)}492 aria-label="Toggle lock on close"493 >494 <span class="peach-toggle-knob"></span>495 </button>496 </div>497 <div class="peach-settings-row">498 <span>Auto-lock after</span>499 <select500 class="peach-settings-select"501 value={String(props.autoLockMinutes)}502 onChange={(e) => void props.onAutoLockMinutesChange(Number(e.currentTarget.value))}503 >504 <option value="1">1 minute</option>505 <option value="5">5 minutes</option>506 <option value="15">15 minutes</option>507 <option value="30">30 minutes</option>508 <option value="0">Never</option>509 </select>510 </div>511 <div class="peach-settings-row">512 <span>Breach monitoring</span>513 <button514 class={`peach-toggle ${props.breachMonitoring ? "is-on" : ""}`}515 onClick={() =>516 void props.onBreachMonitoringChange(!props.breachMonitoring)517 }518 aria-label="Toggle breach monitoring"519 >520 <span class="peach-toggle-knob"></span>521 </button>522 </div>523 <div class="peach-settings-row peach-settings-row--meta">524 <span>Check all passwords</span>525 <button526 class="peach-btn peach-btn--subtle peach-btn--sm"527 onClick={() => void handleCheckAllBreaches()}528 disabled={checkingBreaches()}529 >530 {checkingBreaches() ? "checking..." : "run"}531 </button>532 </div>533 {breachResult() && (534 <div class="peach-settings-row peach-settings-row--meta">535 <span>Last check</span>536 <span class="peach-settings-value">{breachResult()}</span>537 </div>538 )}539 {}540 <div class="peach-settings-row peach-settings-row--meta">541 <span>Master password</span>542 <button543 class="peach-btn peach-btn--subtle peach-btn--sm"544 onClick={() => { setShowChangePw((v) => !v); setChangePwError(null); }}545 >546 {showChangePw() ? "cancel" : "change"}547 </button>548 </div>549 <Show when={showChangePw()}>550 <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">551 <input552 class="peach-settings-input"553 type="password"554 placeholder="current password"555 value={changePwCurrent()}556 onInput={(e) => setChangePwCurrent((e.target as HTMLInputElement).value)}557 autocomplete="current-password"558 />559 <input560 class="peach-settings-input"561 type="password"562 placeholder="new password (min 12 chars)"563 value={changePwNew()}564 onInput={(e) => setChangePwNew((e.target as HTMLInputElement).value)}565 autocomplete="new-password"566 />567 <input568 class="peach-settings-input"569 type="password"570 placeholder="confirm new password"571 value={changePwConfirm()}572 onInput={(e) => setChangePwConfirm((e.target as HTMLInputElement).value)}573 autocomplete="new-password"574 onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}575 />576 <Show when={changePwError()}>577 <p class="peach-hint peach-hint-error" style="margin:0">{changePwError()}</p>578 </Show>579 <Show when={changePwSuccess()}>580 <p class="peach-hint" style="margin:0;color:var(--peach-accent)">password changed ✓</p>581 </Show>582 <button583 class="peach-btn peach-btn--accent peach-btn--sm"584 style="align-self:flex-start"585 onClick={() => void handleChangePassword()}586 disabled={changePwSaving() || !changePwCurrent() || !changePwNew() || !changePwConfirm()}587 >588 {changePwSaving() ? "saving…" : "save new password"}589 </button>590 </div>591 </Show>592 </div>593594 {}595 <div class="peach-settings-group">596 <p class="peach-settings-label">duress password</p>597 <div class="peach-settings-row peach-settings-row--top">598 <div class="peach-settings-row-main">599 <span>Duress password</span>600 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">601 If entered under coercion, shows a decoy empty vault instead of your real one.602 </span>603 </div>604 <div class={`peach-duress-status-pill ${duressEnabled() ? "is-active" : ""}`}>605 {duressEnabled() ? "on" : "off"}606 </div>607 </div>608609 <Show when={duressEnabled() && !showDuressSetup()}>610 <div class="peach-settings-row peach-settings-row--meta">611 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">612 If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.613 </span>614 </div>615 </Show>616617 <Show when={duressSuccess()}>618 <div class="peach-settings-row peach-settings-row--meta">619 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">{duressSuccess()}</span>620 </div>621 </Show>622623 <Show when={!showDuressSetup()}>624 <div class="peach-settings-row peach-settings-row--meta">625 <button626 class="peach-btn peach-btn--subtle peach-btn--sm"627 onClick={() => { setShowDuressSetup(true); setDuressError(null); setDuressSuccess(null); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); }}628 >629 {duressEnabled() ? "Change / disable" : "Set up"}630 </button>631 </div>632 </Show>633634 <Show when={showDuressSetup()}>635 <div class="peach-duress-form">636 <label class="peach-label">main password (to confirm)</label>637 <input638 class="peach-settings-input"639 type="password"640 placeholder="your current master password"641 autocomplete="current-password"642 value={duressMainPassword()}643 onInput={(e) => setDuressMainPassword(e.currentTarget.value)}644 />645 <Show when={!duressEnabled()}>646 <label class="peach-label">new duress password</label>647 <input648 class="peach-settings-input"649 type="password"650 placeholder="duress password (min 8 chars)"651 autocomplete="new-password"652 value={duressPassword()}653 onInput={(e) => setDuressPassword(e.currentTarget.value)}654 />655 <label class="peach-label">confirm duress password</label>656 <input657 class="peach-settings-input"658 type="password"659 placeholder="confirm duress password"660 autocomplete="new-password"661 value={duressConfirm()}662 onInput={(e) => setDuressConfirm(e.currentTarget.value)}663 />664 </Show>665666 <Show when={duressError()}>667 <p class="peach-hint peach-hint-error">{duressError()}</p>668 </Show>669670 <div class="peach-duress-actions">671 <button672 class="peach-btn peach-btn--subtle peach-btn--sm"673 onClick={() => { setShowDuressSetup(false); setDuressError(null); }}674 >675 cancel676 </button>677 <Show when={duressEnabled()}>678 <button679 class="peach-btn peach-btn--ghost peach-btn--sm"680 style="color:var(--peach-breach)"681 onClick={() => void handleDisableDuress()}682 disabled={duressSaving()}683 >684 {duressSaving() ? "disabling..." : "disable duress"}685 </button>686 </Show>687 <Show when={!duressEnabled()}>688 <button689 class="peach-btn peach-btn--accent peach-btn--sm"690 onClick={() => void handleSetupDuress()}691 disabled={duressSaving() || !duressMainPassword() || !duressPassword() || !duressConfirm()}692 >693 {duressSaving() ? "saving..." : "save"}694 </button>695 </Show>696 </div>697 </div>698 </Show>699 </div>700701 <div class="peach-settings-group">702 <p class="peach-settings-label">passkeys</p>703 <div class="peach-settings-row peach-settings-row--top">704 <div class="peach-settings-row-main">705 <span>Intercept passkey requests</span>706 <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--t2)">707 Peach can handle passkey sign-in and creation on supported Chromium browsers.708 </span>709 </div>710 <button711 class={`peach-toggle ${passkeyState()?.enabled ? "is-on" : ""}`}712 onClick={() => void handleTogglePasskeys()}713 aria-label="Toggle passkey interception"714 disabled={!passkeyState()?.supported || passkeySaving()}715 >716 <span class="peach-toggle-knob"></span>717 </button>718 </div>719 <div class="peach-settings-row peach-settings-row--meta">720 <span>Status</span>721 <span class="peach-settings-value">{passkeyState()?.statusLabel ?? "Unavailable"}</span>722 </div>723 <div class="peach-settings-row peach-settings-row--meta">724 <span>Browser</span>725 <span class="peach-settings-value">726 {passkeyState()?.browserLabel ?? "Unknown"}727 {passkeyState()?.supported ? " \u2713" : " (interception unavailable)"}728 </span>729 </div>730 <div class="peach-info-notice" style="margin-top:8px">731 Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.732 </div>733 {passkeyState()?.conflict && (734 <div class="peach-settings-row peach-settings-row--meta">735 <span class="peach-settings-value" style="color:#e7b36b">{passkeyState()!.conflict}</span>736 </div>737 )}738 </div>739740 {renderSyncSection()}741742 <div class="peach-settings-group">743 <p class="peach-settings-label">vault</p>744 <div class="peach-settings-row peach-settings-row--meta">745 <span>Entries</span>746 <span class="peach-settings-value">{props.entriesCount}</span>747 </div>748 <div class="peach-settings-row peach-settings-row--meta">749 <span>Duplicates</span>750 <Show when={dedupDone() !== null} fallback={751 <button752 class="peach-btn peach-btn--subtle peach-btn--sm"753 onClick={() => void handleCountDuplicates()}754 disabled={dedupRunning()}755 >756 {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`}757 </button>758 }>759 <span class="peach-settings-value" style="color:var(--peach-sync-ok)">760 {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}761 </span>762 </Show>763 </div>764 <Show when={dedupCount() !== null && dedupCount()! > 0 && dedupDone() === null}>765 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">766 <div class="peach-restore-warning">767 {dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.768 </div>769 <div style="display:flex;gap:6px">770 <button771 class="peach-btn peach-btn--subtle peach-btn--sm"772 onClick={() => setDedupCount(null)}773 >774 cancel775 </button>776 <button777 class="peach-btn peach-btn--accent peach-btn--sm"778 onClick={() => void handleDeduplicate()}779 disabled={dedupRunning()}780 >781 {dedupRunning() ? "removing..." : "remove duplicates"}782 </button>783 </div>784 </div>785 </Show>786 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--accent">787 <span>Accent</span>788 <div class="peach-accent-picker">789 <For each={ACCENT_COLORS}>790 {(color) => (791 <button792 class="peach-accent-swatch"793 classList={{ "is-active": props.accentColor === color.name }}794 style={{ background: color.hex }}795 onClick={() => void props.onAccentChange(color.name)}796 aria-label={color.name}797 title={color.name}798 />799 )}800 </For>801 </div>802 </div>803 <div class="peach-settings-row peach-settings-row--meta">804 <span>Theme</span>805 <button806 class="peach-btn peach-btn--subtle peach-btn--sm"807 onClick={() =>808 props.onThemeChange(props.theme === "dark" ? "light" : "dark")809 }810 >811 {props.theme === "dark" ? "Switch to light" : "Switch to dark"}812 </button>813 </div>814 <div class="peach-settings-row peach-settings-row--meta">815 <span>Trash auto-delete</span>816 <select817 class="peach-settings-select"818 value={String(props.trashRetentionDays)}819 onChange={(e) => void props.onTrashRetentionChange(Number(e.currentTarget.value))}820 >821 <option value="7">7 days</option>822 <option value="30">30 days</option>823 <option value="90">90 days</option>824 <option value="0">Never</option>825 </select>826 </div>827 <div class="peach-settings-row peach-settings-row--meta">828 <span>Import passwords</span>829 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onImport}>830 open831 </button>832 </div>833 <div class="peach-settings-row peach-settings-row--meta">834 <span>Export backup</span>835 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onExport}>836 open837 </button>838 </div>839 <div class="peach-settings-row peach-settings-row--meta">840 <span>Vault Health</span>841 <button class="peach-btn peach-btn--subtle peach-btn--sm" onClick={props.onOpenHealth}>842 open843 </button>844 </div>845 <div class="peach-settings-row peach-settings-row--meta" style="flex-direction:column;align-items:stretch;gap:6px">846 <span>Replace vault from backup</span>847 <div class="peach-restore-warning" style="margin-top:2px">848 This will permanently replace your current vault with the backup.849 </div>850 <div style="display:flex;gap:6px">851 <button852 class="peach-btn peach-btn--subtle peach-btn--sm"853 onClick={() => props.onOpenCJKImport?.()}854 >855 CJK phrase856 </button>857 <button858 class="peach-btn peach-btn--subtle peach-btn--sm"859 onClick={() => props.onOpenFileImport?.()}860 >861 backup file862 </button>863 </div>864 </div>865 </div>866867 <div class="peach-settings-group">868 <p class="peach-settings-label">license</p>869 {licenseInfo()?.valid && licenseInfo()?.plan === "pro" ? (870 <div class="peach-settings-row peach-settings-row--meta">871 <span>Peach Pro</span>872 <span class="peach-settings-value">873 activated {licenseInfo()?.issued ? new Date(licenseInfo()!.issued!).toLocaleDateString() : ""}874 </span>875 </div>876 ) : (877 <>878 <div class="peach-settings-row" style="flex-direction:column;align-items:stretch;gap:8px">879 <input880 class="peach-underline-input"881 type="text"882 placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"883 value={licenseKeyInput()}884 onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}885 style="width:100%;box-sizing:border-box"886 />887 <button888 class="peach-btn peach-btn--subtle peach-btn--sm"889 onClick={() => void handleActivateLicense()}890 disabled={activatingLicense() || !licenseKeyInput().trim()}891 >892 {activatingLicense() ? "activating..." : "Activate"}893 </button>894 </div>895 {licenseError() && (896 <div class="peach-settings-row peach-settings-row--meta">897 <span class="peach-settings-value" style="color:#e55">{licenseError()}</span>898 </div>899 )}900 {licenseSuccess() && (901 <div class="peach-settings-row peach-settings-row--meta">902 <span class="peach-settings-value" style="color:#5e5">{licenseSuccess()}</span>903 </div>904 )}905 <div class="peach-settings-row peach-settings-row--meta">906 <a907 href="https://peachpasswords.com/#pricing"908 target="_blank"909 rel="noopener noreferrer"910 class="peach-link"911 style="color:var(--peach-accent)"912 >913 Purchase →914 </a>915 </div>916 </>917 )}918 </div>919920 {}921 <div class="peach-settings-group">922 <p class="peach-settings-label">keyboard shortcuts</p>923 <div class="peach-shortcuts-grid">924 <span class="peach-shortcut-key">C</span><span class="peach-shortcut-desc">copy password</span>925 <span class="peach-shortcut-key">U</span><span class="peach-shortcut-desc">copy username</span>926 <span class="peach-shortcut-key">/</span><span class="peach-shortcut-desc">search vault</span>927 <span class="peach-shortcut-key">↑ ↓</span><span class="peach-shortcut-desc">navigate entries</span>928 <span class="peach-shortcut-key">Esc</span><span class="peach-shortcut-desc">back / close</span>929 <span class="peach-shortcut-key">⌘⇧P</span><span class="peach-shortcut-desc">autofill on page</span>930 </div>931 </div>932933 <div class="peach-settings-footer">934 <span>Peach {version}</span>935 <span class="peach-divider"></span>936 <span>Browser extension</span>937 </div>938 </div>939 </div>940 );941}