Build MVP: campaigns, characters, combat, dice, compendium

- Rules abstraction (5e + pf2e) behind one interface, fully tested
- Pure combat engine: turn-order safe across add/remove/reorder/init-change
- Dexie storage with real cascade deletes + Zod validation on write
- Seedable dice engine with notation parser
- Lazy SRD compendium (code-split), bestiary -> combat
- Strict TS, 54 unit tests, Playwright e2e smoke (all green)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 00:09:42 +02:00
parent fe84dc365d
commit 1a9e5e2c18
93 changed files with 412052 additions and 9 deletions
+41
View File
@@ -0,0 +1,41 @@
import { cn } from '@/lib/cn';
/** Integer input that never emits NaN — coerces blanks/garbage to a fallback. */
export function NumberField({
value,
onChange,
min,
max,
className,
'aria-label': ariaLabel,
}: {
value: number;
onChange: (n: number) => void;
min?: number;
max?: number;
className?: string;
'aria-label'?: string;
}) {
return (
<input
type="number"
inputMode="numeric"
aria-label={ariaLabel}
value={Number.isFinite(value) ? value : 0}
min={min}
max={max}
onChange={(e) => {
const raw = Number(e.target.value);
let n = Number.isFinite(raw) ? Math.trunc(raw) : 0;
if (min !== undefined) n = Math.max(min, n);
if (max !== undefined) n = Math.min(max, n);
onChange(n);
}}
className={cn(
'w-full rounded-md border border-line bg-surface px-2 py-1.5 text-center text-sm text-ink',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60',
className,
)}
/>
);
}