Implement the 027-ui-polish feature that adds six combatant row improvements (inline conditions, row-click stat block, hover-only remove button, AC shield shape, expanded concentration click target, larger d20 icon) plus top bar redesign with icon-only StepBack/StepForward navigation buttons, dark-themed scrollbars, and multiple UX fixes including stat block panel stability during initiative rolls and mobile touch safety for hidden buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,7 @@ The constitution (`.specify/memory/constitution.md`) governs all feature work:
|
|||||||
- N/A (no storage changes — purely derived from existing bestiary data) (025-display-initiative)
|
- N/A (no storage changes — purely derived from existing bestiary data) (025-display-initiative)
|
||||||
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Lucide React (icons), Vite 6 (026-roll-initiative)
|
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Lucide React (icons), Vite 6 (026-roll-initiative)
|
||||||
- N/A (no storage changes — existing localStorage persistence handles initiative via `setInitiativeUseCase`) (026-roll-initiative)
|
- N/A (no storage changes — existing localStorage persistence handles initiative via `setInitiativeUseCase`) (026-roll-initiative)
|
||||||
|
- N/A (no storage changes — purely presentational) (027-ui-polish)
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
- 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite
|
- 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite
|
||||||
|
|||||||
@@ -93,8 +93,12 @@ export function App() {
|
|||||||
}, [encounter.activeIndex]);
|
}, [encounter.activeIndex]);
|
||||||
|
|
||||||
// Auto-show stat block for the active combatant when turn changes,
|
// Auto-show stat block for the active combatant when turn changes,
|
||||||
// but only when the viewport is wide enough to show it alongside the tracker
|
// but only when the viewport is wide enough to show it alongside the tracker.
|
||||||
|
// Only react to activeIndex changes — not combatant reordering (e.g. Roll All).
|
||||||
|
const prevActiveIndexRef = useRef(encounter.activeIndex);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (prevActiveIndexRef.current === encounter.activeIndex) return;
|
||||||
|
prevActiveIndexRef.current = encounter.activeIndex;
|
||||||
if (!window.matchMedia("(min-width: 1024px)").matches) return;
|
if (!window.matchMedia("(min-width: 1024px)").matches) return;
|
||||||
const active = encounter.combatants[encounter.activeIndex];
|
const active = encounter.combatants[encounter.activeIndex];
|
||||||
if (!active?.creatureId || !isLoaded) return;
|
if (!active?.creatureId || !isLoaded) return;
|
||||||
@@ -104,7 +108,7 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<div className="mx-auto flex w-full max-w-2xl flex-1 flex-col gap-6 px-4 min-h-0">
|
<div className="mx-auto flex w-full max-w-2xl flex-1 flex-col gap-3 px-4 min-h-0">
|
||||||
{/* Turn Navigation — fixed at top */}
|
{/* Turn Navigation — fixed at top */}
|
||||||
<div className="shrink-0 pt-8">
|
<div className="shrink-0 pt-8">
|
||||||
<TurnNavigation
|
<TurnNavigation
|
||||||
@@ -118,13 +122,7 @@ export function App() {
|
|||||||
|
|
||||||
{/* Scrollable area — combatant list */}
|
{/* Scrollable area — combatant list */}
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
<header className="space-y-1 mb-6">
|
<div className="flex flex-col pb-2">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">
|
|
||||||
Initiative Tracker
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 pb-2">
|
|
||||||
{encounter.combatants.length === 0 ? (
|
{encounter.combatants.length === 0 ? (
|
||||||
<p className="py-12 text-center text-sm text-muted-foreground">
|
<p className="py-12 text-center text-sm text-muted-foreground">
|
||||||
No combatants yet — add one to get started
|
No combatants yet — add one to get started
|
||||||
|
|||||||
37
apps/web/src/components/ac-shield.tsx
Normal file
37
apps/web/src/components/ac-shield.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
interface AcShieldProps {
|
||||||
|
readonly value: number | undefined;
|
||||||
|
readonly onClick?: () => void;
|
||||||
|
readonly className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AcShield({ value, onClick, className }: AcShieldProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"relative inline-flex items-center justify-center text-sm tabular-nums text-muted-foreground transition-colors hover:text-primary",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{ width: 28, height: 32 }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 28 32"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="absolute inset-0 h-full w-full"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M14 1.5 L2.5 6.5 L2.5 15 Q2.5 25 14 30.5 Q25.5 25 25.5 15 L25.5 6.5 Z" />
|
||||||
|
</svg>
|
||||||
|
<span className="relative text-xs font-medium leading-none">
|
||||||
|
{value !== undefined ? value : "\u2014"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,9 +3,10 @@ import {
|
|||||||
type ConditionId,
|
type ConditionId,
|
||||||
deriveHpStatus,
|
deriveHpStatus,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import { BookOpen, Brain, Shield, X } from "lucide-react";
|
import { Brain, X } from "lucide-react";
|
||||||
import { type Ref, useCallback, useEffect, useRef, useState } from "react";
|
import { type Ref, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
|
import { AcShield } from "./ac-shield";
|
||||||
import { ConditionPicker } from "./condition-picker";
|
import { ConditionPicker } from "./condition-picker";
|
||||||
import { ConditionTags } from "./condition-tags";
|
import { ConditionTags } from "./condition-tags";
|
||||||
import { D20Icon } from "./d20-icon";
|
import { D20Icon } from "./d20-icon";
|
||||||
@@ -86,7 +87,10 @@ function EditableName({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={startEditing}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startEditing();
|
||||||
|
}}
|
||||||
className="truncate text-left text-sm text-foreground hover:text-primary transition-colors"
|
className="truncate text-left text-sm text-foreground hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
@@ -236,36 +240,24 @@ function AcDisplay({
|
|||||||
|
|
||||||
if (editing) {
|
if (editing) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<Input
|
||||||
<Shield size={14} className="text-muted-foreground" />
|
ref={inputRef}
|
||||||
<Input
|
type="text"
|
||||||
ref={inputRef}
|
inputMode="numeric"
|
||||||
type="text"
|
value={draft}
|
||||||
inputMode="numeric"
|
placeholder="AC"
|
||||||
value={draft}
|
className="h-7 w-[6ch] text-center text-sm tabular-nums"
|
||||||
placeholder="AC"
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
className="h-7 w-[6ch] text-center text-sm tabular-nums"
|
onBlur={commit}
|
||||||
onChange={(e) => setDraft(e.target.value)}
|
onKeyDown={(e) => {
|
||||||
onBlur={commit}
|
if (e.key === "Enter") commit();
|
||||||
onKeyDown={(e) => {
|
if (e.key === "Escape") setEditing(false);
|
||||||
if (e.key === "Enter") commit();
|
}}
|
||||||
if (e.key === "Escape") setEditing(false);
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <AcShield value={ac} onClick={startEditing} />;
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={startEditing}
|
|
||||||
className="flex items-center gap-1 text-sm tabular-nums text-muted-foreground transition-colors hover:text-primary"
|
|
||||||
>
|
|
||||||
<Shield size={14} />
|
|
||||||
{ac !== undefined ? <span>{ac}</span> : null}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function InitiativeDisplay({
|
function InitiativeDisplay({
|
||||||
@@ -338,7 +330,7 @@ function InitiativeDisplay({
|
|||||||
title="Roll initiative"
|
title="Roll initiative"
|
||||||
aria-label="Roll initiative"
|
aria-label="Roll initiative"
|
||||||
>
|
>
|
||||||
<D20Icon className="h-5 w-5" />
|
<D20Icon className="h-7 w-7" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -410,27 +402,34 @@ export function CombatantRow({
|
|||||||
}, [combatant.isConcentrating]);
|
}, [combatant.isConcentrating]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
/* biome-ignore lint/a11y/useKeyWithClickEvents: row click opens stat block */
|
||||||
|
/* biome-ignore lint/a11y/noStaticElementInteractions: row click opens stat block */
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group rounded-md px-3 py-2 transition-colors",
|
"group rounded-md pr-3 transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "border-l-2 border-l-accent bg-accent/10"
|
? "border-l-2 border-l-accent bg-accent/10"
|
||||||
: combatant.isConcentrating
|
: combatant.isConcentrating
|
||||||
? "border-l-2 border-l-purple-400"
|
? "border-l-2 border-l-purple-400"
|
||||||
: "border-l-2 border-l-transparent",
|
: "border-l-2 border-l-transparent",
|
||||||
isPulsing && "animate-concentration-pulse",
|
isPulsing && "animate-concentration-pulse",
|
||||||
|
onShowStatBlock && "cursor-pointer",
|
||||||
)}
|
)}
|
||||||
|
onClick={onShowStatBlock}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-[1.25rem_3rem_1fr_auto_auto_2rem] items-center gap-3">
|
<div className="grid grid-cols-[2rem_3rem_1fr_auto_auto_2rem] items-center gap-3 py-2">
|
||||||
{/* Concentration */}
|
{/* Concentration */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggleConcentration(id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggleConcentration(id);
|
||||||
|
}}
|
||||||
title="Concentrating"
|
title="Concentrating"
|
||||||
aria-label="Toggle concentration"
|
aria-label="Toggle concentration"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center transition-opacity",
|
"flex w-full items-center justify-center self-stretch -my-2 -ml-[2px] pl-[14px] transition-opacity",
|
||||||
combatant.isConcentrating
|
combatant.isConcentrating
|
||||||
? dimmed
|
? dimmed
|
||||||
? "opacity-50 text-purple-400"
|
? "opacity-50 text-purple-400"
|
||||||
@@ -442,37 +441,59 @@ export function CombatantRow({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Initiative */}
|
{/* Initiative */}
|
||||||
<InitiativeDisplay
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation wrapper */}
|
||||||
initiative={initiative}
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper */}
|
||||||
combatantId={id}
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
dimmed={dimmed}
|
<InitiativeDisplay
|
||||||
onSetInitiative={onSetInitiative}
|
initiative={initiative}
|
||||||
onRollInitiative={onRollInitiative}
|
combatantId={id}
|
||||||
/>
|
dimmed={dimmed}
|
||||||
|
onSetInitiative={onSetInitiative}
|
||||||
|
onRollInitiative={onRollInitiative}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Name */}
|
{/* Name + Conditions */}
|
||||||
<div className={cn("flex items-center gap-1", dimmed && "opacity-50")}>
|
<div
|
||||||
<EditableName name={name} combatantId={id} onRename={onRename} />
|
className={cn(
|
||||||
{onShowStatBlock && (
|
"relative flex flex-wrap items-center gap-1 min-w-0",
|
||||||
<button
|
dimmed && "opacity-50",
|
||||||
type="button"
|
)}
|
||||||
onClick={onShowStatBlock}
|
>
|
||||||
className="text-muted-foreground hover:text-amber-400 transition-colors"
|
<span className="min-w-0 truncate">
|
||||||
title="View stat block"
|
<EditableName name={name} combatantId={id} onRename={onRename} />
|
||||||
aria-label="View stat block"
|
</span>
|
||||||
>
|
<ConditionTags
|
||||||
<BookOpen size={14} />
|
conditions={combatant.conditions}
|
||||||
</button>
|
onRemove={(conditionId) => onToggleCondition(id, conditionId)}
|
||||||
|
onOpenPicker={() => setPickerOpen((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
{pickerOpen && (
|
||||||
|
<ConditionPicker
|
||||||
|
activeConditions={combatant.conditions}
|
||||||
|
onToggle={(conditionId) => onToggleCondition(id, conditionId)}
|
||||||
|
onClose={() => setPickerOpen(false)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AC */}
|
{/* AC */}
|
||||||
<div className={cn(dimmed && "opacity-50")}>
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation wrapper */}
|
||||||
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper */}
|
||||||
|
<div
|
||||||
|
className={cn(dimmed && "opacity-50")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<AcDisplay ac={combatant.ac} onCommit={(v) => onSetAc(id, v)} />
|
<AcDisplay ac={combatant.ac} onCommit={(v) => onSetAc(id, v)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HP */}
|
{/* HP */}
|
||||||
<div className="flex items-center gap-1">
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation wrapper */}
|
||||||
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<ClickableHp
|
<ClickableHp
|
||||||
currentHp={currentHp}
|
currentHp={currentHp}
|
||||||
maxHp={maxHp}
|
maxHp={maxHp}
|
||||||
@@ -498,35 +519,17 @@ export function CombatantRow({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto focus:opacity-100 focus:pointer-events-auto transition-opacity"
|
||||||
"h-7 w-7 text-muted-foreground hover:text-destructive",
|
onClick={(e) => {
|
||||||
dimmed && "opacity-50",
|
e.stopPropagation();
|
||||||
)}
|
onRemove(id);
|
||||||
onClick={() => onRemove(id)}
|
}}
|
||||||
title="Remove combatant"
|
title="Remove combatant"
|
||||||
aria-label="Remove combatant"
|
aria-label="Remove combatant"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conditions */}
|
|
||||||
<div className="relative ml-[calc(3rem+0.75rem)]">
|
|
||||||
<div className={cn(dimmed && "opacity-50")}>
|
|
||||||
<ConditionTags
|
|
||||||
conditions={combatant.conditions}
|
|
||||||
onRemove={(conditionId) => onToggleCondition(id, conditionId)}
|
|
||||||
onOpenPicker={() => setPickerOpen((prev) => !prev)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{pickerOpen && (
|
|
||||||
<ConditionPicker
|
|
||||||
activeConditions={combatant.conditions}
|
|
||||||
onToggle={(conditionId) => onToggleCondition(id, conditionId)}
|
|
||||||
onClose={() => setPickerOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ export function ConditionTags({
|
|||||||
title={def.label}
|
title={def.label}
|
||||||
aria-label={`Remove ${def.label}`}
|
aria-label={`Remove ${def.label}`}
|
||||||
className={`inline-flex items-center rounded p-0.5 hover:bg-card transition-colors ${colorClass}`}
|
className={`inline-flex items-center rounded p-0.5 hover:bg-card transition-colors ${colorClass}`}
|
||||||
onClick={() => onRemove(condId)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove(condId);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon size={14} />
|
<Icon size={14} />
|
||||||
</button>
|
</button>
|
||||||
@@ -86,8 +89,11 @@ export function ConditionTags({
|
|||||||
type="button"
|
type="button"
|
||||||
title="Add condition"
|
title="Add condition"
|
||||||
aria-label="Add condition"
|
aria-label="Add condition"
|
||||||
className="inline-flex items-center rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-card transition-colors"
|
className="inline-flex items-center rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-card transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity"
|
||||||
onClick={onOpenPicker}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpenPicker();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Encounter } from "@initiative/domain";
|
import type { Encounter } from "@initiative/domain";
|
||||||
import { ChevronLeft, ChevronRight, Trash2 } from "lucide-react";
|
import { StepBack, StepForward, Trash2 } from "lucide-react";
|
||||||
import { D20Icon } from "./d20-icon";
|
import { D20Icon } from "./d20-icon";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
@@ -26,13 +26,14 @@ export function TurnNavigation({
|
|||||||
<div className="flex items-center justify-between rounded-md border border-border bg-card px-4 py-3">
|
<div className="flex items-center justify-between rounded-md border border-border bg-card px-4 py-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="icon"
|
||||||
className="hover:bg-muted"
|
className="h-8 w-8 text-foreground border-foreground hover:text-primary hover:border-primary hover:bg-muted"
|
||||||
onClick={onRetreatTurn}
|
onClick={onRetreatTurn}
|
||||||
disabled={!hasCombatants || isAtStart}
|
disabled={!hasCombatants || isAtStart}
|
||||||
|
title="Previous turn"
|
||||||
|
aria-label="Previous turn"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<StepBack className="h-5 w-5" />
|
||||||
Previous
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
@@ -49,35 +50,38 @@ export function TurnNavigation({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-muted-foreground hover:text-primary"
|
||||||
|
onClick={onRollAllInitiative}
|
||||||
|
title="Roll all initiative"
|
||||||
|
aria-label="Roll all initiative"
|
||||||
|
>
|
||||||
|
<D20Icon className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={onClearEncounter}
|
||||||
|
disabled={!hasCombatants}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="icon"
|
||||||
className="hover:bg-muted"
|
className="h-8 w-8 text-foreground border-foreground hover:text-primary hover:border-primary hover:bg-muted"
|
||||||
onClick={onAdvanceTurn}
|
onClick={onAdvanceTurn}
|
||||||
disabled={!hasCombatants}
|
disabled={!hasCombatants}
|
||||||
|
title="Next turn"
|
||||||
|
aria-label="Next turn"
|
||||||
>
|
>
|
||||||
Next
|
<StepForward className="h-5 w-5" />
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-muted-foreground hover:text-primary"
|
|
||||||
onClick={onRollAllInitiative}
|
|
||||||
title="Roll all initiative"
|
|
||||||
aria-label="Roll all initiative"
|
|
||||||
>
|
|
||||||
<D20Icon className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
|
||||||
onClick={onClearEncounter}
|
|
||||||
disabled={!hasCombatants}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,6 +68,11 @@
|
|||||||
concentration-glow 1200ms ease-out;
|
concentration-glow 1200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-color: var(--color-border) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
|
|||||||
36
specs/027-ui-polish/checklists/requirements.md
Normal file
36
specs/027-ui-polish/checklists/requirements.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Specification Quality Checklist: Combatant Row UI Polish
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-03-10
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
|
||||||
|
- Assumption: "20×20px" in FR-010 refers to the current rendered size (user-facing description), not a technology-specific measurement.
|
||||||
|
- The spec covers purely presentational/interaction changes — no domain or data model changes needed.
|
||||||
25
specs/027-ui-polish/data-model.md
Normal file
25
specs/027-ui-polish/data-model.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Data Model: Combatant Row UI Polish
|
||||||
|
|
||||||
|
**Feature**: 027-ui-polish | **Date**: 2026-03-10
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
**None.** This feature is purely presentational. No domain entities, application state, or persistence formats change.
|
||||||
|
|
||||||
|
All six improvements modify only the rendering and interaction behavior of existing data:
|
||||||
|
- Conditions (already on combatant) — rendered inline instead of below
|
||||||
|
- Stat block toggle (already exists) — triggered by row click instead of icon click
|
||||||
|
- Remove action (already exists) — visibility changed to hover-only
|
||||||
|
- AC value (already on combatant) — rendered inside shield shape instead of beside icon
|
||||||
|
- Concentration (already on combatant) — click target area expanded
|
||||||
|
- D20 icon (already exists) — size increased
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
|
||||||
|
| Component | File | Change Type |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| CombatantRow | `apps/web/src/components/combatant-row.tsx` | Layout restructure, event handling |
|
||||||
|
| ConditionTags | `apps/web/src/components/condition-tags.tsx` | No changes (reused as-is) |
|
||||||
|
| ConditionPicker | `apps/web/src/components/condition-picker.tsx` | Minor positioning adjustment |
|
||||||
|
| D20Icon | `apps/web/src/components/d20-icon.tsx` | No changes (sized via className) |
|
||||||
|
| AcShield (new) | `apps/web/src/components/ac-shield.tsx` | New presentational component |
|
||||||
129
specs/027-ui-polish/plan.md
Normal file
129
specs/027-ui-polish/plan.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Implementation Plan: Combatant Row UI Polish
|
||||||
|
|
||||||
|
**Branch**: `027-ui-polish` | **Date**: 2026-03-10 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/027-ui-polish/spec.md`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Six presentational improvements to the combatant row: move conditions inline after the creature name, make the row clickable for stat blocks (removing the BookOpen icon), hide the remove button until hover, display AC inside a shield shape, expand the concentration click target, and increase the d20 icon size. All changes are confined to the web adapter layer — no domain or application changes needed.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: TypeScript 5.8 (strict mode, verbatimModuleSyntax)
|
||||||
|
**Primary Dependencies**: React 19, Tailwind CSS v4, Lucide React (icons), Vite 6
|
||||||
|
**Storage**: N/A (no storage changes — purely presentational)
|
||||||
|
**Testing**: Vitest (existing domain tests must continue passing; no new component tests)
|
||||||
|
**Target Platform**: Modern browsers (desktop primary, touch secondary)
|
||||||
|
**Project Type**: Web application (single-page, local-first)
|
||||||
|
**Performance Goals**: No perceptible layout shift on hover; smooth opacity transitions
|
||||||
|
**Constraints**: Must not break any existing interactions; all changes in `apps/web/` only
|
||||||
|
**Scale/Scope**: ~1 main component file restructured, 1 new small component
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
| Principle | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| I. Deterministic Domain Core | PASS | No domain changes whatsoever |
|
||||||
|
| II. Layered Architecture | PASS | All changes in adapter layer (apps/web). No domain or application imports added. |
|
||||||
|
| III. Agent Boundary | N/A | No agent features involved |
|
||||||
|
| IV. Clarification-First | PASS | All six changes were discussed and confirmed with user before speccing |
|
||||||
|
| V. Escalation Gates | PASS | All changes are within spec scope |
|
||||||
|
| VI. MVP Baseline Language | PASS | No permanent bans introduced |
|
||||||
|
| VII. No Gameplay Rules | PASS | No gameplay mechanics in this feature |
|
||||||
|
|
||||||
|
**Post-Phase 1 Re-check**: All gates still pass. No domain or application layer changes introduced during design.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/027-ui-polish/
|
||||||
|
├── plan.md # This file
|
||||||
|
├── research.md # Phase 0 output
|
||||||
|
├── data-model.md # Phase 1 output
|
||||||
|
├── quickstart.md # Phase 1 output
|
||||||
|
├── checklists/
|
||||||
|
│ └── requirements.md # Spec quality checklist
|
||||||
|
└── tasks.md # Phase 2 output (via /speckit.tasks)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/web/src/components/
|
||||||
|
├── combatant-row.tsx # MODIFY — layout restructure, event handling, hover states
|
||||||
|
├── condition-tags.tsx # UNCHANGED — reused inline
|
||||||
|
├── condition-picker.tsx # MINOR MODIFY — positioning may need adjustment for inline context
|
||||||
|
├── d20-icon.tsx # UNCHANGED — sized via className prop
|
||||||
|
└── ac-shield.tsx # NEW — shield-shaped AC display component
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: No new directories or architectural changes. One new presentational component (`ac-shield.tsx`). All modifications within existing `apps/web/src/components/` directory.
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### D1: Inline Conditions Layout
|
||||||
|
|
||||||
|
The name column (`1fr`) already uses `flex items-center gap-1`. Conditions move into this flex container after the name text. The name text gets `min-w-0 truncate` to shrink when conditions need space. The `ConditionTags` component is reused as-is. The "+" button uses `opacity-0 group-hover:opacity-100` to appear only on hover.
|
||||||
|
|
||||||
|
The separate conditions row below the grid (with `ml-[calc(3rem+0.75rem)]`) is removed entirely.
|
||||||
|
|
||||||
|
### D2: Row Click Stat Block via Event Delegation
|
||||||
|
|
||||||
|
The row container gets an `onClick` handler that calls `onShowStatBlock` (for bestiary combatants). All interactive child elements add `event.stopPropagation()` to their existing click handlers so clicks on initiative, HP, AC, conditions, "+", "×", and concentration don't bubble up to the row handler.
|
||||||
|
|
||||||
|
Bestiary rows get `cursor-pointer` on the row container. Custom combatant rows (no `creatureId`) don't get the handler or cursor.
|
||||||
|
|
||||||
|
The BookOpen icon button is deleted from the name area.
|
||||||
|
|
||||||
|
### D3: Hover-Only Remove Button
|
||||||
|
|
||||||
|
The remove button gets `opacity-0 group-hover:opacity-100 focus:opacity-100` classes. The button remains in the DOM (preserving the `2rem` grid column) so no layout shift occurs. This matches the existing pattern used by the concentration button.
|
||||||
|
|
||||||
|
### D4: AC Shield Shape Component
|
||||||
|
|
||||||
|
New `AcShield` component renders:
|
||||||
|
- An inline SVG shield outline (stroke-based, `currentColor`) as background
|
||||||
|
- The AC number centered inside via absolute positioning
|
||||||
|
- Click handler passed through for editing
|
||||||
|
- Sized approximately 28×32px to fit 1-3 digit values
|
||||||
|
|
||||||
|
Replaces the current `Shield` Lucide icon + number in `AcDisplay`. The edit mode input still appears on click, positioned over/replacing the shield.
|
||||||
|
|
||||||
|
### D5: Concentration Click Target
|
||||||
|
|
||||||
|
Widen the first grid column from `1.25rem` to `2rem` and make the concentration button fill the full column with padding. The brain icon stays visually centered. The button's click area now spans from the row's left edge (after border) to the initiative column.
|
||||||
|
|
||||||
|
Grid changes from:
|
||||||
|
```
|
||||||
|
grid-cols-[1.25rem_3rem_1fr_auto_auto_2rem]
|
||||||
|
```
|
||||||
|
to:
|
||||||
|
```
|
||||||
|
grid-cols-[2rem_3rem_1fr_auto_auto_2rem]
|
||||||
|
```
|
||||||
|
|
||||||
|
### D6: D20 Icon Size
|
||||||
|
|
||||||
|
Change `className="h-5 w-5"` to `className="h-7 w-7"` on the `D20Icon` in `InitiativeDisplay`. The icon grows from 20px to 28px. The initiative column (3rem = 48px) and the button (`h-7 w-full`) accommodate this without overflow.
|
||||||
|
|
||||||
|
### D7: Dark Scrollbar Styling
|
||||||
|
|
||||||
|
Apply `scrollbar-color: var(--color-border) transparent` and `scrollbar-width: thin` on `*` in `index.css`. This gives all scrollbars (main page, stat block panel) a slim dark thumb (`#334155`) on a transparent track, matching the dark UI. Supported in Chrome 121+, Firefox 64+, Safari 18+.
|
||||||
|
|
||||||
|
### D8: Top Bar Icon Redesign
|
||||||
|
|
||||||
|
Replace the Previous/Next text+chevron buttons with icon-only `StepBack`/`StepForward` buttons using the `outline` variant with `border-foreground` (white border matching the icon color). Utility actions (d20, trash) use `ghost` variant with `text-muted-foreground` — creating a two-tier visual hierarchy: primary navigation (outlined, white) vs secondary utilities (borderless, grey).
|
||||||
|
|
||||||
|
Layout: `[StepBack]` — center info — `[d20][trash] [StepForward]`. The d20 and trash are tightly grouped (`gap-0`) with `gap-3` separating them from the Next button. Icon sizes: d20 `h-6 w-6`, trash `h-5 w-5`, step buttons `h-5 w-5`. All buttons `h-8 w-8`.
|
||||||
|
|
||||||
|
### D9: Remove "Initiative Tracker" Heading
|
||||||
|
|
||||||
|
Remove the `<header>` block containing the `<h1>Initiative Tracker</h1>` from `apps/web/src/App.tsx`. The turn navigation bar already provides context (round number, active combatant name), making the heading redundant. Removing it reclaims ~60px of vertical space for the combatant list.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
No constitution violations to justify. All changes are straightforward adapter-layer modifications.
|
||||||
39
specs/027-ui-polish/quickstart.md
Normal file
39
specs/027-ui-polish/quickstart.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Quickstart: Combatant Row UI Polish
|
||||||
|
|
||||||
|
**Feature**: 027-ui-polish | **Date**: 2026-03-10
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Six presentational improvements to the combatant row in the initiative tracker. No domain or application layer changes — all work is in `apps/web/src/components/`.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `apps/web/src/components/combatant-row.tsx` — Main component; most changes here
|
||||||
|
- `apps/web/src/components/condition-picker.tsx` — May need positioning tweak for inline context
|
||||||
|
- `apps/web/src/components/ac-shield.tsx` — New component for shield-shaped AC display
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter web dev # Start dev server
|
||||||
|
pnpm check # Run before committing (knip + format + lint + typecheck + test)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. Inline conditions + remove BookOpen icon (layout restructure)
|
||||||
|
2. Row click opens stat block (event delegation)
|
||||||
|
3. Hover-only remove button (CSS opacity)
|
||||||
|
4. AC shield shape (new component)
|
||||||
|
5. Expanded concentration click target (grid/padding tweak)
|
||||||
|
6. Larger d20 icon (className change)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
No domain tests needed. Verify visually via dev server:
|
||||||
|
- Conditions appear inline after creature name
|
||||||
|
- "+" and "×" appear only on hover
|
||||||
|
- Clicking row opens stat block (bestiary creatures only)
|
||||||
|
- AC displays inside shield shape
|
||||||
|
- Concentration clickable across full left gutter
|
||||||
|
- D20 icon clearly recognizable
|
||||||
66
specs/027-ui-polish/research.md
Normal file
66
specs/027-ui-polish/research.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Research: Combatant Row UI Polish
|
||||||
|
|
||||||
|
**Feature**: 027-ui-polish | **Date**: 2026-03-10
|
||||||
|
|
||||||
|
## R1: CSS Shield Shape for AC
|
||||||
|
|
||||||
|
**Decision**: Use an inline SVG background with the AC number centered inside, rendered as a dedicated `AcShield` component.
|
||||||
|
|
||||||
|
**Rationale**: CSS `clip-path` clips the element itself (including borders and backgrounds) but makes it hard to get a clean outlined shield. An inline SVG shield path with the number as a `<text>` or overlaid HTML gives full control over stroke, fill, and sizing. This matches how D&D Beyond and character sheet PDFs render AC.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- `clip-path: polygon(...)` — clips the element shape but can't produce a stroke outline easily. Would need a pseudo-element hack.
|
||||||
|
- Background image SVG — works but harder to make the stroke color respond to CSS custom properties (theme-aware).
|
||||||
|
- Lucide Shield icon with number overlaid via absolute positioning — fragile alignment, icon stroke competes with the number visually.
|
||||||
|
|
||||||
|
**Approach**: Create a small SVG shield outline (viewBox-based) as a React component. The AC number is rendered as a centered `<span>` overlaid on the SVG using relative/absolute positioning. The SVG uses `currentColor` for the stroke so it inherits theme colors. Size: approximately 28×32px to comfortably fit 1-3 digit numbers.
|
||||||
|
|
||||||
|
## R2: Row Click Stat Block — Event Delegation
|
||||||
|
|
||||||
|
**Decision**: Attach `onClick` handler to the row container, use `event.stopPropagation()` on all interactive child elements to prevent bubbling.
|
||||||
|
|
||||||
|
**Rationale**: This is the standard pattern for "click anywhere except interactive elements." Each interactive element (initiative, HP, AC, conditions, "+", "×", concentration) already has its own click handler — adding `stopPropagation()` to each ensures the row-level handler only fires for non-interactive areas.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Checking `event.target` against a list of interactive selectors — fragile and hard to maintain.
|
||||||
|
- Wrapping non-interactive areas in a separate clickable element — would complicate the grid layout.
|
||||||
|
|
||||||
|
## R3: Hover-Only Elements — Touch Device Accessibility
|
||||||
|
|
||||||
|
**Decision**: Use CSS `opacity-0 group-hover:opacity-100` for hide/show, combined with `focus-within:opacity-100` for keyboard accessibility. On touch devices, the elements are accessible via tap (the first tap reveals, second tap activates — standard mobile pattern with opacity transitions).
|
||||||
|
|
||||||
|
**Rationale**: The concentration button already uses this exact pattern (`opacity-0 group-hover:opacity-50`). Extending it to the remove button and "+" condition button is consistent.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- `display: none` / `display: block` — causes layout shifts (violates FR-004).
|
||||||
|
- `visibility: hidden` / `visible` — works but doesn't allow opacity transitions.
|
||||||
|
|
||||||
|
## R4: Inline Conditions Layout
|
||||||
|
|
||||||
|
**Decision**: Move `ConditionTags` and the "+" button into the name column's flex container (the `1fr` column), after the name text. The conditions and "+" sit inline with the name, wrapping if needed.
|
||||||
|
|
||||||
|
**Rationale**: The name column is already a flex container (`flex items-center gap-1`). Adding condition icons here is natural. The `truncate` class on the name will need adjustment — the name should shrink (`min-w-0 truncate` on just the name text) while conditions fill remaining space.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Dedicated column for conditions — adds complexity to the grid and uses horizontal space poorly.
|
||||||
|
- Conditions as a second flex row within the name cell — still uses `flex-wrap` but explicitly creates a two-row layout when many conditions exist.
|
||||||
|
|
||||||
|
## R5: D20 Icon Sizing
|
||||||
|
|
||||||
|
**Decision**: Increase from `h-5 w-5` (20px) to `h-7 w-7` (28px). The initiative column is 3rem (48px) wide, so 28px fits comfortably with padding.
|
||||||
|
|
||||||
|
**Rationale**: At 20px the internal geometry lines of the d20 are too dense to read as a die shape. At 28px the icosahedron silhouette becomes recognizable. The column has room — the d20 button is already `h-7 w-full` so only the icon within needs to grow.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- `h-6 w-6` (24px) — marginal improvement, still a bit small.
|
||||||
|
- `h-8 w-8` (32px) — might feel oversized relative to the initiative numbers in adjacent rows.
|
||||||
|
|
||||||
|
## R6: Concentration Click Target
|
||||||
|
|
||||||
|
**Decision**: Expand the concentration button from `1.25rem` width to fill the full gutter. Change the grid column from `1.25rem` to `2rem` (or use padding on the button to extend its hit area to the row edge). The brain icon stays centered visually.
|
||||||
|
|
||||||
|
**Rationale**: The left border of the row already has `px-3` padding. The concentration column at `1.25rem` (20px) with a 16px icon leaves very little extra hit area. Widening the column or extending the button's padding makes tapping much easier.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Negative margin on the button — hacky, could affect layout.
|
||||||
|
- Separate invisible overlay — unnecessary complexity.
|
||||||
155
specs/027-ui-polish/spec.md
Normal file
155
specs/027-ui-polish/spec.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Feature Specification: Combatant Row UI Polish
|
||||||
|
|
||||||
|
**Feature Branch**: `027-ui-polish`
|
||||||
|
**Created**: 2026-03-10
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "UI/UX improvements to the combatant row — inline conditions, hover-only remove button, row-click stat block, AC shield shape, expanded concentration click target, larger d20 icon"
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Inline Conditions (Priority: P1)
|
||||||
|
|
||||||
|
As a DM running an encounter, I want condition icons to appear inline next to the creature name so that each combatant row takes up minimal vertical space while conditions remain visible at a glance.
|
||||||
|
|
||||||
|
**Why this priority**: Conditions below each row are the largest source of visual clutter and wasted vertical space. This single change will have the most visible impact on the tracker's compactness and readability.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by adding conditions to combatants and verifying they appear on the same line as the name, with the "+" button visible only on hover.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a combatant has no conditions, **When** the row is not hovered, **Then** no condition UI is visible on that row.
|
||||||
|
2. **Given** a combatant has no conditions, **When** the user hovers over the row, **Then** a "+" button appears inline after the creature name.
|
||||||
|
3. **Given** a combatant has conditions applied, **When** viewing the tracker, **Then** condition icons appear inline after the creature name on the same row (no second line).
|
||||||
|
4. **Given** a combatant has conditions applied, **When** the user hovers over the row, **Then** a "+" button appears after the last condition icon.
|
||||||
|
5. **Given** a combatant has many conditions, **When** the row is displayed, **Then** the conditions wrap gracefully within the name column without overlapping other columns.
|
||||||
|
6. **Given** conditions are displayed inline, **When** the user clicks a condition icon, **Then** the condition is removed (existing behavior preserved).
|
||||||
|
7. **Given** conditions are displayed inline, **When** the user clicks the "+" button, **Then** the condition picker dropdown opens (existing behavior preserved).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Row Click Opens Stat Block (Priority: P1)
|
||||||
|
|
||||||
|
As a DM, I want to click anywhere on a combatant row to open its stat block so that I have a larger click target and a cleaner row without a dedicated book icon.
|
||||||
|
|
||||||
|
**Why this priority**: Removing the BookOpen icon and making the row clickable simultaneously reduces visual clutter and improves usability. Tightly coupled with inline conditions (both change the name area).
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by clicking various non-interactive areas of a bestiary combatant row and verifying the stat block panel opens.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a combatant has a linked creature (bestiary entry), **When** the user clicks on the name text, **Then** the stat block panel opens for that creature.
|
||||||
|
2. **Given** a combatant has a linked creature, **When** the user clicks on empty space within the row (not on an interactive element), **Then** the stat block panel opens.
|
||||||
|
3. **Given** the user clicks on an interactive element (initiative, HP, AC, condition icon, "+", "×", concentration), **Then** the stat block does NOT open; the element's own action fires instead.
|
||||||
|
4. **Given** a combatant does NOT have a linked creature (custom combatant), **When** the user clicks on the row, **Then** nothing happens (no stat block to show).
|
||||||
|
5. **Given** a combatant row is displayed, **When** viewing the row, **Then** no dedicated book/stat-block icon is visible (the BookOpen icon is removed).
|
||||||
|
6. **Given** a bestiary combatant row, **When** the user hovers over non-interactive areas, **Then** the cursor indicates the row is clickable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Hover-Only Remove Button (Priority: P2)
|
||||||
|
|
||||||
|
As a DM, I want the remove (×) button to only appear when I hover over a combatant row so that the tracker looks cleaner during play and I'm less likely to accidentally remove a combatant.
|
||||||
|
|
||||||
|
**Why this priority**: Reduces persistent visual noise. Lower priority than layout changes since it's a simpler visibility toggle.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by hovering and unhovering combatant rows and verifying the × button appears/disappears.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a combatant row is not hovered, **When** viewing the tracker, **Then** the remove button is not visible.
|
||||||
|
2. **Given** a combatant row is hovered, **When** the user moves the mouse over the row, **Then** the remove button becomes visible.
|
||||||
|
3. **Given** the remove button is visible on hover, **When** the user clicks it, **Then** the combatant is removed (existing behavior preserved).
|
||||||
|
4. **Given** the remove button is hidden, **When** viewing the row layout, **Then** the row does not shift or reflow when the button appears on hover (space is reserved).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 4 - AC Shield Shape (Priority: P2)
|
||||||
|
|
||||||
|
As a DM, I want the AC number displayed inside a shield-shaped outline so that AC is instantly recognizable as armor class — matching the visual language of D&D character sheets.
|
||||||
|
|
||||||
|
**Why this priority**: High visual impact and strong thematic resonance, but independent of the layout restructuring in P1 stories.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by viewing any combatant with AC set and verifying the number appears inside a shield-shaped visual element.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a combatant has an AC value, **When** viewing the row, **Then** the AC number is displayed inside a shield-shaped outline.
|
||||||
|
2. **Given** a combatant has an AC value, **When** viewing the shield, **Then** the current separate shield icon is no longer displayed (replaced by the shield shape around the number).
|
||||||
|
3. **Given** a combatant has no AC value set, **When** viewing the row, **Then** the shield shape is still displayed with a placeholder or empty state.
|
||||||
|
4. **Given** the AC shield is displayed, **When** the user clicks on it, **Then** the AC editing interaction works as before (existing edit behavior preserved).
|
||||||
|
5. **Given** the AC shield is displayed at various AC values (single-digit, double-digit, triple-digit), **When** viewing the shield, **Then** the number is centered and the shield scales appropriately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 5 - Expanded Concentration Click Target (Priority: P3)
|
||||||
|
|
||||||
|
As a DM, I want the concentration toggle to have a larger clickable area — filling the gutter between the purple active-turn border and the initiative column — so that toggling concentration is easier to hit during fast-paced play.
|
||||||
|
|
||||||
|
**Why this priority**: Quality-of-life improvement that doesn't change visual design, only interaction area. Lower impact than layout and visual changes.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by clicking in the gutter area left of the initiative number and verifying concentration toggles.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a combatant row, **When** the user clicks anywhere in the space between the left border and the initiative column, **Then** the concentration toggle is activated.
|
||||||
|
2. **Given** the concentration button has an expanded click target, **When** viewing the row, **Then** the visual appearance of the brain icon remains unchanged.
|
||||||
|
3. **Given** a concentrating combatant receives damage, **When** the shake/glow animation plays, **Then** the animation still works correctly with the expanded click target.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 6 - Larger D20 Icon (Priority: P3)
|
||||||
|
|
||||||
|
As a DM, I want the d20 roll-initiative button to be slightly larger so that the icosahedron shape is clearly recognizable as a d20 die rather than an ambiguous icon.
|
||||||
|
|
||||||
|
**Why this priority**: Minor visual refinement. Enabled by the vertical space savings from inline conditions.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by viewing a bestiary combatant without initiative set and verifying the d20 icon is visually recognizable.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a bestiary combatant has no initiative set, **When** viewing the row, **Then** the d20 icon is displayed at a larger size than the current 20×20 pixels.
|
||||||
|
2. **Given** the d20 icon is larger, **When** viewing the row, **Then** the icon does not overflow or misalign with the initiative column.
|
||||||
|
3. **Given** the d20 icon is larger, **When** the user clicks it, **Then** initiative is rolled as before (existing behavior preserved).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when a combatant has so many conditions that they exceed the available inline space? They wrap within the name column; the row height increases to accommodate.
|
||||||
|
- How does the condition picker dropdown position itself when opened from an inline "+" button? It positions relative to the "+" button, flipping vertically if near the viewport edge (existing flip logic preserved).
|
||||||
|
- What happens on touch devices where hover is not available? The "+" condition button and "×" remove button remain accessible via tap (they appear on touch/focus rather than requiring hover).
|
||||||
|
- What happens when the stat block panel is already open for a creature and the user clicks the same row again? The panel closes (toggle behavior).
|
||||||
|
- How does the row-click stat block interact with initiative editing? Clicking the initiative area starts editing; it does not open the stat block.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: Condition icons MUST render inline after the creature name within the same row, not on a separate line below the row.
|
||||||
|
- **FR-002**: The "+" condition button MUST be hidden by default and appear only on row hover (or touch/focus on touch devices).
|
||||||
|
- **FR-003**: The remove (×) button MUST be hidden by default and appear only on row hover (or touch/focus on touch devices).
|
||||||
|
- **FR-004**: The row MUST reserve layout space for the remove button so that appearing/disappearing does not cause layout shifts.
|
||||||
|
- **FR-005**: Clicking non-interactive areas of a bestiary combatant row MUST open the stat block panel for that creature.
|
||||||
|
- **FR-006**: Clicking interactive elements (initiative, HP, AC, conditions, "+", "×", concentration) MUST NOT trigger the stat block panel — only the element's own action.
|
||||||
|
- **FR-007**: The BookOpen icon MUST be removed from the combatant row.
|
||||||
|
- **FR-008**: The AC value MUST be displayed inside a shield-shaped visual element (outline or background) replacing the separate shield icon.
|
||||||
|
- **FR-009**: The concentration toggle's clickable area MUST extend to fill the full gutter between the left border and the initiative column.
|
||||||
|
- **FR-010**: The d20 roll-initiative icon MUST be displayed at a larger size than the current 20×20px while remaining contained within the initiative column.
|
||||||
|
- **FR-011**: All existing interactions (condition add/remove, HP adjustment, AC editing, initiative editing/rolling, concentration toggle, combatant removal) MUST continue to work as before.
|
||||||
|
- **FR-012**: Bestiary combatant rows with a linked creature MUST show a pointer cursor on hover over non-interactive areas to indicate clickability.
|
||||||
|
- **FR-013**: Browser scrollbars MUST be styled to match the dark UI theme — using thin, dark-colored scrollbar thumbs instead of the browser's default light/grey appearance.
|
||||||
|
- **FR-014**: The top bar's Roll All (d20) and Clear Encounter (trash) icons MUST be sized proportionally to the bar (d20 at 24px, trash at 20px) and visually grouped together, separated from the turn navigation controls by spacing.
|
||||||
|
- **FR-016**: Turn navigation (Previous/Next) MUST use StepBack/StepForward icons in outline button style with foreground-colored borders, replacing the previous text+chevron buttons. Utility actions (d20/trash) MUST use ghost button style to create a clear visual hierarchy.
|
||||||
|
- **FR-017**: The Previous and Next turn buttons MUST be positioned at the far left and far right of the top bar respectively, with the round/combatant info centered between them.
|
||||||
|
- **FR-015**: The "Initiative Tracker" heading MUST be removed to maximize vertical space for combatants. The turn navigation bar provides sufficient context.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: Each combatant row without conditions takes up exactly one line height (no empty second row).
|
||||||
|
- **SC-002**: All six visual/interaction improvements are implemented without breaking any existing tracker functionality.
|
||||||
|
- **SC-003**: The DM can open a stat block by clicking anywhere on the combatant name area — no need to locate a small icon.
|
||||||
|
- **SC-004**: The AC number is visually identifiable as "armor class" without a separate icon label, through the shield shape alone.
|
||||||
|
- **SC-005**: The d20 icon is recognizable as a twenty-sided die at its displayed size.
|
||||||
|
- **SC-006**: No layout shift occurs when hovering/unhovering rows (remove button and "+" button appear without reflowing content).
|
||||||
187
specs/027-ui-polish/tasks.md
Normal file
187
specs/027-ui-polish/tasks.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# Tasks: Combatant Row UI Polish
|
||||||
|
|
||||||
|
**Input**: Design documents from `/specs/027-ui-polish/`
|
||||||
|
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md
|
||||||
|
|
||||||
|
**Tests**: No new tests required — this feature is purely presentational. Existing domain tests must continue passing.
|
||||||
|
|
||||||
|
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||||
|
|
||||||
|
## Format: `[ID] [P?] [Story] Description`
|
||||||
|
|
||||||
|
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||||
|
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
|
||||||
|
- Include exact file paths in descriptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Setup
|
||||||
|
|
||||||
|
**Purpose**: No new project setup needed — all infrastructure exists. This phase is empty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Layout Restructure)
|
||||||
|
|
||||||
|
**Purpose**: Restructure the combatant row grid layout to support inline conditions. This MUST be complete before individual user stories can be implemented, since it changes the core grid structure.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: US1 restructures the grid and name column — all subsequent stories build on this new layout.
|
||||||
|
|
||||||
|
- [x] T001 Move `ConditionTags` and the "+" condition button from the separate conditions row (the `div` with `ml-[calc(3rem+0.75rem)]`) into the name column's flex container in `apps/web/src/components/combatant-row.tsx`. Place them after the name text inside the `1fr` grid cell. Add `flex-wrap` to the container so conditions wrap gracefully. Apply `min-w-0` and `truncate` on the name text so it truncates when conditions need space. Do NOT use `shrink-0` — the name must be allowed to shrink to make room for inline conditions. Remove the entire separate conditions row `div` below the main grid.
|
||||||
|
- [x] T002 Make the "+" condition button hover-only by adding `opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity` classes in `apps/web/src/components/combatant-row.tsx`. When no conditions exist, the "+" should be the only inline element after the name and should only appear on hover. When conditions exist, the "+" appears after the last condition icon on hover.
|
||||||
|
- [x] T003 Verify the `ConditionPicker` dropdown positioning still works correctly from its new inline position in `apps/web/src/components/condition-picker.tsx`. The existing flip logic (checking `rect.bottom > window.innerHeight`) should still work since positioning is `absolute` relative to the picker's parent. Adjust if the new inline context changes the reference point.
|
||||||
|
|
||||||
|
**Checkpoint**: Conditions render inline after creature name. "+" appears on hover only. No second row below combatants. All condition add/remove interactions work as before.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 2 — Row Click Opens Stat Block (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Make the entire combatant row clickable to open the stat block, removing the dedicated BookOpen icon.
|
||||||
|
|
||||||
|
**Independent Test**: Click on the creature name or empty row space for a bestiary combatant — stat block panel opens. Click on interactive elements (HP, AC, initiative, conditions) — their own actions fire, stat block does not open.
|
||||||
|
|
||||||
|
- [x] T004 [US2] Remove the BookOpen icon button from the name area in the `EditableName` section of `apps/web/src/components/combatant-row.tsx`. Remove the `BookOpen` import from `lucide-react` if no longer used elsewhere in the file.
|
||||||
|
- [x] T005 [US2] Add an `onClick` handler to the row container `div` in `apps/web/src/components/combatant-row.tsx` that calls `onShowStatBlock?.()` when the combatant has a `creatureId`. Add `cursor-pointer` to the row container for bestiary combatants. Confirm that the parent component's `onShowStatBlock` handler implements toggle behavior (clicking the same row again closes the stat block panel, per spec edge case).
|
||||||
|
- [x] T006 [US2] Add `event.stopPropagation()` to all interactive child element click handlers in `apps/web/src/components/combatant-row.tsx` — specifically: concentration button, initiative display (d20 button, edit click, display click), condition icon buttons, "+" condition button, AC display click, HP display click, and remove button. This prevents row-level stat block opening when interacting with these elements.
|
||||||
|
|
||||||
|
**Checkpoint**: Clicking non-interactive row areas opens stat block for bestiary combatants. BookOpen icon is gone. All existing interactions (HP, AC, initiative, conditions, concentration, remove) still work without triggering stat block.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 3 — Hover-Only Remove Button (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Hide the × remove button by default, showing it only on row hover.
|
||||||
|
|
||||||
|
**Independent Test**: Hover over a combatant row — × appears. Move mouse away — × disappears. Row layout does not shift.
|
||||||
|
|
||||||
|
- [x] T007 [US3] Add `opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity` classes to the remove button (X icon) in `apps/web/src/components/combatant-row.tsx`. Keep the button in the DOM (preserving the `2rem` grid column) so no layout shift occurs. This follows the same pattern used by the concentration button.
|
||||||
|
|
||||||
|
**Checkpoint**: Remove button hidden by default, visible on hover, no layout shift. Removal still works.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 4 — AC Shield Shape (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Display the AC number inside a shield-shaped outline instead of beside a shield icon.
|
||||||
|
|
||||||
|
**Independent Test**: View any combatant with AC set — number appears inside a shield-shaped SVG outline. Click to edit AC — editing works as before.
|
||||||
|
|
||||||
|
- [x] T008 [P] [US4] Create new `AcShield` component in `apps/web/src/components/ac-shield.tsx`. Render an inline SVG shield outline (stroke-based, `currentColor`, approximately 28×32px viewBox) with a centered `<span>` overlay for the AC number using relative/absolute positioning. Accept `value` (number | undefined), `onClick` callback, and `className` props. Show a placeholder (e.g., "—") when no value is set.
|
||||||
|
- [x] T009 [US4] Replace the `AcDisplay` sub-component's non-edit rendering in `apps/web/src/components/combatant-row.tsx` to use the new `AcShield` component instead of the Lucide `Shield` icon + number. Keep the edit-mode input behavior unchanged — when clicked, the shield is replaced by the existing input field. Remove the `Shield` import from `lucide-react` if no longer used elsewhere in the file.
|
||||||
|
|
||||||
|
**Checkpoint**: AC displays inside shield shape. Edit mode still works. Single-digit, double-digit, and triple-digit values render centered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: User Story 5 — Expanded Concentration Click Target (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Widen the concentration button's clickable area to fill the gutter between the left border and initiative column.
|
||||||
|
|
||||||
|
**Independent Test**: Click in the space left of the initiative number — concentration toggles. Brain icon stays visually centered.
|
||||||
|
|
||||||
|
- [x] T010 [US5] Widen the first grid column from `1.25rem` to `2rem` in the grid template `grid-cols-[1.25rem_3rem_1fr_auto_auto_2rem]` in `apps/web/src/components/combatant-row.tsx`. Make the concentration button fill the full column width (`w-full h-full`) while keeping the brain icon visually centered. Verify the concentration shake/glow animation still works with the wider button.
|
||||||
|
|
||||||
|
**Checkpoint**: Concentration clickable across full left gutter. Brain icon centered. Animation intact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: User Story 6 — Larger D20 Icon (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Increase the d20 roll-initiative icon size for better recognizability.
|
||||||
|
|
||||||
|
**Independent Test**: View a bestiary combatant without initiative — d20 icon is visibly larger and recognizable as a die.
|
||||||
|
|
||||||
|
- [x] T011 [P] [US6] Change the D20Icon className from `h-5 w-5` to `h-7 w-7` in the `InitiativeDisplay` sub-component within `apps/web/src/components/combatant-row.tsx`. Verify the icon fits within the 3rem initiative column without overflow.
|
||||||
|
|
||||||
|
**Checkpoint**: D20 icon at 28px is recognizable. No overflow. Roll interaction works.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
- [x] T012 Run `pnpm check` to verify all existing tests pass, linting is clean, types check, and no unused exports are flagged by Knip in the project root
|
||||||
|
- [x] T013 Visual verification: check all combatant states (active turn, concentrating, unconscious/dimmed, bloodied, no conditions, many conditions, no AC, no initiative, custom combatant without creatureId) render correctly with the new layout
|
||||||
|
- [x] T014 Verify touch device behavior: "+" condition button and "×" remove button are accessible via tap/focus even without hover
|
||||||
|
- [x] T015 [P] Style browser scrollbars to match dark UI by adding `scrollbar-color` and `scrollbar-width` properties in `apps/web/src/index.css`
|
||||||
|
- [x] T016 [P] Redesign top bar buttons: replace Previous/Next text+chevron with StepBack/StepForward outline icon buttons (`border-foreground`, white), keep d20/trash as ghost buttons (grey), group d20+trash tightly with spacing before Next in `apps/web/src/components/turn-navigation.tsx`
|
||||||
|
- [x] T017 [P] Remove the "Initiative Tracker" `<header>` heading from `apps/web/src/App.tsx` to maximize vertical combatant space
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Foundational (Phase 2)**: No prerequisites — restructures the core layout
|
||||||
|
- **US2 Row Click (Phase 3)**: Depends on Phase 2 (BookOpen removal and row click both affect the name area)
|
||||||
|
- **US3 Hover Remove (Phase 4)**: Depends on Phase 2 (needs stable grid layout)
|
||||||
|
- **US4 AC Shield (Phase 5)**: Independent — can run in parallel with Phase 3/4
|
||||||
|
- **US5 Concentration (Phase 6)**: Depends on Phase 2 (modifies the same grid template)
|
||||||
|
- **US6 D20 Size (Phase 7)**: Independent — can run in parallel with any phase after Phase 2
|
||||||
|
- **Polish (Phase 8)**: Depends on all previous phases
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1 (Inline Conditions)**: Foundational — extracted to Phase 2 since all stories depend on it
|
||||||
|
- **US2 (Row Click Stat Block)**: Depends on US1 layout restructure (Phase 2)
|
||||||
|
- **US3 (Hover Remove)**: Depends on US1 layout restructure (Phase 2)
|
||||||
|
- **US4 (AC Shield)**: Independent — new component + replacement in AcDisplay
|
||||||
|
- **US5 (Concentration Target)**: Depends on US1 grid template change (Phase 2)
|
||||||
|
- **US6 (D20 Size)**: Independent — single className change
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Implementation tasks are sequential within each story
|
||||||
|
- Stories are independently testable at each checkpoint
|
||||||
|
|
||||||
|
### Parallel Opportunities
|
||||||
|
|
||||||
|
After Phase 2 (Foundational) completes:
|
||||||
|
- T007 (US3), T008 (US4), T010 (US5), and T011 (US6) can all start in parallel
|
||||||
|
- T004-T006 (US2) can run in parallel with T008 (US4) and T011 (US6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Example: After Phase 2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# These can all launch in parallel after foundational layout restructure:
|
||||||
|
Task: "T007 [US3] Hover-only remove button in combatant-row.tsx"
|
||||||
|
Task: "T008 [US4] Create AcShield component in ac-shield.tsx"
|
||||||
|
Task: "T011 [US6] Increase D20Icon size in combatant-row.tsx"
|
||||||
|
|
||||||
|
# Then sequentially:
|
||||||
|
Task: "T009 [US4] Integrate AcShield into AcDisplay in combatant-row.tsx" (after T008)
|
||||||
|
Task: "T010 [US5] Widen concentration grid column in combatant-row.tsx" (after T007 or independently)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (Phase 2 + User Story 2)
|
||||||
|
|
||||||
|
1. Complete Phase 2: Layout restructure (inline conditions, hover-only "+")
|
||||||
|
2. Complete Phase 3: Row click stat block + remove BookOpen icon
|
||||||
|
3. **STOP and VALIDATE**: Conditions inline, row click works, no regressions
|
||||||
|
4. This alone delivers the biggest visual improvement
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Phase 2 → Inline conditions (biggest layout change)
|
||||||
|
2. Phase 3 → Row click stat block (biggest UX improvement)
|
||||||
|
3. Phase 4 → Hover-only remove (visual cleanup)
|
||||||
|
4. Phase 5 → AC shield shape (visual polish)
|
||||||
|
5. Phase 6+7 → Concentration target + D20 size (minor refinements)
|
||||||
|
6. Phase 8 → Polish and verify all states
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All tasks modify `apps/web/src/components/` only — no domain or application layer changes
|
||||||
|
- Only one new file created: `apps/web/src/components/ac-shield.tsx`
|
||||||
|
- The main file `combatant-row.tsx` is modified by most tasks — be careful with parallel edits to this file
|
||||||
|
- Existing domain tests must pass after all changes (`pnpm check`)
|
||||||
|
- Commit after each phase or logical group for clean history
|
||||||
Reference in New Issue
Block a user