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:
@@ -1,5 +1,6 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Map as MapIcon, Cast, Download, ExternalLink, X } from 'lucide-react';
|
||||
import type { BattleMap, Campaign } from '@/lib/schemas';
|
||||
import { mapsRepo } from '@/lib/db/repositories';
|
||||
import { parseUvtt, toUvtt } from '@/lib/vtt/uvtt';
|
||||
@@ -68,6 +69,7 @@ function Maps({ campaign }: { campaign: Campaign }) {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader
|
||||
eyebrow="The cartographer's table"
|
||||
title="Maps"
|
||||
subtitle={campaign.name}
|
||||
actions={<Button variant="primary" onClick={() => fileRef.current?.click()}>+ Add map</Button>}
|
||||
@@ -79,30 +81,44 @@ function Maps({ campaign }: { campaign: Campaign }) {
|
||||
{maps.length === 0 ? (
|
||||
<EmptyState title="No maps yet" hint="Upload an image or import a Universal VTT (.dd2vtt / .uvtt) map — walls, doors and lights come across — then add a grid, fog, tokens, and show it to players." action={<Button variant="primary" onClick={() => fileRef.current?.click()}>+ Add map</Button>} />
|
||||
) : (
|
||||
<div className="grid gap-4 lg:grid-cols-[210px_1fr]">
|
||||
<ul className="space-y-1">
|
||||
{maps.map((mp) => (
|
||||
<li key={mp.id} className="group flex items-center gap-1 rounded-md border border-line">
|
||||
<button className={cn('flex-1 truncate px-3 py-2 text-left text-sm', selected?.id === mp.id ? 'text-accent' : 'text-ink')} onClick={() => setSelectedId(mp.id)}>
|
||||
{mp.name}{activeMapId === mp.id ? ' 📺' : ''}
|
||||
</button>
|
||||
<button className="px-2 text-muted opacity-0 hover:text-danger group-hover:opacity-100" onClick={async () => { await mapsRepo.remove(mp.id); if (selectedId === mp.id) setSelectedId(null); if (activeMapId === mp.id) setActiveMap(null); }} aria-label={`Delete ${mp.name}`}>✕</button>
|
||||
</li>
|
||||
))}
|
||||
<div className="grid gap-4 lg:grid-cols-[230px_1fr]">
|
||||
<ul className="space-y-1.5">
|
||||
{maps.map((mp) => {
|
||||
const active = selected?.id === mp.id;
|
||||
return (
|
||||
<li key={mp.id} className={cn('group flex items-center gap-1 rounded-xl border bg-panel transition-colors', active ? 'border-accent bg-accent-glow' : 'border-line hover:border-line-strong')}>
|
||||
<button className={cn('flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-left text-sm', active ? 'text-accent-deep' : 'text-ink')} onClick={() => setSelectedId(mp.id)}>
|
||||
<MapIcon size={15} className={active ? 'text-accent-deep' : 'text-muted'} aria-hidden />
|
||||
<span className="truncate font-display font-medium">{mp.name}</span>
|
||||
{activeMapId === mp.id && <Cast size={13} className="ml-auto shrink-0 text-accent-deep" aria-hidden />}
|
||||
</button>
|
||||
<button className="px-2 text-muted opacity-0 hover:text-danger group-hover:opacity-100" onClick={async () => { await mapsRepo.remove(mp.id); if (selectedId === mp.id) setSelectedId(null); if (activeMapId === mp.id) setActiveMap(null); }} aria-label={`Delete ${mp.name}`}>
|
||||
<X size={14} aria-hidden />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{selected ? (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2 rounded-xl border border-line bg-panel p-2">
|
||||
<Button size="sm" variant={activeMapId === selected.id ? 'primary' : 'secondary'} onClick={() => setActiveMap(activeMapId === selected.id ? null : selected.id)}>
|
||||
{activeMapId === selected.id ? '📺 Showing to players' : 'Show to players'}
|
||||
<Cast size={15} aria-hidden />
|
||||
{activeMapId === selected.id ? 'Showing to players' : 'Show to players'}
|
||||
</Button>
|
||||
<Link to="/play" className="inline-flex items-center gap-1 text-sm text-accent hover:text-accent-deep hover:underline">Open player view <ExternalLink size={13} aria-hidden /></Link>
|
||||
<Button size="sm" variant="ghost" className="ml-auto" onClick={() => void exportUvtt(selected)} title="Export as Universal VTT (.uvtt)">
|
||||
<Download size={15} aria-hidden />
|
||||
Export .uvtt
|
||||
</Button>
|
||||
<Link to="/play" className="text-sm text-accent hover:underline">Open player view ↗</Link>
|
||||
<Button size="sm" variant="ghost" className="ml-auto" onClick={() => void exportUvtt(selected)} title="Export as Universal VTT (.uvtt)">Export .uvtt</Button>
|
||||
</div>
|
||||
<MapEditor key={selected.id} map={selected} campaign={campaign} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-line bg-panel p-5 text-sm text-muted">Select a map.</div>
|
||||
<div className="grid place-items-center rounded-xl border border-dashed border-line bg-panel/50 p-10 text-center text-sm text-muted">
|
||||
<MapIcon size={26} className="mb-2 text-faint" aria-hidden />
|
||||
Select a map.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user