test(ui): gate the two card-header classes that have to agree

The header fix needs two declarations to hold together: the second grid row is
conditional on a description, and CardAction spans the full row set. The second
is the trap. `row-span-2` against a single explicit row mints an implicit second
row and brings the phantom gap straight back for every header that carries an
action but no description.

Reverting it broke nothing. Nothing in the suite asserts a `row-span` class, so
the regression is DOM geometry the unit tests cannot see, and it would have come
back silently at the next tidy-up. This is a literal-class assertion in the shape
`assessment-spine-order.test.ts` already uses, because the question has an exact
answer.

Also finishes the compact-card tier while it is open: three cards on the metric
surfaces still carried `gap-1.5` where the assessment cards carry `gap-2`. One
tier, one value, no `gap-1.5 py-3` left in the tree.
This commit is contained in:
Marc-André Bombeck committed 2026-07-30 15:52:21 +02:00
1 parent 14d0eeeb46
commit 8a2bb70dbd
3 files changed
+65 -3

No files matched your search

@@ -258,7 +258,7 @@ export function MetricStatStrip({
data-multi-series="true"
role="group"
aria-label={groupLabel ?? t("insights.subPage.stats.label")}
className="gap-1.5 py-3 md:py-4"
className="gap-2 py-3 md:py-4"
>
<CardContent>
<div className="grid grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-2">
@@ -291,7 +291,7 @@ export function MetricStatStrip({
? `${t("insights.subPage.stats.label")} — ${seriesLabel}`
: t("insights.subPage.stats.label")
}
className="gap-1.5 py-3 md:py-4"
className="gap-2 py-3 md:py-4"
>
<CardContent>
<SeriesBlock
@@ -326,7 +326,7 @@ function TargetReferencePanel({
<Card
data-slot="metric-target-summary"
data-target-type={target.type}
className="gap-1.5 py-3 md:py-4"
className="gap-2 py-3 md:py-4"
>
{/* Row 1: the canonical tile header — a single-word "Ziel" / "Target"
heading (the range numbers live on the range bar below, so the
@@ -0,0 +1,62 @@
/**
* The Card header reserves its second row only when there is one to fill.
*
* `CardHeader` is a grid. It used to declare `grid-rows-[auto_auto]`
* unconditionally, so a card with a title and no `CardDescription` carried an
* empty second track and drew the header's `gap-2` against it. Together with the
* Card's own `gap-4 md:gap-6` that made the distance from a heading to its body
* 32 px on desktop where 24 px was intended. It was visible, it was reported, and
* it affected 45 of the 48 files that mount a `CardHeader`.
*
* Two class declarations have to agree for the fix to hold:
*
* 1. the second row is conditional on a description being present, and
* 2. `CardAction` spans the full row set rather than a hard-coded two.
*
* The second one is the trap. `row-span-2` against a single explicit row mints
* an implicit second row, which brings the gap straight back for every header
* that carries an action but no description. Reverting it produces no test
* failure anywhere else in the suite, because nothing asserts a `row-span`
* class: the regression is DOM geometry, which the unit tests cannot see.
*
* So this file is the guard. It is deliberately a literal-class assertion, the
* same shape `assessment-spine-order.test.ts` already uses, because the question
* has an exact answer and an exact answer belongs in a gate rather than in a
* reviewer's memory.
*/
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const SOURCE = readFileSync(
join(process.cwd(), "src/components/ui/card.tsx"),
"utf8",
);
describe("CardHeader row reservation", () => {
it("reserves the second row only when a description is present", () => {
expect(SOURCE).toContain(
"has-data-[slot=card-description]:grid-rows-[auto_auto]",
);
});
it("never declares the second row unconditionally", () => {
// The unconditional form is what drew the phantom gap. It may only ever
// appear behind the `has-data-[slot=card-description]:` variant, so the bare
// token must not occur on its own.
const bare = SOURCE.replace(
/has-data-\[slot=card-description\]:grid-rows-\[auto_auto\]/g,
"",
);
expect(bare).not.toContain("grid-rows-[auto_auto]");
});
});
describe("CardAction row span", () => {
it("spans the full row set rather than a hard-coded two", () => {
// `row-span-2` would mint an implicit second row in a description-less
// header and resurrect the gap the header fix just removed.
expect(SOURCE).toContain("row-span-full");
expect(SOURCE).not.toContain("row-span-2");
});
});