Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions src/lib/tailwind-merge.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,19 +63,34 @@ export const CLINICAL_TWMERGE_THEME = {
// which are padding utilities and should conflict with pt-*/pb-* like any
// other.
//
// `tap` (--spacing-tap, the 48px target knob) is DELIBERATELY ABSENT, and
// this is the one omission that is not an oversight. Tailwind emits
// `.min-h-tap` after every numeric `.min-h-*` and `.h-tap` after `.h-4` /
// `.h-10.5`, so at equal specificity the tap token wins today wherever a
// call site pairs the two. Declaring `tap` here would hand the win to the
// later class instead — measured across 22 call sites (document-admin,
// DocumentManagerPanel, favourites-hub, settings-dialog, service-detail-page,
// form-detail-page, clinical-output-helpers, account-setup-dialog), 18 of
// which would drop from 48px to 32/36/40/42px. AGENTS.md and SPEC §4.10 are
// explicit that no production target is ever reduced, so the merge stays off
// for this family until those sites drop the numeric class they already
// cannot apply. Until then `min-h-tap min-h-9` passes through unmerged,
// exactly as it does today; `tests/tailwind-merge-config.test.ts` pins that.
// `tap` (--spacing-tap, the 48px target knob) was held out of this list until
// 8 August 2026. The stated reason was that declaring it hands the win to the
// later class — "22 call sites, 18 of which would drop from 48px to
// 32/36/40/42px". That figure did not survive re-measurement (#270), and the
// omission is no longer justified:
//
// - There are ZERO same-variant tap/numeric pairs in production source. The
// 84 survivors the old scan counted are cross-variant responsive
// step-downs (`min-h-tap` with `sm:min-h-9`, `h-10.5` with `sm:h-tap`),
// and tailwind-merge groups by variant, so declaring `tap` cannot reach
// them. They are live breakpoint steps, not dead classes — deleting them
// would RAISE those controls at their breakpoint.
// - The named call sites were stale: DocumentManagerPanel and
// settings-dialog carry no tap token at all, and document-admin and
// service-detail-page pair theirs with no numeric height.
// - A per-string-literal scan cannot see a conflict composed across `cn()`
// arguments — `cn(recipe, "min-h-tap")` pairs the recipe's `min-h-7` with
// the token. A composition-aware sweep that resolves constant recipe
// identifiers at each of the 1418 `cn()` call sites finds zero
// same-variant pairs in either direction. It was mutation-tested against
// synthetic `cn("h-tap", "h-4")` and `cn("min-h-tap", recipe)` probes,
// both of which it flags, so that zero is a measurement rather than a
// pattern that never matches.
//
// AGENTS.md and SPEC §4.10 remain explicit that no production target is ever
// reduced. That rule is unchanged; what changed is the evidence that declaring
// `tap` would reduce one. Re-measure before introducing a same-variant pair:
// order decides the outcome, and tap-then-numeric is the forbidden direction.
spacing: [
"icon-xs",
"icon-sm",
Expand All@@ -86,6 +101,7 @@ export const CLINICAL_TWMERGE_THEME = {
"mode-home-composer-wide",
"safe",
"safe-2",
"tap",
],

// globals.css @theme --ease-*.
Expand Down
32 changes: 21 additions & 11 deletions tests/tailwind-merge-config.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,15 +106,25 @@ describe("custom @theme scales are not misclassified", () => {
});
});

describe("--spacing-tap is deliberately NOT merged", () => {
// Tailwind emits `.min-h-tap` after every numeric `.min-h-*`, so the tap token
// wins today at the 22 call sites that pair them. Declaring `tap` in the merge
// config would hand the win to the numeric class and drop 18 production targets
// below 48px. If someone adds `tap` to `theme.spacing`, this fails.
it("passes min-h-tap through beside a smaller numeric min-h", () => {
expect(cn("min-h-tap", "min-h-9")).toBe("min-h-tap min-h-9");
expect(cn("min-h-tap", "min-h-8")).toBe("min-h-tap min-h-8");
expect(cn("h-tap", "h-10.5")).toBe("h-tap h-10.5");
describe("--spacing-tap merges, and only within a variant", () => {
// `tap` was pinned out of the merge config on the premise that declaring it
// would drop 18 production targets below 48px across 22 call sites. That did
// not survive re-measurement (#270): a composition-aware sweep resolving
// constant recipe identifiers at all 1418 `cn()` call sites finds ZERO
// same-variant tap/numeric pairs, so there is nothing for the merge to lower.
it("merges a same-variant pair, last class winning", () => {
expect(cn("min-h-9", "min-h-tap")).toBe("min-h-tap");
expect(cn("h-10.5", "h-tap")).toBe("h-tap");
});

// The load-bearing half. The 84 surviving pairs in production are all
// cross-variant responsive step-downs, and tailwind-merge groups by variant,
// so they must pass through untouched. If this ever merges, a control loses
// its breakpoint step and the sweep's zero stops meaning anything.
it("leaves cross-variant responsive step-downs alone", () => {
expect(cn("min-h-tap", "sm:min-h-9")).toBe("min-h-tap sm:min-h-9");
expect(cn("h-10.5", "sm:h-tap")).toBe("h-10.5 sm:h-tap");
expect(cn("min-h-tap", "lg:min-h-9")).toBe("min-h-tap lg:min-h-9");
});
});

Expand DownExpand Up@@ -148,13 +158,13 @@ describe("the config tracks globals.css", () => {
},
);

it("declares every --spacing-* token except the deliberately-held tap knob", () => {
it("declares every --spacing-* token, including the tap knob", () => {
const fromCss = declared("spacing");
expect(fromCss).toContain("tap");
expect([...CLINICAL_TWMERGE_THEME.spacing].sort()).toEqual(
// `safe` / `safe-2` come from @utility rules, not @theme, so they are
// config-only and are added to the CSS-derived list for the comparison.
[...fromCss.filter((name) => name !== "tap"), "safe", "safe-2"].sort(),
[...fromCss, "safe", "safe-2"].sort(),
);
});

Expand Down
Loading