Polish sprint 1/2: restore crash fix, pf2e parity, full glyph purge

Crash fix:
- restoreBackup (file + cloud pull) now validates each row through its Zod
  schema, backfilling new fields — an older backup no longer lands characters
  missing feats/concentration/etc and crashing the sheet. + test.

pf2e parity (closing feature gaps vs 5e):
- +9 missing classes (Animist, Exemplar, Gunslinger, Inventor, Kineticist,
  Magus, Psychic, Summoner, Thaumaturge) with data + class tips.
- the wizard now enriches pf2e classes with their caster ability, so pf2e
  spellcasters get the Spells step + "Caster" tag like 5e.
- editable Perception rank and spellcasting proficiency on the pf2e sheet
  (fixes wrong initiative / spell DC at higher levels); rank-10 spell slots.
- pf2e monster resistances/weaknesses/immunities now apply in combat (flat
  amounts: resistance subtracts, weakness adds, 'all' matches any type). + tests.

Glyph purge (finishing the emoji→Lucide migration): replaced ~40 leftover
text-glyphs (✕ − + ✦ 🤫 ⬆ ⬇ ☑ ☐ ▶ ◀ ‹ › ★ ↻ ▸ ↑ ●) with Lucide icons across
Modal (every dialog), the sheet sections, dice, settings, player panels, world
pages, map editor, roll tray, and session UI.

Gate: 293 unit + e2e green; tsc + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 11:29:36 +02:00
parent f75b50adb8
commit dd694477b2
38 changed files with 355 additions and 84 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, type ReactNode } from 'react';
import { X } from 'lucide-react';
import { cn } from '@/lib/cn';
import { Button } from './Button';
@@ -82,7 +83,7 @@ export function Modal({ open, onClose, title, children, footer, className }: Mod
<div className="flex items-center justify-between border-b border-line px-5 py-3">
<h2 className="text-lg font-display font-semibold text-ink">{title}</h2>
<Button size="icon" variant="ghost" onClick={onClose} aria-label="Close dialog">
<X size={16} aria-hidden />
</Button>
</div>
<div className="px-5 py-4">{children}</div>
+4 -3
View File
@@ -1,3 +1,4 @@
import { Sparkles, Skull, X } from 'lucide-react';
import { useRollStore, type TrayRoll } from '@/stores/rollStore';
import { DEGREE_COLOR, DEGREE_LABEL } from '@/lib/dice/check';
import { naturalD20 } from '@/lib/dice/notation';
@@ -35,8 +36,8 @@ export function RollTray() {
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
{last.label && <div className="truncate text-xs font-medium text-muted">{last.label}</div>}
{crit === 'crit' && <div className="text-xs font-bold uppercase tracking-wide text-warning"> Critical </div>}
{crit === 'fumble' && <div className="text-xs font-bold uppercase tracking-wide text-danger"> Fumble </div>}
{crit === 'crit' && <div className="flex items-center gap-1 text-xs font-bold uppercase tracking-wide text-warning"><Sparkles size={12} aria-hidden /> Critical <Sparkles size={12} aria-hidden /></div>}
{crit === 'fumble' && <div className="flex items-center gap-1 text-xs font-bold uppercase tracking-wide text-danger"><Skull size={12} aria-hidden /> Fumble</div>}
{last.degree && (
<div className={cn('text-xs font-semibold', DEGREE_COLOR[last.degree])}>
{DEGREE_LABEL[last.degree]}
@@ -44,7 +45,7 @@ export function RollTray() {
</div>
)}
</div>
<button onClick={dismiss} aria-label="Dismiss roll" className="text-muted hover:text-ink"></button>
<button onClick={dismiss} aria-label="Dismiss roll" className="text-muted hover:text-ink"><X size={14} aria-hidden /></button>
</div>
<div className={cn('mt-1 font-display text-4xl font-bold', crit === 'crit' ? 'text-warning' : crit === 'fumble' ? 'text-danger' : 'text-accent')}>{last.result.total}</div>
<div className="mt-1 font-mono text-xs text-muted">{last.result.breakdown}</div>