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:
co-authored by
Claude Opus 5
parent
394c9326ac
commit
613ac20013
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user