From 04a4f18f98d25d1d806f7acf2ff4019c2c08604c Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 6 Mar 2026 15:43:27 +0100 Subject: [PATCH] Implement the 020-fix-zero-hp-opacity feature that replaces container-level opacity dimming with element-level opacity on individual leaf elements so that HP popover and condition picker render at full opacity for unconscious combatants Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 1 + apps/web/src/components/combatant-row.tsx | 58 +++++++--- .../checklists/requirements.md | 35 ++++++ specs/020-fix-zero-hp-opacity/data-model.md | 14 +++ specs/020-fix-zero-hp-opacity/plan.md | 81 ++++++++++++++ specs/020-fix-zero-hp-opacity/quickstart.md | 47 ++++++++ specs/020-fix-zero-hp-opacity/research.md | 84 +++++++++++++++ specs/020-fix-zero-hp-opacity/spec.md | 82 ++++++++++++++ specs/020-fix-zero-hp-opacity/tasks.md | 101 ++++++++++++++++++ 9 files changed, 489 insertions(+), 14 deletions(-) create mode 100644 specs/020-fix-zero-hp-opacity/checklists/requirements.md create mode 100644 specs/020-fix-zero-hp-opacity/data-model.md create mode 100644 specs/020-fix-zero-hp-opacity/plan.md create mode 100644 specs/020-fix-zero-hp-opacity/quickstart.md create mode 100644 specs/020-fix-zero-hp-opacity/research.md create mode 100644 specs/020-fix-zero-hp-opacity/spec.md create mode 100644 specs/020-fix-zero-hp-opacity/tasks.md diff --git a/CLAUDE.md b/CLAUDE.md index d7f1ea7..b6aa2a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,6 +70,7 @@ The constitution (`.specify/memory/constitution.md`) governs all feature work: - Browser localStorage (existing adapter, transparent JSON serialization) (016-combatant-ac) - TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Vite 6, Tailwind CSS v4, Lucide React (icons) (017-combat-conditions) - N/A (no storage changes — existing localStorage persistence unchanged) (019-combatant-row-declutter) +- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Lucide React (icons) (020-fix-zero-hp-opacity) ## Recent Changes - 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite diff --git a/apps/web/src/components/combatant-row.tsx b/apps/web/src/components/combatant-row.tsx index 3a55f29..0a06684 100644 --- a/apps/web/src/components/combatant-row.tsx +++ b/apps/web/src/components/combatant-row.tsx @@ -154,17 +154,24 @@ function ClickableHp({ currentHp, maxHp, onAdjust, + dimmed, }: { currentHp: number | undefined; maxHp: number | undefined; onAdjust: (delta: number) => void; + dimmed?: boolean; }) { const [popoverOpen, setPopoverOpen] = useState(false); const status = deriveHpStatus(currentHp, maxHp); if (maxHp === undefined) { return ( - + -- ); @@ -180,6 +187,7 @@ function ClickableHp({ status === "bloodied" && "text-amber-400", status === "unconscious" && "text-red-400", status === "healthy" && "text-foreground", + dimmed && "opacity-50", )} > {currentHp} @@ -271,6 +279,7 @@ export function CombatantRow({ }: CombatantRowProps) { const { id, name, initiative, maxHp, currentHp } = combatant; const status = deriveHpStatus(currentHp, maxHp); + const dimmed = status === "unconscious"; const [pickerOpen, setPickerOpen] = useState(false); const prevHpRef = useRef(currentHp); @@ -309,7 +318,6 @@ export function CombatantRow({ : combatant.isConcentrating ? "border-l-2 border-l-purple-400" : "border-l-2 border-l-transparent", - status === "unconscious" && "opacity-50", isPulsing && "animate-concentration-pulse", )} > @@ -323,7 +331,9 @@ export function CombatantRow({ className={cn( "flex items-center justify-center transition-opacity", combatant.isConcentrating - ? "opacity-100 text-purple-400" + ? dimmed + ? "opacity-50 text-purple-400" + : "opacity-100 text-purple-400" : "opacity-0 group-hover:opacity-50 text-muted-foreground", )} > @@ -336,7 +346,10 @@ export function CombatantRow({ inputMode="numeric" value={initiative ?? ""} placeholder="--" - className="h-7 w-[6ch] text-center text-sm tabular-nums" + className={cn( + "h-7 w-[6ch] text-center text-sm tabular-nums", + dimmed && "opacity-50", + )} onChange={(e) => { const raw = e.target.value; if (raw === "") { @@ -351,10 +364,14 @@ export function CombatantRow({ /> {/* Name */} - +
+ +
{/* AC */} - onSetAc(id, v)} /> +
+ onSetAc(id, v)} /> +
{/* HP */}
@@ -362,20 +379,31 @@ export function CombatantRow({ currentHp={currentHp} maxHp={maxHp} onAdjust={(delta) => onAdjustHp(id, delta)} + dimmed={dimmed} /> {maxHp !== undefined && ( - + / )} - onSetHp(id, v)} /> +
+ onSetHp(id, v)} /> +
{/* Actions */}