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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<span className="inline-block h-7 w-[4ch] text-center text-sm leading-7 tabular-nums text-muted-foreground">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-7 w-[4ch] text-center text-sm leading-7 tabular-nums text-muted-foreground",
|
||||
dimmed && "opacity-50",
|
||||
)}
|
||||
>
|
||||
--
|
||||
</span>
|
||||
);
|
||||
@@ -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 */}
|
||||
<EditableName name={name} combatantId={id} onRename={onRename} />
|
||||
<div className={cn(dimmed && "opacity-50")}>
|
||||
<EditableName name={name} combatantId={id} onRename={onRename} />
|
||||
</div>
|
||||
|
||||
{/* AC */}
|
||||
<AcDisplay ac={combatant.ac} onCommit={(v) => onSetAc(id, v)} />
|
||||
<div className={cn(dimmed && "opacity-50")}>
|
||||
<AcDisplay ac={combatant.ac} onCommit={(v) => onSetAc(id, v)} />
|
||||
</div>
|
||||
|
||||
{/* HP */}
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -362,20 +379,31 @@ export function CombatantRow({
|
||||
currentHp={currentHp}
|
||||
maxHp={maxHp}
|
||||
onAdjust={(delta) => onAdjustHp(id, delta)}
|
||||
dimmed={dimmed}
|
||||
/>
|
||||
{maxHp !== undefined && (
|
||||
<span className="text-sm tabular-nums text-muted-foreground">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm tabular-nums text-muted-foreground",
|
||||
dimmed && "opacity-50",
|
||||
)}
|
||||
>
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
<MaxHpDisplay maxHp={maxHp} onCommit={(v) => onSetHp(id, v)} />
|
||||
<div className={cn(dimmed && "opacity-50")}>
|
||||
<MaxHpDisplay maxHp={maxHp} onCommit={(v) => onSetHp(id, v)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||
className={cn(
|
||||
"h-7 w-7 text-muted-foreground hover:text-destructive",
|
||||
dimmed && "opacity-50",
|
||||
)}
|
||||
onClick={() => onRemove(id)}
|
||||
title="Remove combatant"
|
||||
aria-label="Remove combatant"
|
||||
@@ -386,11 +414,13 @@ export function CombatantRow({
|
||||
|
||||
{/* Conditions */}
|
||||
<div className="relative ml-[calc(3rem+0.75rem)]">
|
||||
<ConditionTags
|
||||
conditions={combatant.conditions}
|
||||
onRemove={(conditionId) => onToggleCondition(id, conditionId)}
|
||||
onOpenPicker={() => setPickerOpen((prev) => !prev)}
|
||||
/>
|
||||
<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}
|
||||
|
||||
Reference in New Issue
Block a user