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
10 changes: 9 additions & 1 deletion scripts/check-design-system-contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const SOURCE_EXTENSIONS = new Set([".css", ".ts", ".tsx"]);
const RAW_COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|oklch)\(/gi;
/** Whole-file backstop for literal shadow utilities the AST class-root pass can miss. */
const LITERAL_SHADOW_TEXT = /(?:^|[\s"'`])shadow-\[(?!var\()[^\]]+\]/g;
const ARBITRARY_TRACKING_TEXT = /(?:^|[\s"'`])tracking-\[(?!var\()[^\]]+\]/g;
const CUSTOM_CONTROL_CLASS_PROP =
/(?:closeButtonClassName|sheetCloseButtonClassName|buttonClassName|triggerClassName)\s*=\s*(?:"([^"]*)"|`([^`]*)`)/g;

Expand Down Expand Up @@ -116,6 +117,7 @@ const metrics = {
legacyPaletteUtilities: 0,
darkColorOverrides: 0,
legacyShadowAliases: 0,
arbitraryTracking: 0,
layoutTransitionExceptions: 0,
textSoftConsumers: 0,
};
Expand Down Expand Up @@ -149,6 +151,7 @@ for (const file of files) {
const classTextSource = withoutComments(source);
const textLegacyTap = countMatches(classTextSource, LEGACY_TAP_CLASS);
const textLiteralShadow = countMatches(classTextSource, LITERAL_SHADOW_TEXT);
const textArbitraryTracking = countMatches(classTextSource, ARBITRARY_TRACKING_TEXT);
assert(
classAnalysis.legacyTapClasses.length >= textLegacyTap,
`${file.relativePath} has ${textLegacyTap} legacy tap class text match(es) but the AST class-root pass only saw ${classAnalysis.legacyTapClasses.length}`,
Expand All @@ -157,11 +160,16 @@ for (const file of files) {
classAnalysis.literalShadowClasses.length >= textLiteralShadow,
`${file.relativePath} has ${textLiteralShadow} literal shadow class text match(es) but the AST class-root pass only saw ${classAnalysis.literalShadowClasses.length}`,
);
assert(
classAnalysis.arbitraryTracking.length >= textArbitraryTracking,
`${file.relativePath} has ${textArbitraryTracking} arbitrary tracking text match(es) but the AST class-root pass only saw ${classAnalysis.arbitraryTracking.length}`,
);
const fileEdgeFindings = classAnalysis.edgeOwnershipConflicts;
recordDebt("edgeOwnershipConflicts", file.relativePath, fileEdgeFindings.length);
recordDebt("legacyPaletteUtilities", file.relativePath, classAnalysis.legacyPaletteUtilities.length);
recordDebt("darkColorOverrides", file.relativePath, classAnalysis.darkColorOverrides.length);
recordDebt("legacyShadowAliases", file.relativePath, classAnalysis.legacyShadowAliases.length);
recordDebt("arbitraryTracking", file.relativePath, classAnalysis.arbitraryTracking.length);
densityOverrideFindings.push(...classAnalysis.densityOverrides);
hardcodedMotionClassFindings.push(...classAnalysis.hardcodedMotionClasses);
layoutTransitionFindings.push(...classAnalysis.layoutTransitions);
Expand Down Expand Up @@ -374,7 +382,7 @@ console.log(
`Design-system contract passed (${files.length} production files; raw colors ${metrics.rawColorLiterals}; literal shadows ${metrics.literalShadowClasses}; legacy tap classes ${metrics.legacyTapClasses}; edge conflicts ${metrics.edgeOwnershipConflicts}; 1px shadow spreads ${metrics.onePixelShadowSpreads}).`,
);
console.log(
`Motion/z/palette ratchets: hardcoded CSS durations ${metrics.hardcodedCssMotionDurations}; layout transitions ${metrics.layoutTransitionExceptions}; raw CSS z-index ${metrics.rawCssZIndices}; legacy palette utilities ${metrics.legacyPaletteUtilities}; dark color overrides ${metrics.darkColorOverrides}; legacy shadow aliases ${metrics.legacyShadowAliases}.`,
`Motion/z/palette ratchets: hardcoded CSS durations ${metrics.hardcodedCssMotionDurations}; layout transitions ${metrics.layoutTransitionExceptions}; raw CSS z-index ${metrics.rawCssZIndices}; legacy palette utilities ${metrics.legacyPaletteUtilities}; dark color overrides ${metrics.darkColorOverrides}; legacy shadow aliases ${metrics.legacyShadowAliases}; arbitrary tracking ${metrics.arbitraryTracking}.`,
);
console.log(`Text-role ratchet: --text-soft consumers ${metrics.textSoftConsumers}.`);
console.log(`Raw-color exemptions: ${RAW_COLOR_EXEMPTIONS.map(({ category }) => category).join(", ")}.`);
43 changes: 43 additions & 0 deletions scripts/design-system-contract-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"legacyPaletteUtilities": 2,
"darkColorOverrides": 3,
"legacyShadowAliases": 231,
"arbitraryTracking": 68,
"layoutTransitionExceptions": 12,
"textSoftConsumers": 0
},
Expand Down Expand Up @@ -147,6 +148,48 @@
"src/components/ui/tooltip.tsx": 1,
"src/components/ui-primitives.tsx": 6
},
"arbitraryTracking": {
"src/app/reference/colour-coding/page.tsx": 1,
"src/components/AccessibleTable.tsx": 2,
"src/components/calculators/bedside-sheet.tsx": 1,
"src/components/calculators/clinical-console.tsx": 1,
"src/components/calculators/directory-grid.tsx": 3,
"src/components/calculators/guided-flow.tsx": 4,
"src/components/calculators/search-detail.tsx": 4,
"src/components/calculators/search-page.tsx": 1,
"src/components/clinical-dashboard/account-setup-dialog.tsx": 1,
"src/components/clinical-dashboard/cross-mode-links.tsx": 1,
"src/components/clinical-dashboard/differentials-home.tsx": 3,
"src/components/clinical-dashboard/document-results.tsx": 1,
"src/components/clinical-dashboard/document-search-results.tsx": 2,
"src/components/clinical-dashboard/evidence-panels.tsx": 1,
"src/components/clinical-dashboard/favourites-command-library-page.tsx": 1,
"src/components/clinical-dashboard/library-health-strip.tsx": 1,
"src/components/clinical-dashboard/medication-prescribing-workspace.tsx": 1,
"src/components/clinical-dashboard/output-panel.tsx": 2,
"src/components/clinical-dashboard/search-pins-menu.tsx": 1,
"src/components/clinical-dashboard/settings-dialog.tsx": 1,
"src/components/clinical-dashboard/universal-search-command-surface.tsx": 1,
"src/components/ClinicalDashboard.tsx": 1,
"src/components/differentials/differential-detail-page.tsx": 1,
"src/components/differentials/differential-presentation-workflow-page.tsx": 1,
"src/components/document-viewer/document-clinical-summary.tsx": 3,
"src/components/document-viewer/document-overview-landing.tsx": 1,
"src/components/document-viewer/section-nav.tsx": 2,
"src/components/document-viewer/source-panels.tsx": 5,
"src/components/dsm/dsm-comparison-page.tsx": 1,
"src/components/factsheets/factsheet-detail-page.tsx": 6,
"src/components/factsheets/factsheets-search-page.tsx": 2,
"src/components/mode-nav/mode-nav.tsx": 1,
"src/components/patient-safety-plan.tsx": 2,
"src/components/pwa-lifecycle.tsx": 1,
"src/components/services/services-navigator-page.tsx": 2,
"src/components/specifiers/specifier-ui.tsx": 1,
"src/components/therapy-compass/screens/recommend-screen.tsx": 1,
"src/components/therapy-compass/screens/sheets-screen.tsx": 1,
"src/components/therapy-compass/therapy-card.tsx": 1,
"src/components/ui-primitives.tsx": 1
},
"layoutTransitionExceptions": {
"src/app/globals.css": 4,
"src/components/calculators/guided-flow.tsx": 1,
Expand Down
8 changes: 8 additions & 0 deletions scripts/design-system-contract-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ const DENSITY_HEIGHT_UTILITY = /^(?:h|min-h|max-h|size)-/;
const DENSITY_TEXT_UTILITY = /^text-(?:2xs|xs|sm-minus|sm|base|lg|xl|[2-9]xl|\[[^\]]*(?:px|rem|em|clamp\()[^\]]*\])$/;
const HARDCODED_MOTION_UTILITY = /^(?:duration|delay)-(?:\d+|\[(?!var\(--duration-)[^\]]+\])$/;
const LITERAL_SHADOW_UTILITY = /^shadow-\[(?!var\()[^\]]+\]$/;
// Same shape as the literal-shadow ratchet, for the same reason: 371 arbitrary
// letterspacing values across 31 distinct spellings, seven of them positive steps
// between 0.04 and 0.16em that no reader can tell apart. `tracking-[var(--…)]` is
// the sanctioned token form and is deliberately NOT counted, exactly as
// `text-[color:var(--…)]` is exempt from the type-scale check.
const ARBITRARY_TRACKING_UTILITY = /^tracking-\[(?!var\()[^\]]+\]$/;
const LEGACY_SHADOW_ALIAS = /var\(--shadow-(?:tight|card|soft|hover|elevated|lux|lift)\)/g;
const LEGACY_PALETTE_UTILITY =
/^(?:bg|text|border|ring|outline|fill|stroke|placeholder|from|via|to)-(?:white|black|(?:slate|gray|zinc|neutral|stone)-\d{2,3})(?:\/\d{1,3})?$/;
Expand Down Expand Up @@ -660,6 +666,7 @@ function uniqueTokenEntries(possibilities) {
export function analyzeClassContractsInSource(relativePath, sourceText) {
const analyzer = classExpressionAnalyzer(relativePath, sourceText);
const result = {
arbitraryTracking: [],
darkColorOverrides: [],
densityOverrides: [],
edgeOwnershipConflicts: [],
Expand Down Expand Up @@ -709,6 +716,7 @@ export function analyzeClassContractsInSource(relativePath, sourceText) {
result.hardcodedMotionClasses.push(`${relativePath}:${line} (${token})`);
}
if (LITERAL_SHADOW_UTILITY.test(base)) result.literalShadowClasses.push(`${relativePath}:${line} (${token})`);
if (ARBITRARY_TRACKING_UTILITY.test(base)) result.arbitraryTracking.push(`${relativePath}:${line} (${token})`);
if (hasLegacyTapClass(token)) result.legacyTapClasses.push(`${relativePath}:${line} (${token})`);
for (const match of token.matchAll(LEGACY_SHADOW_ALIAS)) {
result.legacyShadowAliases.push(`${relativePath}:${line} (${match[0]})`);
Comment thread
BigSimmo marked this conversation as resolved.
Expand Down
42 changes: 39 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,46 @@
--leading-display: 1.05;
--leading-prose: 1.6;

/* Uppercase eyebrow tracking. 0.08em was spelled as `tracking-[0.08em]` at 60+
call sites; this makes it a named utility (`tracking-eyebrow`) with the
identical computed value. */
/* ---- Letterspacing scale -------------------------------------------------
Measured 7 Aug 2026, production only (`*-mockups.tsx` and `mockups/` are
design scratch and gate-exempt): 110 arbitrary `tracking-[…]` call sites
across 18 distinct values, against one named token. Counting mockups gives
372/31 — that is the number to quote about the *sprawl*, not about the debt
a gate can act on.

The real defect is sharper than sprawl. 57 of the 110 sites are a single
value, 0.06em — and `--tracking-eyebrow` is declared at 0.08em, so the one
named token does not match the value its own call sites overwhelmingly use.
That is why it has almost no consumers: the token was named for a number
nobody writes.

Named roles, with the measured production counts each one absorbs:

--tracking-display -0.02em folds: -0.008 (1), -0.01 (3), -0.035 (1), -0.04 (1)
--tracking-normal 0 folds: 0.01 (1), 0.02 (2)
--tracking-label 0.06em folds: 0.04 (3), 0.05 (6), 0.06 (57) <- the mass
--tracking-eyebrow 0.08em folds: 0.08 (8), 0.09 (1) [pre-existing, value unchanged]
--tracking-kicker 0.12em folds: 0.1 (7), 0.11 (1), 0.12 (14), 0.16 (1)

Five roles, not seven: production does not justify a second wide step or a
separate tight-display step — the negative cluster is six sites total. No
fold moves a call site by more than 0.04em, and most by 0.01–0.02em.

`--tracking-eyebrow` keeps 0.08em deliberately. Retuning it to 0.06em would
silently move every existing consumer of the token; that is a different
change from naming the scale, and wants its own measured step.

The `arbitraryTracking` contract ratchet holds the family at its current
per-path count (68 statically-resolvable sites across 40 files) so it cannot
regrow while the call-site migration waits on the open answer-shell and
doc-truth PRs. `tracking-[var(--…)]` is the sanctioned token form and is not
counted, exactly as `text-[color:var(--…)]` is exempt from the type-scale
check. */
--tracking-display: -0.02em;
--tracking-normal: 0em;
--tracking-label: 0.06em;
--tracking-eyebrow: 0.08em;
--tracking-kicker: 0.12em;

/* Font families: bind Tailwind's font-sans / font-mono to the loaded Geist
faces (variables set on <html> by next/font). font-mono is used for
Expand Down
Loading