- Notifications
You must be signed in to change notification settings - Fork 0
feat(design-system): ratchet raw scale literals and gate type-step selection (#262 parts 2 and 3)#1780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
feat(design-system): ratchet raw scale literals and gate type-step selection (#262 parts 2 and 3) #1780
Changes from all commits
9d53416c2f7f066ee9357227bfedec3a602c6e1fe7f88f90b5f879a71671c1608ac25078facd7File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -289,6 +289,69 @@ const INVERSION_FUNCTION = /(?:invert|hue-rotate)\(/; | ||
| // 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\()[^\]]+\]$/; | ||
| /** | ||
| * Spacing, radius and line-height written as a bare literal — `px-[22px]`, | ||
| * `rounded-[7px]`, `leading-[1.15]` — rather than picked off the scale. | ||
| * | ||
| * These deliberately exempt any arbitrary value containing a CSS *function*, | ||
| * not just `var(`. `tracking-[var(--…)]` above can use the narrower `(?!var\()` | ||
| * because letterspacing is only ever a token or a literal, but padding is not: | ||
| * production carries `pb-[env(safe-area-inset-bottom)]`, | ||
| * `pt-[max(0.75rem,var(--safe-area-top))]`, `pt-[clamp(1.5rem,5vh,3rem)]` and | ||
| * `pb-[calc(7rem+env(safe-area-inset-bottom))]`. Those are computed from the | ||
| * viewport or the safe-area inset, cannot be spelled as a scale step, and are | ||
| * sanctioned. A `(?!var\()` lookahead would flag every one of them, because | ||
| * they open with `max(`, `clamp(`, `env(` or `calc(` rather than `var(`. | ||
| * | ||
| * So the rule is: a value with no function call in it at all is a raw literal. | ||
| * That keeps the sanctioned computed forms out without enumerating them. | ||
| */ | ||
| const RAW_LITERAL_VALUE = String.raw`\[(?![^\]]*\w\()[^\]]+\]`; | ||
| const RAW_PADDING_UTILITY = new RegExp(String.raw`^p[xytrbles]?-${RAW_LITERAL_VALUE}$`); | ||
| const RAW_RADIUS_UTILITY = new RegExp( | ||
| String.raw`^rounded(?:-(?:[trblse]|[tb][lr]|ss|se|ee|es))?-${RAW_LITERAL_VALUE}$`, | ||
| ); | ||
| const RAW_LINE_HEIGHT_UTILITY = new RegExp(String.raw`^leading-${RAW_LITERAL_VALUE}$`); | ||
BigSimmo marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| /** | ||
| * The CSS-declaration half of the same three rules, so a literal cannot simply | ||
| * move from a class into `globals.css` to escape the ratchet — the same reason | ||
| * `legacyShadowAliases` and the colour ratchet count both sides. | ||
| * | ||
| * `0` in any unit carries no design decision and is exempt, as are the CSS-wide | ||
| * keywords and `line-height: normal`. The zero matcher takes any CSS unit | ||
| * identifier (`0dvh`, `0svw`, `0cqw`, `0lh`, …), not a finite allowlist — a | ||
| * closed list falsely counted those as raw debt. | ||
| * | ||
| * Tailwind's arbitrary-property spelling (`[padding:22px]`, | ||
| * `[border-radius:7px]`, `[line-height:1.35]`) reaches the same properties as | ||
| * the named utilities above and is counted with the same raw-literal predicate, | ||
| * or the ratchet could be bypassed by changing syntax alone. | ||
| */ | ||
| const RAW_PADDING_PROPERTY = /^padding(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?$/; | ||
| const RAW_RADIUS_PROPERTY = /^border(?:-(?:top|bottom)-(?:left|right)|-(?:start|end)-(?:start|end))?-radius$/; | ||
| const CSS_WIDE_KEYWORD = /^(?:inherit|initial|unset|revert|revert-layer|normal|auto)$/; | ||
| const CSS_ZERO_VALUE = /^-?0(?:\.0+)?(?:[a-z%]+)?$/i; | ||
| const ARBITRARY_PROPERTY_UTILITY = /^\[([a-z-]+):([^\]]+)\]$/i; | ||
| function isRawScaleLiteralValue(value) { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed || /\w\(/.test(trimmed) || CSS_WIDE_KEYWORD.test(trimmed)) return false; | ||
| return !trimmed.split(/\s+/).every((part) => CSS_ZERO_VALUE.test(part)); | ||
| } | ||
| function recordRawScaleLiteralProperty(result, relativePath, line, prop, value, token) { | ||
| if (!isRawScaleLiteralValue(value)) return; | ||
| const label = token ?? `${prop}: ${value}`; | ||
| if (RAW_PADDING_PROPERTY.test(prop)) { | ||
| result.rawPaddingLiterals.push(`${relativePath}:${line} (${label})`); | ||
| } | ||
| if (RAW_RADIUS_PROPERTY.test(prop)) { | ||
| result.rawRadiusLiterals.push(`${relativePath}:${line} (${label})`); | ||
| } | ||
| if (prop === "line-height") { | ||
| result.rawLineHeightLiterals.push(`${relativePath}:${line} (${label})`); | ||
| } | ||
| } | ||
| 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})?$/; | ||
| @@ -974,7 +1037,11 @@ export function analyzeClassContractsInSource(relativePath, sourceText) { | ||
| legacyTapClasses: [], | ||
| legacyPaletteUtilities: [], | ||
| literalShadowClasses: [], | ||
| rawLineHeightLiterals: [], | ||
| rawPaddingLiterals: [], | ||
| rawRadiusLiterals: [], | ||
| statusColouredNumerals: [], | ||
| typeStepUsages: [], | ||
| unapprovedZIndices: [], | ||
| }; | ||
| if (!analyzer) return result; | ||
| @@ -1057,6 +1124,27 @@ export function analyzeClassContractsInSource(relativePath, sourceText) { | ||
| } | ||
| 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 (RAW_PADDING_UTILITY.test(base)) result.rawPaddingLiterals.push(`${relativePath}:${line} (${token})`); | ||
| if (RAW_RADIUS_UTILITY.test(base)) result.rawRadiusLiterals.push(`${relativePath}:${line} (${token})`); | ||
| if (RAW_LINE_HEIGHT_UTILITY.test(base)) result.rawLineHeightLiterals.push(`${relativePath}:${line} (${token})`); | ||
| const arbitraryProperty = base.match(ARBITRARY_PROPERTY_UTILITY); | ||
| if (arbitraryProperty) { | ||
| recordRawScaleLiteralProperty( | ||
| result, | ||
| relativePath, | ||
| line, | ||
| arbitraryProperty[1].toLowerCase(), | ||
| arbitraryProperty[2], | ||
| token, | ||
| ); | ||
| } | ||
| // Every bare `text-<name>` this file uses, whatever `<name>` turns out to | ||
| // mean. The caller decides which of these are type steps by reading the | ||
| // `@theme` block, so the scale is never spelled out twice — writing the | ||
| // step list here would make this module a second source of truth that | ||
| // globals.css could drift away from silently. | ||
| const bareTextUtility = base.match(/^text-([a-z0-9][a-z0-9-]*)$/); | ||
| if (bareTextUtility) result.typeStepUsages.push(bareTextUtility[1]); | ||
| 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]})`); | ||
| @@ -1186,11 +1274,19 @@ export function analyzeCssContractsInSource(relativePath, sourceText) { | ||
| layoutTransitions: [], | ||
| legacyShadowAliases: [], | ||
| onePixelShadowSpreads: [], | ||
| rawLineHeightLiterals: [], | ||
| rawPaddingLiterals: [], | ||
| rawRadiusLiterals: [], | ||
| rawZIndices: [], | ||
| }; | ||
| for (const declaration of cssDeclarations(sourceText)) { | ||
| const line = declaration.source?.start?.line ?? 1; | ||
| const prop = declaration.prop.toLowerCase(); | ||
| // Custom-property declarations are the token definitions themselves — the | ||
| // scale has to be written down somewhere — so only real properties count. | ||
| if (!prop.startsWith("--")) { | ||
| recordRawScaleLiteralProperty(result, relativePath, line, prop, declaration.value); | ||
| } | ||
| if (/^(?:-webkit-)?(?:backdrop-)?filter$/.test(prop)) { | ||
| for (const match of declaration.value.matchAll(/\b(invert|hue-rotate)\(/g)) { | ||
| result.imageInversions.push(`${relativePath}:${line} (${prop}: ${match[1]}())`); | ||
| @@ -1237,6 +1333,62 @@ export function countRawCssZIndicesInSource(sourceText) { | ||
| return analyzeCssContractsInSource("source.css", sourceText).rawZIndices.length; | ||
| } | ||
| export function findRawScaleLiteralClassesInSource(relativePath, sourceText) { | ||
| const analysis = analyzeClassContractsInSource(relativePath, sourceText); | ||
| return { | ||
| padding: analysis.rawPaddingLiterals, | ||
| radius: analysis.rawRadiusLiterals, | ||
| lineHeight: analysis.rawLineHeightLiterals, | ||
| }; | ||
| } | ||
| export function findTypeStepUsagesInSource(relativePath, sourceText) { | ||
| return analyzeClassContractsInSource(relativePath, sourceText).typeStepUsages; | ||
| } | ||
| /** | ||
| * Direct `var(--text-<step>)` consumers in any production source. The unused-step | ||
| * gate and its exemption anti-rot check must share this predicate — a class-only | ||
| * check lets an exemption survive once a CSS consumer appears. | ||
| * | ||
| * CSS sources are walked declaration-by-declaration so a quoted `content:` | ||
| * string cannot fake a consumer. Non-CSS sources strip comments first, then | ||
| * match `var(--text-*)` in remaining text (covers inline style strings). | ||
| */ | ||
| export function findTypeStepCssUsagesInSource(sourceText, relativePath = "source.css") { | ||
| const steps = []; | ||
| const record = (step) => { | ||
| if (!step.includes("--") && !step.endsWith("-tr")) steps.push(step); | ||
| }; | ||
| if (relativePath.endsWith(".css")) { | ||
| for (const declaration of cssDeclarations(sourceText)) { | ||
| if (declaration.prop.startsWith("--")) continue; | ||
| // Drop CSS string tokens so `content:"var(--text-…)"` cannot count. | ||
| const value = declaration.value.replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/'(?:\\.|[^'\\])*'/g, "''"); | ||
| for (const match of value.matchAll(/var\(\s*--text-([a-z0-9-]+)\s*[,)]/g)) { | ||
| record(match[1]); | ||
| } | ||
| } | ||
| return steps; | ||
| } | ||
| const withoutComments = sourceText.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1"); | ||
| for (const match of withoutComments.matchAll(/var\(\s*--text-([a-z0-9-]+)\s*[,)]/g)) { | ||
| record(match[1]); | ||
| } | ||
| return steps; | ||
| } | ||
| export function findRawScaleLiteralDeclarationsInSource(sourceText) { | ||
| const analysis = analyzeCssContractsInSource("source.css", sourceText); | ||
| return { | ||
| padding: analysis.rawPaddingLiterals, | ||
| radius: analysis.rawRadiusLiterals, | ||
| lineHeight: analysis.rawLineHeightLiterals, | ||
| }; | ||
| } | ||
| export function findDebtPathRegressions(metric, currentByPath, baselineByPath) { | ||
| return Object.entries(currentByPath) | ||
| .filter(([relativePath, count]) => count > (baselineByPath?.[relativePath] ?? 0)) | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.