Phase 10: command palette, settings, backup/restore, sample campaign

- Command palette (Ctrl/Cmd+K): fuzzy search over pages + active-campaign
  characters/notes/NPCs/quests + actions; keyboard nav; header ⌘K button
- Settings page: theme, full backup export/import (restore replaces all in one
  transaction), load sample campaign, danger-zone clear-all
- Backup lib (src/lib/io/backup.ts) snapshots every table; sample seeder
  (src/lib/sample.ts) builds a demo campaign
- Header gets ⌘K + settings gear; routes + nav wired

Backup round-trip unit tests + command-palette/settings e2e.
(Deferred from this phase, noted in plan: audio, Discord/Foundry/Roll20, i18n,
push notifications.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 08:00:09 +02:00
parent 7100ea8dd4
commit bd6bb240a1
9 changed files with 400 additions and 1 deletions
+20
View File
@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react';
import { Link, Outlet, useRouterState } from '@tanstack/react-router';
import { useUiStore } from '@/stores/uiStore';
import { CommandPalette } from '@/components/CommandPalette';
import { useActiveCampaign, useCampaigns } from '@/features/campaigns/hooks';
import { cn } from '@/lib/cn';
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
@@ -53,6 +55,19 @@ function ThemeToggle() {
export function RootLayout() {
const pathname = useRouterState({ select: (s) => s.location.pathname });
const [paletteOpen, setPaletteOpen] = useState(false);
// Global Ctrl/Cmd+K opens the command palette.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
setPaletteOpen((v) => !v);
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
return (
<div className="flex h-full flex-col">
@@ -78,6 +93,10 @@ export function RootLayout() {
})}
</nav>
<div className="ml-auto flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={() => setPaletteOpen(true)} title="Command palette (Ctrl/Cmd+K)" aria-label="Open command palette">
K
</Button>
<Link to="/settings" className="rounded-md p-1.5 text-sm text-muted hover:text-ink" title="Settings" aria-label="Settings"></Link>
<CampaignSwitcher />
<ThemeToggle />
</div>
@@ -90,6 +109,7 @@ export function RootLayout() {
</main>
<RollTray />
{paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} />}
</div>
);
}