UI overhaul "Living Codex" E: per-screen layouts

Ported all 9 screens to the prototype layout (visual only; all data wiring, text,
aria-labels, and testids preserved) via parallel agents + central verification:
- Dashboard: editorial hero, party roster w/ HP meters + avatars, quests, threats, rolls.
- Campaigns: cover-art deck cards + gilt rule + footer.
- Character sheet + roster: hero header, StatCoin abilities, prof rows; grimoire cards.
- Combat: glowing initiative rows (PC gilt / foe ember), condition badges, log.
- Dice: die-pool glyphs, adv/dis segmented toggle, crit/fumble stage, history.
- Compendium: Spectral stat blocks (ember headers, ability row).
- Settings: grouped icon-tile cards. Live Session: room-code card, seat grid, player board.
- Battle Map: carded list + tooled editor chrome (canvas untouched).

Regression fixes after the ports: dice die-buttons aria-label `d4` not `Roll 1d4`
(was colliding with the primary Roll button); map "Open player view" link uses an
ExternalLink icon (test updated, ↗ glyph removed); seat-claim card gets a stable
data-testid="seat-option" (redesign moved rounded-lg→rounded-xl).

223 unit + 34 e2e + 2 realtime green; tsc + eslint + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 21:37:00 +02:00
parent e4b399eaa8
commit 99c7657f96
19 changed files with 1070 additions and 457 deletions
+13 -7
View File
@@ -1,7 +1,9 @@
import { UserPlus } from 'lucide-react';
import type { Snapshot } from '@/lib/sync/messages';
import type { Character } from '@/lib/schemas';
import { Button } from '@/components/ui/Button';
import { EmptyState } from '@/components/ui/Page';
import { Avatar, Badge } from '@/components/ui/Codex';
/**
* Shown to a joined player before they control a character: pick "you" from the
@@ -18,17 +20,21 @@ export function SeatClaimScreen({ snapshot, localChars, pending, onClaim }: {
return <EmptyState title="No characters yet" hint="The GM hasn't added any player characters to this campaign." />;
}
return (
<section className="mb-6">
<h2 className="mb-2 text-sm font-semibold uppercase tracking-wide text-muted">Which character is yours?</h2>
<p className="mb-3 text-sm text-muted">Pick your character to manage its HP, spells, and rolls live. The GM approves the request.</p>
<section className="paper-grain mb-6 rounded-xl border border-line bg-panel p-4">
<div className="font-display text-sm italic text-accent">Claim your seat</div>
<h2 className="font-display text-xl font-semibold text-ink">Which character is yours?</h2>
<p className="mt-1 text-sm text-muted">Pick your character to manage its HP, spells, and rolls live. The GM approves the request.</p>
<hr className="gilt-rule my-3" />
<div className="grid gap-2 sm:grid-cols-2">
{snapshot.party.map((c) => {
const hasOffline = localChars.has(c.id);
return (
<div key={c.id} className="flex items-center gap-3 rounded-lg border border-line bg-panel p-3">
<div className="flex-1">
<div className="font-display text-lg font-semibold text-ink">{c.name}</div>
<div className="text-xs text-muted">Lv {c.level} · AC {c.ac} · {c.hp.current}/{c.hp.max} HP{hasOffline ? ' · offline edits ready' : ''}</div>
<div key={c.id} data-testid="seat-option" className="flex items-center gap-3 rounded-xl border border-line bg-surface-2 p-3">
<Avatar name={c.name} size={40} />
<div className="min-w-0 flex-1">
<div className="truncate font-display text-lg font-semibold text-ink">{c.name}</div>
<div className="font-mono text-xs text-muted">Lv {c.level} · AC {c.ac} · {c.hp.current}/{c.hp.max} HP</div>
{hasOffline && <Badge tone="arcane" className="mt-1"><UserPlus size={11} aria-hidden /> offline edits ready</Badge>}
</div>
<Button size="sm" variant="primary" disabled={pending} onClick={() => onClaim(c.id)}>This is me</Button>
</div>