Report a colour utility on the application's own theme colour in the design guard

The guard knew M3's roles and Tailwind's palette, so `bg-off-plan`,
`text-sport-run`, `from-brand` or `border-l-zone-4` - colours an
application's own Tailwind theme named before 2.0.0 - were the leftovers
its count never showed, though none has compiled since. After every
other family, a class-list token shaped like a colour utility on a name
that starts with a letter is now reported with the application's own
`var(--...)` as its replacement (`color-mix()` with an opacity), unless
the application's CSS declares the class.

So that none of Tailwind's own utilities on those prefixes reads as a
theme colour, each one Tailwind 4.2 lists is now named by its family:
`border-collapse`, `border-spacing-*`, the block borders `border-bs`/
`border-be`, `bg-blend-*` and the other background positions and
angles, gradient stops, `fill-none`/`stroke-*`, `text-shadow-*`,
`shadow-inner`, `accent-auto`, `inherit`/`initial` as colour keywords,
and the `mauve`, `olive`, `mist` and `taupe` palettes; only
`inset-ring-*` is still not read. BoostVocabularyTest no longer reads
a span holding a single property, prop or role name (`border-color`,
`placeholder-value`, `outline-variant`) as a class list. A fixture
test covers the theme colours and the Tailwind names beside them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-17 04:18:10 +02:00
co-authored by Claude Opus 5
parent 394c9326ac
commit 613ac20013
9 changed files with 169 additions and 21 deletions
+1 -1
View File
@@ -150,7 +150,7 @@ it('uses only what compiles', function () {
});
```
The guard reads every path it is given and fails, with `path:line` and the 2.0.0 replacement, on any Tailwind utility or variant still in a view, PHP or JS file — none compiles, since the application carries no Tailwind — and on 1.x's own utilities (`type-body-md`, `text-meta`, `rounded-corner-lg`, `focus-ring`): a layout component and prop (`flex gap-4``<x-row gap="space200">`), an `md-*` class (`truncate``md-truncate`), or a token for the application's own CSS (`rounded-lg``var(--md-sys-shape-corner-lg)`). A class the application's own stylesheets declare is exempt. In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, easing or duration, and on a media query off M3's 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine. It also fails on unknown Material Symbol names and Blade directives written inside component tags.
The guard reads every path it is given and fails, with `path:line` and the 2.0.0 replacement, on any Tailwind utility or variant still in a view, PHP or JS file, a colour of the application's own Tailwind theme (`bg-brand`) included — none compiles, since the application carries no Tailwind — and on 1.x's own utilities (`type-body-md`, `text-meta`, `rounded-corner-lg`, `focus-ring`): a layout component and prop (`flex gap-4``<x-row gap="space200">`), an `md-*` class (`truncate``md-truncate`), or a token for the application's own CSS (`rounded-lg``var(--md-sys-shape-corner-lg)`). A class the application's own stylesheets declare is exempt. In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, easing or duration, and on a media query off M3's 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine. It also fails on unknown Material Symbol names and Blade directives written inside component tags.
`missingStylesheets($cssEntry)` checks the entry's relative `@import` graph, followed through every package file's own imports, against the package tags the views render (unprefixed, under the configured prefix, or `<x-livewire-material::…>`) and `->links()`, and names each missing `@import` line once; a tag the application shadows with its own component is reported instead. It reads imports only, so leave `resource_path('css')` out of `scan()` while the stylesheets still hold literals. `forbidColours([...])` names roles the application leaves out, and fails wherever one (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds any pattern of your own, line by line.
+8
View File
@@ -9,6 +9,14 @@
it in the layout, `toggle()` opens and closes the modal rail (`show()`, `hide()`) instead of
changing a choice nothing draws there. A shortcut that pressed the rail's menu button to get this
can call `$store.rail.toggle()` again.
- **`DesignGuard`** reports a colour utility on a colour the application's own Tailwind theme
named (`bg-brand`, `text-sport-run`, `from-brand`, `border-l-zone-4`, `bg-brand/8`) in a class
list, with `var(--…)` of the application's own as its replacement, unless the application's
CSS declares the class. None of them has compiled since 2.0.0, and they were the leftovers its
count never showed. Tailwind's other utilities on the same prefixes are named by their own
family now too: `border-collapse`, `border-spacing-*`, `from-10%`, `via-none`, `fill-none`,
`stroke-*`, `text-shadow-*`, `text-inherit`, and Tailwind 4.2's `mauve`, `olive`, `mist` and
`taupe` palettes. A test that expects no findings may fail on them.
## From 2.0.0 to 2.1.0
@@ -1196,6 +1196,7 @@ It reads every path it is given and fails with `path:line` on unknown symbol nam
| `border-outline-variant`, `border-divider` | `<x-divider>` or `<x-surface outlined>` |
| `bg-surface-container` | `<x-surface level="surface-container">` |
| `bg-primary`, `border-error`, `text-red-500` … | `var(--md-sys-color-*)` |
| `bg-brand`, `text-sport-run`, `border-l-zone-4/40` … (a colour your own Tailwind theme named) | your own `var(--…)`, or `color-mix(in srgb, var(--…) 40%, transparent)` |
| `bg-white`, `text-black` | a surface or `on-` role — M3 has no white or black |
| `text-on-surface/60`, `bg-on-surface/8` | `md-ink-variant`/`md-ink-quiet`, or `md-state-layer` for an overlay |
| `rounded-lg`, `rounded-corner-lg` (1.x) | `var(--md-sys-shape-corner-lg)`, or `<x-surface corner="lg">` |
+49 -16
View File
@@ -19,7 +19,8 @@ use Symfony\Component\Finder\Finder;
* cleared scale (radius, shadow, type size/weight/leading/tracking, easing, duration), a
* layout, spacing, sizing, position, border, effect, interactivity, text or display
* utility, a colour utility on a role, a 1.x ink, white or black (`text-meta`, `bg-white`,
* `text-on-surface/60`), a 1.x package utility (`type-body-md`, `focus-ring`,
* `text-on-surface/60`) or on a colour the application's own Tailwind theme named
* (`bg-brand`, `text-sport-run/60`), a 1.x package utility (`type-body-md`, `focus-ring`,
* `rounded-corner-lg`), a variant, or an arbitrary `[…]` value or property each with its
* 2.0.0 replacement: a layout component and prop (`gap-4` `gap="space200"`), an `md-*`
* class, or a token for the application's own CSS. A class the application's own
@@ -60,14 +61,14 @@ use Symfony\Component\Finder\Finder;
* colour-value families, which also have to see a class assembled as a plain PHP or JS string, as
* an enum's own literal colour string does) requires the utility's actual shape a digit, a
* known scale step or a colour function never a bare word. The trade-off: a bare-word utility
* (`flex`, `hidden`) anywhere but inside a class list, and an application's own Tailwind theme
* names (`bg-brand`), are invisible to this guard.
* (`flex`, `hidden`) and a colour of the application's own Tailwind theme (`bg-brand`) are
* invisible to this guard anywhere but inside a class list.
*/
class DesignGuard
{
protected const string UTILITY = '(?:bg|text|border(?:-[trblxyse])?|ring|ring-offset|fill|stroke|from|via|to|outline|divide|decoration|caret|accent|shadow|placeholder)';
protected const string UTILITY = '(?:bg|text|border(?:-(?:[trblxyse]|bs|be))?|ring|ring-offset|fill|stroke|from|via|to|outline|divide|decoration|caret|accent|shadow|placeholder)';
protected const string PALETTE = '(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-(?:50|[1-9]00|950)';
protected const string PALETTE = '(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone|mauve|olive|mist|taupe)-(?:50|[1-9]00|950)';
/** A colour written as a value: an arbitrary hex, function or mix instead of a role. */
protected const string ARBITRARY_COLOUR = '/(?<![\w-])'.self::UTILITY.'-\[(?:#|rgb|hsl|oklch|color-mix)[^\]\s"\']*\]?/';
@@ -153,21 +154,24 @@ class DesignGuard
['/^(?:min-|max-)?(?:w|h|size)-(?:\d+(?:\.\d+)?|\d+\/\d+|px|full|screen|auto|min|max|fit|dvh|svh|lvh|dvw|svw|lvw|lh|prose|none|3xs|2xs|xs|sm|md|lg|xl|[2-7]xl|screen-(?:sm|md|lg|xl|2xl))$/', 'Tailwind sizing utility `%s` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column\'s measure, `<x-icon size>` an icon\'s; anything else is a length in your own CSS'],
['/^container$/', 'Tailwind\'s `container` compiles to nothing — use `<x-pane width>`, which sets M3\'s margins and a measure'],
['/^(?:table(?:-[a-z-]+)?|flow-root|list-item|inline-table)$/', 'Tailwind display utility `%s` compiles to nothing — write the `display` rule in your own CSS'],
['/^(?:border(?:-[trblxyse])?(?:-\d+)?|border-(?:solid|dashed|dotted|double|hidden|none)|divide-[xy](?:-\d+|-reverse)?|divide-(?:solid|dashed|dotted|double|none))$/', 'Tailwind border utility `%s` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`'],
['/^(?:border(?:-(?:[trblxyse]|bs|be))?(?:-\d+)?|border-(?:solid|dashed|dotted|double|hidden|none)|divide-[xy](?:-\d+|-reverse)?|divide-(?:solid|dashed|dotted|double|none))$/', 'Tailwind border utility `%s` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`'],
['/^rounded(?:-(?:ss|se|ee|es|tl|tr|br|bl|t|r|b|l|s|e))?$/', 'Tailwind radius utility `%s` compiles to nothing — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`'],
['/^(?:shadow|shadow-none|inset-shadow(?:-[\w]+)?|drop-shadow(?:-[\w]+)?)$/', 'Tailwind shadow utility `%s` compiles to nothing — use `var(--md-sys-elevation-*)` in your own CSS'],
['/^(?:shadow|shadow-none|shadow-inner|inset-shadow(?:-[\w]+)?|drop-shadow(?:-[\w]+)?)$/', 'Tailwind shadow utility `%s` compiles to nothing — use `var(--md-sys-elevation-*)` in your own CSS'],
['/^(?:outline(?:-none|-hidden|-\d+|-offset-\d+|-dashed|-dotted|-double|-solid)?|ring(?:-\d+|-inset)?|ring-offset-\d+)$/', 'Tailwind outline utility `%s` compiles to nothing — M3\'s focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS'],
['/^(?:transition(?:-(?:all|colors|opacity|shadow|transform|none|discrete))?|animate-[\w-]+)$/', 'Tailwind motion utility `%s` compiles to nothing — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition` or `animation`'],
['/^opacity-\d+$/', 'Tailwind opacity utility `%s` compiles to nothing — write `opacity` in your own CSS (M3\'s disabled content is 38 %%)'],
['/^(?:transform(?:-none|-gpu|-cpu)?|-?(?:scale|rotate|skew-[xy]|translate-[xy]|scale-[xy])-[\w.\/]+|origin-[\w-]+|will-change-[\w-]+|blur(?:-\w+)?|backdrop-[\w-]+|mix-blend-[\w-]+|isolate|isolation-auto)$/', 'Tailwind effect utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:cursor-[\w-]+|pointer-events-(?:none|auto)|select-(?:none|text|all|auto)|touch-[\w-]+|resize(?:-[xy]|-none)?|appearance-(?:none|auto)|scroll-(?:smooth|auto)|-?scroll-[mp][trblxyse]?-[\w.]+|snap-[\w-]+)$/', 'Tailwind interactivity utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:cursor-[\w-]+|accent-auto|pointer-events-(?:none|auto)|select-(?:none|text|all|auto)|touch-[\w-]+|resize(?:-[xy]|-none)?|appearance-(?:none|auto)|scroll-(?:smooth|auto)|-?scroll-[mp][trblxyse]?-[\w.]+|snap-[\w-]+)$/', 'Tailwind interactivity utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:aspect-(?:auto|square|video|\d+\/\d+)|object-(?:contain|cover|fill|none|scale-down|top|bottom|center|left|right|left-top|left-bottom|right-top|right-bottom))$/', 'Tailwind media utility `%s` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS'],
['/^font-(?:sans|serif)$/', 'Tailwind\'s `%s` compiles to nothing — the foundation already sets the brand typeface; any other `font-family` is your own CSS'],
['/^font-mono$/', 'Tailwind\'s `%s` compiles to nothing — put the value in `<code>`, `<kbd>` or `<samp>`, or use `md-mono` (text.css)'],
['/^antialiased$/', 'Tailwind\'s `%s` compiles to nothing — the foundation already smooths text in grayscale (base.css); drop it'],
['/^(?:underline|no-underline)$/', 'Tailwind\'s `%s` compiles to nothing — `md-link` draws a link (interaction.css); any other decoration is your own CSS'],
['/^(?:uppercase|lowercase|capitalize|normal-case|italic|not-italic|overline|line-through|underline-offset-\w+|decoration-(?:\d+|solid|double|dotted|dashed|wavy|auto|from-font|clone|slice)|subpixel-antialiased|whitespace-(?:normal|pre|pre-line|pre-wrap|break-spaces)|break-(?:words|all|keep|normal)|wrap-(?:break-word|anywhere|normal)|text-(?:wrap|balance|pretty|ellipsis|clip|justify)|line-clamp-(?:\d+|none)|not-sr-only|list-(?:disc|decimal|none|inside|outside)|align-(?:baseline|top|middle|bottom|text-top|text-bottom|sub|super)|indent-[\w.]+|hyphens-(?:none|manual|auto)|(?:normal|lining|oldstyle|proportional)-nums|ordinal|slashed-zero|(?:diagonal|stacked)-fractions)$/', 'Tailwind text utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^bg-(?:(?:gradient-to|linear-to)-[a-z]+|radial|conic|cover|contain|auto|center|top|bottom|left|right|no-repeat|repeat(?:-[xy])?|fixed|local|scroll|none|clip-[a-z]+|origin-[a-z]+)$/', 'Tailwind background utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:uppercase|lowercase|capitalize|normal-case|italic|not-italic|overline|line-through|underline-offset-\w+|decoration-(?:\d+|solid|double|dotted|dashed|wavy|auto|from-font|clone|slice)|subpixel-antialiased|whitespace-(?:normal|pre|pre-line|pre-wrap|break-spaces)|break-(?:words|all|keep|normal)|wrap-(?:break-word|anywhere|normal)|text-(?:wrap|balance|pretty|ellipsis|clip|justify)|text-shadow-[\w-]+(?:\/\d+)?|line-clamp-(?:\d+|none)|not-sr-only|list-(?:disc|decimal|none|inside|outside)|align-(?:baseline|top|middle|bottom|text-top|text-bottom|sub|super)|indent-[\w.]+|hyphens-(?:none|manual|auto)|(?:normal|lining|oldstyle|proportional)-nums|ordinal|slashed-zero|(?:diagonal|stacked)-fractions)$/', 'Tailwind text utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^bg-(?:(?:gradient-to|linear-to)-[a-z]+|(?:linear|conic)-\d+|radial|conic|cover|contain|auto|center|top|bottom|left|right|(?:top|bottom)-(?:left|right)|(?:left|right)-(?:top|bottom)|no-repeat|repeat(?:-[xy]|-round|-space)?|fixed|local|scroll|none|clip-[a-z]+|origin-[a-z]+|blend-[a-z-]+)$/', 'Tailwind background utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:from|via|to)-(?:\d{1,3}%|none)$/', 'Tailwind gradient utility `%s` compiles to nothing — write the gradient in your own CSS'],
['/^(?:border-(?:collapse|separate)|border-spacing(?:-[xy])?-(?:\d+(?:\.\d+)?|px))$/', 'Tailwind table utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:(?:fill|stroke)-none|stroke-\d+)$/', 'Tailwind SVG utility `%s` compiles to nothing — write `fill` or `stroke` in your own CSS'],
];
/** Bare display utilities, matched only as a whole class-list token. */
@@ -194,12 +198,23 @@ class DesignGuard
* optional opacity modifier (`text-on-surface/60`, `bg-scrim/[0.32]`). The roles are every
* `--md-sys-color-*` the default scheme and elevation tokens declare.
*/
protected const string COLOUR_UTILITY = '/^(?<utility>bg|text|border(?:-[trblxyse])?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-'
protected const string COLOUR_UTILITY = '/^(?<utility>bg|text|border(?:-(?:[trblxyse]|bs|be))?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-'
.'(?<role>(?:on-)?(?:primary|secondary|tertiary|error|success|warning|info)(?:-container|-dim|-fixed(?:-dim|-variant)?)?'
.'|inverse-(?:primary|surface|on-surface|error|success|warning|info)'
.'|(?:on-)?background|(?:on-)?surface(?:-variant|-dim|-bright|-container(?:-lowest|-low|-high|-highest)?)?'
.'|outline(?:-variant)?|scrim|shadow|body|meta|quiet|structure|chrome|divider|white|black|current|transparent)'
.'(?:\/(?<opacity>\d{1,3}|\[[^\]]*\]))?$/';
.'|outline(?:-variant)?|scrim|shadow|body|meta|quiet|structure|chrome|divider|white|black|current|transparent|inherit|initial)'
.'(?:\/(?<opacity>\d{1,3}|\[[^\]]*\]|\(--[\w-]+\)))?$/';
/**
* A colour utility on any other name (`bg-off-plan`, `text-sport-run`, `border-l-zone-4`,
* `bg-route-reference/8`): a colour the application's own Tailwind theme named before 2.0.0,
* which nothing declares now. Tried only after `COLOUR_UTILITY`, the palette and every family
* in `FAMILIES`, so a Tailwind utility sharing the prefix (`text-balance`, `border-collapse`,
* `bg-cover`, `shadow-none`) is reported as that family; the name starts with a letter, so a
* width or a stop (`border-2`, `from-10%`) never reads as one.
*/
protected const string THEME_COLOUR_UTILITY = '/^(?<utility>bg|text|border(?:-(?:[trblxyse]|bs|be))?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-'
.'(?<name>[a-z][a-z\d]*(?:-[a-z\d]+)*)(?:\/(?<opacity>\d{1,3}|\[[^\]]*\]|\(--[\w-]+\)))?$/';
/** 1.x's semantic ink names, and the M3 role each resolved to (UPGRADE.md § 4). */
protected const array INK_ALIASES = [
@@ -811,7 +826,25 @@ class DesignGuard
}
}
return null;
return $this->themeColourHint($token);
}
/**
* The 2.0.0 replacement for a colour utility on a name neither M3 nor Tailwind has (see
* `THEME_COLOUR_UTILITY`), or null: the application's own custom property in its own CSS, mixed
* toward transparent where the utility carried an opacity.
*/
protected function themeColourHint(string $token): ?string
{
if (preg_match(self::THEME_COLOUR_UTILITY, $token, $m, PREG_UNMATCHED_AS_NULL) !== 1) {
return null;
}
$dead = "Tailwind colour utility `{$token}` compiles to nothing — `{$m['name']}` is neither an M3 role nor a Tailwind colour, so it named a colour of the application's own theme";
return $m['opacity'] !== null
? "{$dead}: use `color-mix(in srgb, var(--…) <n>%, transparent)` in your own CSS"
: "{$dead}: use `var(--…)` in your own CSS";
}
/**
@@ -930,11 +963,11 @@ class DesignGuard
return match (true) {
in_array($role, ['white', 'black'], true) => "{$dead} — M3 paints with roles, never white or black: `<x-surface level=\"surface-container-lowest\">` or `var(--md-sys-color-surface-container-lowest)` for a white surface, an `on-` role (`md-ink`, `var(--md-sys-color-on-primary)`) for ink",
in_array($role, ['current', 'transparent'], true) => "{$dead} — write `".($role === 'current' ? 'currentColor' : 'transparent').'` in your own CSS',
in_array($role, ['current', 'transparent', 'inherit', 'initial'], true) => "{$dead} — write `".($role === 'current' ? 'currentColor' : $role).'` in your own CSS',
$m['opacity'] !== null && $utility === 'text' => "{$dead} — M3's quieter text is a role, not a faded one: `md-ink-variant` or `md-ink-quiet` (text.css)",
$m['opacity'] !== null => "{$dead} — M3's hover, focus and press overlays are `md-state-layer`; any other tint is `color-mix(in srgb, var(--md-sys-color-{$role}) <n>%, transparent)` in your own CSS",
$utility === 'text' && isset(self::INK_ROLE[$role]) => "{$dead} — use `".self::INK_ROLE[$role].'` (text.css)',
preg_match('/^(?:border(?:-[trblxyse])?|divide)$/', $utility) === 1 && in_array($role, ['outline', 'outline-variant'], true) => "{$dead} — a line is `<x-divider>` or `<x-surface outlined>`, not a border utility",
preg_match('/^(?:border(?:-(?:[trblxyse]|bs|be))?|divide)$/', $utility) === 1 && in_array($role, ['outline', 'outline-variant'], true) => "{$dead} — a line is `<x-divider>` or `<x-surface outlined>`, not a border utility",
$utility === 'bg' && in_array($role === 'background' ? 'surface' : $role, self::SURFACE_LEVELS, true) => "{$dead} — use `<x-surface level=\"".($role === 'background' ? 'surface' : $role).'">`',
default => "{$dead} — use `var(--md-sys-color-{$role})` in your own CSS",
};
+46 -3
View File
@@ -1,6 +1,7 @@
<?php
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
@@ -77,16 +78,58 @@ function boostTextAsCode(string $text): array
return ['view' => implode("\n", $view), 'css' => implode("\n", $css)];
}
/**
* The hyphenated names the texts also write alone in a span for something that is not a class:
* every property the package's stylesheets declare (`border-color`), every component prop
* (`placeholder-value`) and every colour role (`outline-variant`). Each is shaped like a colour
* utility on a name the guard does not know, which is what an application's own theme colour
* (`bg-brand`) looks like.
*
* @return array<string, true>
*/
function boostNamesThatAreNotClasses(): array
{
static $names = null;
if ($names !== null) {
return $names;
}
$names = [];
$root = __DIR__.'/../../resources';
foreach (File::allFiles("{$root}/css") as $file) {
preg_match_all('/(?<![\w-])([a-z][a-z-]*-[a-z-]+)\s*:\s*[^;{}]*[;}]|--md-sys-color-([a-z-]+)/', $file->getContents(), $matches);
foreach ([...$matches[1], ...$matches[2]] as $name) {
$names[$name] = true;
}
}
foreach (File::files("{$root}/views/components") as $file) {
if (preg_match('/@props\(\[(.*?)\]\)/s', $file->getContents(), $props) === 1) {
preg_match_all("/^\s*'(\w+)'/m", $props[1], $keys);
foreach ($keys[1] as $key) {
$names[Str::kebab($key)] = true;
}
}
}
return $names;
}
/**
* Whether an inline span reads as a class list. Not: a CSS declaration (`font: var(…)`), a
* command (`php artisan …`), or a single bare word, which is as often a role, prop or value
* (`outline`, `link`, `hidden`) as a utility.
* command (`php artisan …`), a single bare word, which is as often a role, prop or value
* (`outline`, `link`, `hidden`) as a utility, or a single property, prop or role name
* (`border-color`, `placeholder-value`, `outline-variant`).
*/
function boostSpanIsClassList(string $span): bool
{
$tokens = preg_split('/\s+/', trim($span));
if (count($tokens) === 1 && preg_match('/[-:]/', $tokens[0]) !== 1) {
if (count($tokens) === 1 && (preg_match('/[-:]/', $tokens[0]) !== 1 || isset(boostNamesThatAreNotClasses()[$tokens[0]]))) {
return false;
}
+51
View File
@@ -285,6 +285,57 @@ it('exempts a class the application\'s own CSS declares, even when it is Tailwin
]);
});
it('reports a colour utility on the application\'s own theme colour, and a Tailwind utility of the same prefix as its own family', function () {
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/theme-colours.blade.php'))->violations());
$theme = fn (string $token, string $name, string $use = '`var(--…)`'): string => "Tailwind colour utility `{$token}` compiles to nothing — `{$name}` is neither an M3 role nor a Tailwind colour, so it named a colour of the application's own theme: use {$use} in your own CSS";
$tint = '`color-mix(in srgb, var(--…) <n>%, transparent)`';
$line = 'a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`';
$outline = "M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS";
expect($violations)->toBe([
'tailwind/theme-colours.blade.php:1 '.$theme('bg-off-plan', 'off-plan'),
'tailwind/theme-colours.blade.php:1 '.$theme('from-brand', 'brand'),
'tailwind/theme-colours.blade.php:1 '.$theme('text-brand-ink', 'brand-ink'),
'tailwind/theme-colours.blade.php:1 '.$theme('text-sport-run', 'sport-run'),
'tailwind/theme-colours.blade.php:1 '.$theme('to-brand-end', 'brand-end'),
'tailwind/theme-colours.blade.php:2 '.$theme('bg-route-reference/8', 'route-reference', $tint),
'tailwind/theme-colours.blade.php:2 '.$theme('border-l-zone-4', 'zone-4'),
'tailwind/theme-colours.blade.php:2 '.$theme('border-route-reference', 'route-reference'),
'tailwind/theme-colours.blade.php:2 '.$theme('ring-offset-brand/[0.4]', 'brand', $tint),
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-balance` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-clip` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-ellipsis` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-pretty` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-wrap` compiles to nothing — write the rule in your own CSS',
"tailwind/theme-colours.blade.php:3 Tailwind's `text-nowrap` compiles to nothing — use `md-nowrap` (text.css)",
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-dashed` compiles to nothing — {$line}",
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-dotted` compiles to nothing — {$line}",
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-none` compiles to nothing — {$line}",
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-solid` compiles to nothing — {$line}",
'tailwind/theme-colours.blade.php:4 Tailwind table utility `border-collapse` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:4 Tailwind table utility `border-separate` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:5 Tailwind gradient utility `from-0%` compiles to nothing — write the gradient in your own CSS',
'tailwind/theme-colours.blade.php:5 Tailwind gradient utility `via-none` compiles to nothing — write the gradient in your own CSS',
"tailwind/theme-colours.blade.php:5 Tailwind outline utility `outline-dashed` compiles to nothing — {$outline}",
"tailwind/theme-colours.blade.php:5 Tailwind outline utility `outline-none` compiles to nothing — {$outline}",
"tailwind/theme-colours.blade.php:5 Tailwind outline utility `ring-inset` compiles to nothing — {$outline}",
'tailwind/theme-colours.blade.php:5 Tailwind shadow utility `shadow-none` compiles to nothing — use `var(--md-sys-elevation-*)` in your own CSS',
'tailwind/theme-colours.blade.php:5 Tailwind text utility `decoration-wavy` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind SVG utility `fill-none` compiles to nothing — write `fill` or `stroke` in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-center` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-clip-text` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-cover` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-fixed` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-no-repeat` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-top-left` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind colour utility `text-inherit` compiles to nothing — write `inherit` in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind interactivity utility `accent-auto` compiles to nothing — write the rule in your own CSS',
'tailwind/theme-colours.blade.php:6 Tailwind text utility `text-shadow-sm` compiles to nothing — write the rule in your own CSS',
"tailwind/theme-colours.blade.php:7 Tailwind border utility `border-bs-2` compiles to nothing — {$line}",
'tailwind/theme-colours.blade.php:7 Tailwind palette colour `bg-mauve-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
]);
});
it('reports an arbitrary [] value once, whatever utility it modifies, and strips a trailing !', function () {
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/arbitrary.blade.php'))->violations());
@@ -1,3 +1,7 @@
.gap-4 {
gap: 1rem;
}
.bg-brand {
background: var(--brand);
}
@@ -1 +1 @@
<div class="gap-4 mt-2"></div>
<div class="gap-4 mt-2 bg-brand"></div>
@@ -0,0 +1,8 @@
<div class="bg-off-plan text-sport-run from-brand to-brand-end text-brand-ink">
<span class="border-route-reference bg-route-reference/8 border-l-zone-4 ring-offset-brand/[0.4]"></span>
<p class="text-balance text-pretty text-wrap text-nowrap text-ellipsis text-clip"></p>
<table class="border-collapse border-separate border-solid border-dashed border-dotted border-none"></table>
<span class="shadow-none outline-none outline-dashed ring-inset decoration-wavy from-0% via-none"></span>
<span class="bg-clip-text bg-fixed bg-cover bg-center bg-no-repeat bg-top-left text-inherit text-shadow-sm fill-none accent-auto"></span>
<span class="bg-mauve-500 border-bs-2"></span>
</div>