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:
@@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user