mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
fix(ui): card density is a Card prop, and compact is compact on desktop
Nineteen cards spelled the compact tier by hand as gap-2 py-3 md:py-4, which left the base md:gap-6 in place: a compact card opened a 24 px gap between header and body on a desktop. Card takes density="compact" now and pins the gap on both breakpoints; every hand-written compact card, the priority card, the document card and the sample briefing move onto it, and a test refuses the class list on a Card. The precision guard on the steps page now reads rendered text only: the page header's grid template (minmax(0,1fr)) matched its pattern.
This commit is contained in:
1 parent
6885377f35
commit
3cc3829fba
27 files changed
+111
-47
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { Card } from "../card";
|
||||
|
||||
function classes(html: string): string[] {
|
||||
return (html.match(/class="([^"]*)"/)?.[1] ?? "").split(" ");
|
||||
}
|
||||
|
||||
describe("<Card density>", () => {
|
||||
it("ships the default rhythm", () => {
|
||||
const c = classes(renderToStaticMarkup(<Card />));
|
||||
expect(c).toEqual(
|
||||
expect.arrayContaining(["gap-4", "py-4", "md:gap-6", "md:py-6"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("pins the compact gap on both breakpoints", () => {
|
||||
const html = renderToStaticMarkup(<Card density="compact" />);
|
||||
const c = classes(html);
|
||||
expect(c).toEqual(
|
||||
expect.arrayContaining(["gap-2", "py-3", "md:gap-2", "md:py-4"]),
|
||||
);
|
||||
// The hand-written tier kept the base desktop gap; the prop must not.
|
||||
expect(c).not.toContain("md:gap-6");
|
||||
expect(c).not.toContain("md:py-6");
|
||||
expect(html).toContain('data-density="compact"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("compact density is the prop, not a class list", () => {
|
||||
it("leaves no hand-written compact tier on a Card", async () => {
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const { join } = await import("node:path");
|
||||
const { walkSourceFiles } =
|
||||
await import("@/__tests__/helpers/source-files");
|
||||
const src = join(__dirname, "../../..");
|
||||
const hits = walkSourceFiles(src, { floor: 2500 })
|
||||
.filter((p) => p.endsWith(".tsx") && !p.includes("__tests__/"))
|
||||
.filter((p) => !p.startsWith("components/ui/"))
|
||||
.filter((p) =>
|
||||
/<Card\b[^>]*className=(?:"[^"]*|\{cn\([^)]*)\bgap-2 py-3\b/.test(
|
||||
readFileSync(join(src, p), "utf8"),
|
||||
),
|
||||
);
|
||||
expect(hits).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -15,12 +15,30 @@ import { cn } from "@/lib/utils";
|
||||
*
|
||||
* Refs `.planning/round-v1433-audit-polish.md` §4.1 + Win 4.
|
||||
*/
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
/**
|
||||
* The two card densities (UI standards §1) live here, not at the call site:
|
||||
* `default` is the card as shipped, `compact` is the stat-strip and
|
||||
* small-tile rhythm. Spelled by hand as `gap-2 py-3 md:py-4`, the compact
|
||||
* tier kept the base `md:gap-6`, so nineteen "compact" cards opened a 24 px
|
||||
* gap between header and body on a desktop. The prop pins both breakpoints.
|
||||
*/
|
||||
const CARD_DENSITY = {
|
||||
default: "",
|
||||
compact: "gap-2 py-3 md:gap-2 md:py-4",
|
||||
} as const;
|
||||
|
||||
function Card({
|
||||
className,
|
||||
density = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { density?: keyof typeof CARD_DENSITY }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-density={density === "default" ? undefined : density}
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-4 rounded-xl border py-4 shadow-sm md:gap-6 md:py-6",
|
||||
CARD_DENSITY[density],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in new issue
Block a user