From 9cdf004c159112b1f4225e307ecf4c8917955b39 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 23 Mar 2026 13:11:28 +0100 Subject: [PATCH] Restyle HP display as compact rounded pill Group current HP, temp HP, and max HP into a single bordered pill container with a subtle slash separator. Removes the scattered layout with separate elements and gaps. Temp HP +N only renders when present (no invisible spacer). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/__tests__/app-integration.test.tsx | 1 - .../__tests__/combatant-row.test.tsx | 53 ++++++--------- apps/web/src/components/combatant-row.tsx | 65 +++++++------------ 3 files changed, 41 insertions(+), 78 deletions(-) diff --git a/apps/web/src/__tests__/app-integration.test.tsx b/apps/web/src/__tests__/app-integration.test.tsx index 97a0e29..366e0e5 100644 --- a/apps/web/src/__tests__/app-integration.test.tsx +++ b/apps/web/src/__tests__/app-integration.test.tsx @@ -143,7 +143,6 @@ describe("App integration", () => { await addCombatant(user, "Ogre", { maxHp: "59" }); // Verify HP displays — currentHp and maxHp both show "59" - expect(screen.getByText("/")).toBeInTheDocument(); const hpButton = screen.getByRole("button", { name: "Current HP: 59 (healthy)", }); diff --git a/apps/web/src/components/__tests__/combatant-row.test.tsx b/apps/web/src/components/__tests__/combatant-row.test.tsx index f16b7af..fccb223 100644 --- a/apps/web/src/components/__tests__/combatant-row.test.tsx +++ b/apps/web/src/components/__tests__/combatant-row.test.tsx @@ -12,9 +12,8 @@ import { PLAYER_COLOR_HEX } from "../player-icon-map.js"; const TEMP_HP_REGEX = /^\+\d/; // Mock persistence — no localStorage interaction -const mockLoadEncounter = vi.fn<() => unknown>(() => null); vi.mock("../../persistence/encounter-storage.js", () => ({ - loadEncounter: () => mockLoadEncounter(), + loadEncounter: () => null, saveEncounter: () => {}, })); @@ -126,14 +125,14 @@ describe("CombatantRow", () => { expect(nameContainer).not.toBeNull(); }); - it("shows '--' for current HP when no maxHp is set", () => { + it("shows 'Max' placeholder when no maxHp is set", () => { renderRow({ combatant: { id: combatantId("1"), name: "Goblin", }, }); - expect(screen.getByLabelText("No HP set")).toBeInTheDocument(); + expect(screen.getByText("Max")).toBeInTheDocument(); }); it("shows concentration icon when isConcentrating is true", () => { @@ -245,11 +244,6 @@ describe("CombatantRow", () => { tempHp: 8, isConcentrating: true, }; - mockLoadEncounter.mockReturnValueOnce({ - combatants: [combatant], - activeIndex: 0, - roundNumber: 1, - }); const { rerender, container } = renderRow({ combatant }); // Temp HP absorbs all damage, currentHp unchanged rerender( @@ -265,20 +259,15 @@ describe("CombatantRow", () => { describe("temp HP display", () => { it("shows +N when combatant has temp HP", () => { - const combatant = { - id: combatantId("1"), - name: "Goblin", - maxHp: 20, - currentHp: 15, - tempHp: 5, - }; - // Provide encounter with tempHp so hasTempHp is true - mockLoadEncounter.mockReturnValueOnce({ - combatants: [combatant], - activeIndex: 0, - roundNumber: 1, + renderRow({ + combatant: { + id: combatantId("1"), + name: "Goblin", + maxHp: 20, + currentHp: 15, + tempHp: 5, + }, }); - renderRow({ combatant }); expect(screen.getByText("+5")).toBeInTheDocument(); }); @@ -295,19 +284,15 @@ describe("CombatantRow", () => { }); it("temp HP display uses cyan color", () => { - const combatant = { - id: combatantId("1"), - name: "Goblin", - maxHp: 20, - currentHp: 15, - tempHp: 8, - }; - mockLoadEncounter.mockReturnValueOnce({ - combatants: [combatant], - activeIndex: 0, - roundNumber: 1, + renderRow({ + combatant: { + id: combatantId("1"), + name: "Goblin", + maxHp: 20, + currentHp: 15, + tempHp: 8, + }, }); - renderRow({ combatant }); const tempHpEl = screen.getByText("+8"); expect(tempHpEl.className).toContain("text-cyan-400"); }); diff --git a/apps/web/src/components/combatant-row.tsx b/apps/web/src/components/combatant-row.tsx index 764ac35..c9537f0 100644 --- a/apps/web/src/components/combatant-row.tsx +++ b/apps/web/src/components/combatant-row.tsx @@ -172,7 +172,12 @@ function MaxHpDisplay({ @@ -183,35 +188,20 @@ function ClickableHp({ currentHp, maxHp, tempHp, - hasTempHp, onAdjust, onSetTempHp, - dimmed, }: Readonly<{ currentHp: number | undefined; maxHp: number | undefined; tempHp: number | undefined; - hasTempHp: boolean; onAdjust: (delta: number) => void; onSetTempHp: (value: number) => void; - dimmed?: boolean; }>) { const [popoverOpen, setPopoverOpen] = useState(false); const status = deriveHpStatus(currentHp, maxHp); if (maxHp === undefined) { - return ( - - -- - - ); + return null; } return ( @@ -221,24 +211,17 @@ function ClickableHp({ onClick={() => setPopoverOpen(true)} aria-label={`Current HP: ${currentHp}${tempHp ? ` (+${tempHp} temp)` : ""} (${status})`} className={cn( - "inline-block h-7 min-w-[3ch] text-center font-medium text-sm tabular-nums leading-7 transition-colors hover:text-hover-neutral", + "inline-block h-7 min-w-[3ch] text-center font-medium text-sm leading-7 transition-colors hover:text-hover-neutral", status === "bloodied" && "text-amber-400", status === "unconscious" && "text-red-400", status === "healthy" && "text-foreground", - dimmed && "opacity-50", )} > {currentHp} - {!!hasTempHp && ( - - {tempHp ? `+${tempHp}` : ""} + {!!tempHp && ( + + +{tempHp} )} {!!popoverOpen && ( @@ -463,7 +446,6 @@ export function CombatantRow({ setHp, adjustHp, setTempHp, - hasTempHp, setAc, toggleCondition, toggleConcentration, @@ -615,29 +597,26 @@ export function CombatantRow({ {/* HP */} -
+
adjustHp(id, delta)} onSetTempHp={(value) => setTempHp(id, value)} - dimmed={dimmed} /> {maxHp !== undefined && ( - - / - + / )} -
- setHp(id, v)} /> -
+ setHp(id, v)} />
{/* Actions */}