import { useState, type ComponentType, type ReactNode } from 'react'; import { useNavigate } from '@tanstack/react-router'; import { Palette, Sun, Moon, Download, Upload, Sparkles, Cloud, ScrollText, TriangleAlert, type LucideProps, } from 'lucide-react'; import { useUiStore, type Theme } from '@/stores/uiStore'; import { exportBackup, restoreBackup, clearAllData, BackupError } from '@/lib/io/backup'; import { pickTextFile } from '@/lib/io/file'; import { seedSampleCampaign } from '@/lib/sample'; import { cloudUsername, register as cloudRegister, login as cloudLogin, logout as cloudLogout, pushBackup, pullBackup, CloudError } from '@/lib/cloud/client'; import { AssistantSettings } from './AssistantSettings'; import { CloudCampaigns } from './CloudCampaigns'; import { Page, PageHeader } from '@/components/ui/Page'; import { Button } from '@/components/ui/Button'; import { Modal } from '@/components/ui/Modal'; import { Input } from '@/components/ui/Input'; import { cn } from '@/lib/cn'; /** A Living-Codex setting card: section eyebrow + a gilt hairline over its rows. */ function SettingsCard({ label, children, className }: { label: string; children: ReactNode; className?: string }) { return (

{label}


{children}
); } /** An icon-tile row: leading rounded tile, title + description, trailing controls. */ function SettingsRow({ icon: Icon, title, desc, children, }: { icon: ComponentType; title: ReactNode; desc?: ReactNode; children?: ReactNode; }) { return (
{title}
{desc &&
{desc}
}
{children &&
{children}
}
); } export function SettingsPage() { const theme = useUiStore((s) => s.theme); const setTheme = useUiStore((s) => s.setTheme); const setActiveCampaign = useUiStore((s) => s.setActiveCampaign); const navigate = useNavigate(); const [msg, setMsg] = useState(null); const [confirmRestore, setConfirmRestore] = useState(null); const [confirmClear, setConfirmClear] = useState(false); const doImport = async () => { const text = await pickTextFile(); if (text) setConfirmRestore(text); }; const runRestore = async () => { if (!confirmRestore) return; try { await restoreBackup(confirmRestore); setMsg('Backup restored.'); } catch (e) { setMsg(e instanceof BackupError ? e.message : 'Restore failed.'); } setConfirmRestore(null); }; const loadSample = async () => { const id = await seedSampleCampaign(); setActiveCampaign(id); void navigate({ to: '/dashboard' }); }; return (
{(['dark', 'light'] as Theme[]).map((t) => ( ))} {msg &&

{msg}

}
  • D&D 5e — SRD content via Open5e (OGL 1.0a / CC-BY-4.0).
  • Pathfinder 2e — content from the Foundry VTT pf2e project & Archives of Nethys (OGL / ORC / Paizo Community Use).
  • Map import follows the Universal VTT (.dd2vtt) format used by Dungeondraft, Foundry & others.

Danger zone

Permanently delete all campaigns and data on this device.

setConfirmRestore(null)} title="Restore backup?" footer={ <> } >

Restoring replaces all current data with the backup. Export first if unsure.

setConfirmClear(false)} title="Clear all data?" footer={ <> } >

This deletes every campaign, character, encounter, note, map, and homebrew entry. This cannot be undone.

); } function CloudSync() { const [user, setUser] = useState(cloudUsername()); const [u, setU] = useState(''); const [p, setP] = useState(''); const [msg, setMsg] = useState(null); const [busy, setBusy] = useState(false); const [confirmPull, setConfirmPull] = useState(false); const run = async (fn: () => Promise) => { setBusy(true); setMsg(null); try { await fn(); } catch (e) { setMsg(e instanceof CloudError ? e.message : 'Something went wrong.'); } finally { setBusy(false); } }; const auth = (which: 'in' | 'up') => run(async () => { const r = await (which === 'in' ? cloudLogin(u, p) : cloudRegister(u, p)); setUser(r.username); setP(''); setMsg(which === 'up' ? 'Account created.' : 'Signed in.'); }); const push = () => run(async () => { const r = await pushBackup(); if (r.ok) { setMsg(`Backed up to cloud (${Math.round(r.bytes / 1024)} KB).`); return; } // Conflict: the cloud changed on another device since this one last synced. const useCloud = window.confirm('The cloud copy was changed on another device since you last synced.\n\nOK — replace THIS device with the cloud copy.\nCancel — overwrite the CLOUD with this device.'); if (useCloud) { const ok = await pullBackup(); setMsg(ok ? 'Pulled the cloud copy — reloading…' : 'No cloud backup.'); if (ok) setTimeout(() => location.reload(), 600); } else { await pushBackup({ force: true }); setMsg('Overwrote the cloud with this device.'); } }); const pull = () => run(async () => { const ok = await pullBackup(); setMsg(ok ? 'Restored — reloading…' : 'No cloud backup yet.'); if (ok) setTimeout(() => location.reload(), 600); }); const signOut = () => run(async () => { await cloudLogout(); setUser(null); setMsg(null); }); return ( {user && Signed in as {user}} {user ? (
) : (
setU(e.target.value)} aria-label="Cloud username" /> setP(e.target.value)} aria-label="Cloud password" />
)} {msg &&

{msg}

} setConfirmPull(false)} title="Restore from cloud?" footer={<>}>

This replaces all data on this device with your cloud backup.

); }