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 { useState } from 'react';
import { X } from 'lucide-react';
import type { Calendar, Campaign } from '@/lib/schemas';
import { calendarRepo } from '@/lib/db/repositories';
import { newId } from '@/lib/ids';
@@ -55,7 +56,7 @@ function CalendarView({ campaign }: { campaign: Campaign }) {
<li key={ev.id} className="flex items-center gap-3 rounded-md border border-line bg-panel px-3 py-2 text-sm">
<span className={'w-16 shrink-0 font-mono ' + (ev.day === calendar.currentDay ? 'text-accent' : 'text-muted')}>Day {ev.day}</span>
<span className="flex-1 text-ink">{ev.title}</span>
<button className="text-muted hover:text-danger" onClick={() => save({ ...calendar, events: calendar.events.filter((e) => e.id !== ev.id) })} aria-label={`Remove ${ev.title}`}></button>
<button className="text-muted hover:text-danger" onClick={() => save({ ...calendar, events: calendar.events.filter((e) => e.id !== ev.id) })} aria-label={`Remove ${ev.title}`}><X size={13} aria-hidden /></button>
</li>
))}
</ul>