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) <noreply@anthropic.com>
This commit is contained in:
@@ -143,7 +143,6 @@ describe("App integration", () => {
|
|||||||
await addCombatant(user, "Ogre", { maxHp: "59" });
|
await addCombatant(user, "Ogre", { maxHp: "59" });
|
||||||
|
|
||||||
// Verify HP displays — currentHp and maxHp both show "59"
|
// Verify HP displays — currentHp and maxHp both show "59"
|
||||||
expect(screen.getByText("/")).toBeInTheDocument();
|
|
||||||
const hpButton = screen.getByRole("button", {
|
const hpButton = screen.getByRole("button", {
|
||||||
name: "Current HP: 59 (healthy)",
|
name: "Current HP: 59 (healthy)",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import { PLAYER_COLOR_HEX } from "../player-icon-map.js";
|
|||||||
const TEMP_HP_REGEX = /^\+\d/;
|
const TEMP_HP_REGEX = /^\+\d/;
|
||||||
|
|
||||||
// Mock persistence — no localStorage interaction
|
// Mock persistence — no localStorage interaction
|
||||||
const mockLoadEncounter = vi.fn<() => unknown>(() => null);
|
|
||||||
vi.mock("../../persistence/encounter-storage.js", () => ({
|
vi.mock("../../persistence/encounter-storage.js", () => ({
|
||||||
loadEncounter: () => mockLoadEncounter(),
|
loadEncounter: () => null,
|
||||||
saveEncounter: () => {},
|
saveEncounter: () => {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -126,14 +125,14 @@ describe("CombatantRow", () => {
|
|||||||
expect(nameContainer).not.toBeNull();
|
expect(nameContainer).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows '--' for current HP when no maxHp is set", () => {
|
it("shows 'Max' placeholder when no maxHp is set", () => {
|
||||||
renderRow({
|
renderRow({
|
||||||
combatant: {
|
combatant: {
|
||||||
id: combatantId("1"),
|
id: combatantId("1"),
|
||||||
name: "Goblin",
|
name: "Goblin",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(screen.getByLabelText("No HP set")).toBeInTheDocument();
|
expect(screen.getByText("Max")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows concentration icon when isConcentrating is true", () => {
|
it("shows concentration icon when isConcentrating is true", () => {
|
||||||
@@ -245,11 +244,6 @@ describe("CombatantRow", () => {
|
|||||||
tempHp: 8,
|
tempHp: 8,
|
||||||
isConcentrating: true,
|
isConcentrating: true,
|
||||||
};
|
};
|
||||||
mockLoadEncounter.mockReturnValueOnce({
|
|
||||||
combatants: [combatant],
|
|
||||||
activeIndex: 0,
|
|
||||||
roundNumber: 1,
|
|
||||||
});
|
|
||||||
const { rerender, container } = renderRow({ combatant });
|
const { rerender, container } = renderRow({ combatant });
|
||||||
// Temp HP absorbs all damage, currentHp unchanged
|
// Temp HP absorbs all damage, currentHp unchanged
|
||||||
rerender(
|
rerender(
|
||||||
@@ -265,20 +259,15 @@ describe("CombatantRow", () => {
|
|||||||
|
|
||||||
describe("temp HP display", () => {
|
describe("temp HP display", () => {
|
||||||
it("shows +N when combatant has temp HP", () => {
|
it("shows +N when combatant has temp HP", () => {
|
||||||
const combatant = {
|
renderRow({
|
||||||
id: combatantId("1"),
|
combatant: {
|
||||||
name: "Goblin",
|
id: combatantId("1"),
|
||||||
maxHp: 20,
|
name: "Goblin",
|
||||||
currentHp: 15,
|
maxHp: 20,
|
||||||
tempHp: 5,
|
currentHp: 15,
|
||||||
};
|
tempHp: 5,
|
||||||
// Provide encounter with tempHp so hasTempHp is true
|
},
|
||||||
mockLoadEncounter.mockReturnValueOnce({
|
|
||||||
combatants: [combatant],
|
|
||||||
activeIndex: 0,
|
|
||||||
roundNumber: 1,
|
|
||||||
});
|
});
|
||||||
renderRow({ combatant });
|
|
||||||
expect(screen.getByText("+5")).toBeInTheDocument();
|
expect(screen.getByText("+5")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,19 +284,15 @@ describe("CombatantRow", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("temp HP display uses cyan color", () => {
|
it("temp HP display uses cyan color", () => {
|
||||||
const combatant = {
|
renderRow({
|
||||||
id: combatantId("1"),
|
combatant: {
|
||||||
name: "Goblin",
|
id: combatantId("1"),
|
||||||
maxHp: 20,
|
name: "Goblin",
|
||||||
currentHp: 15,
|
maxHp: 20,
|
||||||
tempHp: 8,
|
currentHp: 15,
|
||||||
};
|
tempHp: 8,
|
||||||
mockLoadEncounter.mockReturnValueOnce({
|
},
|
||||||
combatants: [combatant],
|
|
||||||
activeIndex: 0,
|
|
||||||
roundNumber: 1,
|
|
||||||
});
|
});
|
||||||
renderRow({ combatant });
|
|
||||||
const tempHpEl = screen.getByText("+8");
|
const tempHpEl = screen.getByText("+8");
|
||||||
expect(tempHpEl.className).toContain("text-cyan-400");
|
expect(tempHpEl.className).toContain("text-cyan-400");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -172,7 +172,12 @@ function MaxHpDisplay({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={startEditing}
|
onClick={startEditing}
|
||||||
className="inline-block h-7 min-w-[3ch] text-center text-muted-foreground text-sm tabular-nums leading-7 transition-colors hover:text-hover-neutral"
|
className={cn(
|
||||||
|
"inline-block h-7 min-w-[3ch] text-center leading-7 transition-colors hover:text-hover-neutral",
|
||||||
|
maxHp === undefined
|
||||||
|
? "text-muted-foreground text-sm"
|
||||||
|
: "text-muted-foreground text-xs",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{maxHp ?? "Max"}
|
{maxHp ?? "Max"}
|
||||||
</button>
|
</button>
|
||||||
@@ -183,35 +188,20 @@ function ClickableHp({
|
|||||||
currentHp,
|
currentHp,
|
||||||
maxHp,
|
maxHp,
|
||||||
tempHp,
|
tempHp,
|
||||||
hasTempHp,
|
|
||||||
onAdjust,
|
onAdjust,
|
||||||
onSetTempHp,
|
onSetTempHp,
|
||||||
dimmed,
|
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
currentHp: number | undefined;
|
currentHp: number | undefined;
|
||||||
maxHp: number | undefined;
|
maxHp: number | undefined;
|
||||||
tempHp: number | undefined;
|
tempHp: number | undefined;
|
||||||
hasTempHp: boolean;
|
|
||||||
onAdjust: (delta: number) => void;
|
onAdjust: (delta: number) => void;
|
||||||
onSetTempHp: (value: number) => void;
|
onSetTempHp: (value: number) => void;
|
||||||
dimmed?: boolean;
|
|
||||||
}>) {
|
}>) {
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
const status = deriveHpStatus(currentHp, maxHp);
|
const status = deriveHpStatus(currentHp, maxHp);
|
||||||
|
|
||||||
if (maxHp === undefined) {
|
if (maxHp === undefined) {
|
||||||
return (
|
return null;
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"inline-block h-7 w-[4ch] text-center text-muted-foreground text-sm tabular-nums leading-7",
|
|
||||||
dimmed && "opacity-50",
|
|
||||||
)}
|
|
||||||
role="status"
|
|
||||||
aria-label="No HP set"
|
|
||||||
>
|
|
||||||
--
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -221,24 +211,17 @@ function ClickableHp({
|
|||||||
onClick={() => setPopoverOpen(true)}
|
onClick={() => setPopoverOpen(true)}
|
||||||
aria-label={`Current HP: ${currentHp}${tempHp ? ` (+${tempHp} temp)` : ""} (${status})`}
|
aria-label={`Current HP: ${currentHp}${tempHp ? ` (+${tempHp} temp)` : ""} (${status})`}
|
||||||
className={cn(
|
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 === "bloodied" && "text-amber-400",
|
||||||
status === "unconscious" && "text-red-400",
|
status === "unconscious" && "text-red-400",
|
||||||
status === "healthy" && "text-foreground",
|
status === "healthy" && "text-foreground",
|
||||||
dimmed && "opacity-50",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{currentHp}
|
{currentHp}
|
||||||
</button>
|
</button>
|
||||||
{!!hasTempHp && (
|
{!!tempHp && (
|
||||||
<span
|
<span className="font-medium text-cyan-400 text-sm leading-7">
|
||||||
className={cn(
|
+{tempHp}
|
||||||
"inline-block min-w-[3ch] text-center text-sm tabular-nums leading-7",
|
|
||||||
tempHp ? "font-medium text-cyan-400" : "invisible",
|
|
||||||
dimmed && "opacity-50",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{tempHp ? `+${tempHp}` : ""}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!!popoverOpen && (
|
{!!popoverOpen && (
|
||||||
@@ -463,7 +446,6 @@ export function CombatantRow({
|
|||||||
setHp,
|
setHp,
|
||||||
adjustHp,
|
adjustHp,
|
||||||
setTempHp,
|
setTempHp,
|
||||||
hasTempHp,
|
|
||||||
setAc,
|
setAc,
|
||||||
toggleCondition,
|
toggleCondition,
|
||||||
toggleConcentration,
|
toggleConcentration,
|
||||||
@@ -615,29 +597,26 @@ export function CombatantRow({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HP */}
|
{/* HP */}
|
||||||
<div className="flex items-center gap-1">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-md tabular-nums",
|
||||||
|
maxHp === undefined
|
||||||
|
? ""
|
||||||
|
: "gap-0.5 border border-border/50 bg-muted/30 px-1.5",
|
||||||
|
dimmed && "opacity-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<ClickableHp
|
<ClickableHp
|
||||||
currentHp={currentHp}
|
currentHp={currentHp}
|
||||||
maxHp={maxHp}
|
maxHp={maxHp}
|
||||||
tempHp={combatant.tempHp}
|
tempHp={combatant.tempHp}
|
||||||
hasTempHp={hasTempHp}
|
|
||||||
onAdjust={(delta) => adjustHp(id, delta)}
|
onAdjust={(delta) => adjustHp(id, delta)}
|
||||||
onSetTempHp={(value) => setTempHp(id, value)}
|
onSetTempHp={(value) => setTempHp(id, value)}
|
||||||
dimmed={dimmed}
|
|
||||||
/>
|
/>
|
||||||
{maxHp !== undefined && (
|
{maxHp !== undefined && (
|
||||||
<span
|
<span className="text-muted-foreground/50 text-xs">/</span>
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground text-sm tabular-nums",
|
|
||||||
dimmed && "opacity-50",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
/
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
<div className={cn(dimmed && "opacity-50")}>
|
<MaxHpDisplay maxHp={maxHp} onCommit={(v) => setHp(id, v)} />
|
||||||
<MaxHpDisplay maxHp={maxHp} onCommit={(v) => setHp(id, v)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|||||||
Reference in New Issue
Block a user