diff --git a/README.md b/README.md index d075c4f6..a5f2f2bb 100644 --- a/README.md +++ b/README.md @@ -154,8 +154,6 @@ The guard fails on Tailwind palette colours, unknown Material Symbol names and B `missingStylesheets($cssEntry)` checks the CSS entry's `@import` graph (followed through every package file's own imports) against the package tags a view actually renders — unprefixed, under the configured prefix, or `` — and `->links()`; each missing one names the exact `@import` line to add, and a tag the application shadows with its own component of the same name is reported instead. It also turns on a check of the application's own CSS (the entry and what it imports outside the package, plus any `.css` file among the scanned paths), which reports a literal colour, radius, shadow, font size, weight, line height, letter spacing, easing, duration or off-scale media query with its token or breakpoint — a value inside `var(--md-sys-…)` or `calc()` is always fine, and the generated `material-scheme.css` is skipped. -Two checks are opt-in, and matter mainly to an application still migrating off Tailwind: `forbidAbsolutes()` also fails on `bg-white` and `text-black` (M3's white is `surface-container-lowest`), and `forbidOpacityInk()` on opacity used as emphasis (`text-on-surface/60` → `text-on-surface-variant` or `text-outline`). M3 reserves 38 % on content and 12 % on a container for the disabled state, which is what the package's own components use them for, so neither is on by default. - ## AI agents With [Laravel Boost](https://github.com/laravel/boost), `php artisan boost:install` (or `boost:update --discover`) picks up the package's two guidelines — the library's own, and `material-3`, a page of M3's rules an agent reads in every session — and two skills: `livewire-material-development` (every component, prop, slot and trap) and `material-3-design` (M3's colour roles, surfaces, elevation, shape, type, motion, states, window size classes and accessibility, with the library's utility beside each M3 name and Google's source page for each chapter). diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 4e885227..17e26c82 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -1113,8 +1113,6 @@ it('uses only what compiles', function () { expect(DesignGuard::scan([resource_path('views'), resource_path('js'), app_path()]) ->missingStylesheets(resource_path('css/app.css')) ->forbidColours(['tertiary']) // roles this application's rules leave out - ->forbidAbsolutes() // opt-in, migration only: `bg-white`, `text-black` - ->forbidOpacityInk() // opt-in, migration only: `text-on-surface/60` ->violations())->toBe([]); }); ``` @@ -1132,10 +1130,12 @@ It also fails on every Tailwind utility or variant, which compile to nothing in | `hidden` | a layout component's `hide-below`/`hide-from` prop, or `[hidden]` | | `block`, `inline-block`, `invisible` | a plain `display` rule in your own CSS | | `text-center`, `truncate`, `sr-only`, `whitespace-nowrap`, `line-clamp-2`, `tabular-nums` | `md-text-center`, `md-truncate`, `md-visually-hidden`, `md-nowrap`, `md-line-clamp-2`, `md-tabular` | -| `text-on-surface-variant`, `text-primary` … | `md-ink-variant`, `md-ink-primary` … | +| `text-on-surface-variant`, `text-primary`, `text-meta` … | `md-ink-variant`, `md-ink-primary` … | | `border-outline-variant` | `` or `` | | `bg-surface-container` | `` | | `bg-primary`, `border-error` … | `var(--md-sys-color-*)` | +| `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-t-2xl`, `rounded-full` | `var(--md-sys-shape-corner-lg)`, or `` | | `shadow-sm`, `shadow-md` … `shadow-2xl` | `var(--md-sys-elevation-1)` … `var(--md-sys-elevation-5)` | | `text-sm`, `leading-6`, `tracking-wide` | one of the `md-type-*` classes, which set size, line height and tracking together | @@ -1146,8 +1146,6 @@ It also fails on every Tailwind utility or variant, which compile to nothing in `missingStylesheets($cssEntry)` follows the entry's `@import` graph through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too) and checks it against the package tags a scanned view renders (plain, prefixed or ``) and `->links()`; a missing one names the exact `@import` line to add, and a tag the application shadows with its own component of the same name is reported instead — the application's component wins in Blade. It also turns on a check of the application's own CSS (the entry and what it imports outside the package, plus any `.css` file among the scanned paths, `material-scheme.css` excluded): a literal colour, radius, shadow, font size, weight, line height, letter spacing, easing, duration or a media query off M3's 600/840/1200/1600px scale, each with its token or breakpoint — a value inside `var(--md-sys-…)` or `calc()` is always fine. -`forbidAbsolutes()` adds `white` and `black` (M3's white is `surface-container-lowest`, its ink an `on-` role) and `forbidOpacityInk()` adds opacity as emphasis (`text-on-surface/60` → `text-on-surface-variant` or `text-outline`). Both are off by default and matter mainly to an application still migrating off Tailwind: M3 reserves 38 % on content and 12 % on a container for the disabled state, and the package's own components are written with those two opacities. - ## Conventions - Components are anonymous Blade components: `` without a prefix, or `` when `config('livewire-material.prefix')` is set; `` always works. diff --git a/src/Testing/DesignGuard.php b/src/Testing/DesignGuard.php index fb8d2179..d5536e0b 100644 --- a/src/Testing/DesignGuard.php +++ b/src/Testing/DesignGuard.php @@ -35,13 +35,10 @@ use Symfony\Component\Finder\Finder; * `var(--md-sys-…)` or `calc()` is never flagged, whatever it contains. * (iv) Tailwind palette colours, icon names that are not Material Symbols, and Blade * directives written inside a component tag (where they do not compile) — plus whatever - * an application bans on top with `forbidColours()`, `forbidAbsolutes()`, - * `forbidOpacityInk()` and `forbid()`. These, and the breakpoint, - * scale and colour-value checks, read a line at a time, so a class assembled at runtime - * (`'text-'.$tone`) or hidden in a comment stays invisible — the same reason to write - * class names out whole. `forbidAbsolutes()` and `forbidOpacityInk()` matter mainly to an - * application still migrating off Tailwind, where the package's own opacity-based - * disabled state is still the only legitimate use of either. + * an application bans on top with `forbidColours()` and `forbid()`. These, and the + * breakpoint, scale and colour-value checks, read a line at a time, so a class assembled + * at runtime (`'text-'.$tone`) or hidden in a comment stays invisible — the same reason + * to write class names out whole. * * expect(DesignGuard::scan([resource_path('views'), resource_path('js'), app_path()]) * ->missingStylesheets(resource_path('css/app.css')) @@ -69,12 +66,6 @@ class DesignGuard /** A colour written as a value: an arbitrary hex, function or mix instead of a role. */ protected const string ARBITRARY_COLOUR = '/(? 'md-tabular', ]; - /** An ink role Tailwind's `text-*` utility named, and the `md-ink-*` class that replaces it. */ + /** + * A colour utility written on an M3 role, a 1.x ink name, or white/black/current, with an + * 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 = '/^(?bg|text|border(?:-[trblxyse])?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-' + .'(?(?: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)' + .'(?:\/(?\d{1,3}|\[[^\]]*\]))?$/'; + + /** 1.x's semantic ink names, and the M3 role each resolved to (UPGRADE.md § 4). */ + protected const array INK_ALIASES = [ + 'body' => 'on-surface-variant', + 'meta' => 'on-surface-variant', + 'quiet' => 'outline', + 'structure' => 'outline-variant', + 'chrome' => 'outline-variant', + 'divider' => 'outline-variant', + ]; + + /** An ink role, and the `md-ink-*` class (text.css) that sets it on plain text. */ protected const array INK_ROLE = [ 'on-surface' => 'md-ink', 'on-surface-variant' => 'md-ink-variant', @@ -169,6 +182,12 @@ class DesignGuard 'inverse-on-surface' => 'md-ink-inverse', ]; + /** The roles `` takes. */ + protected const array SURFACE_LEVELS = [ + 'surface', 'surface-dim', 'surface-bright', 'surface-container-lowest', 'surface-container-low', + 'surface-container', 'surface-container-high', 'surface-container-highest', + ]; + /** The named colours a browser understands, other than `transparent` and `currentColor`. */ protected const string CSS_NAMED_COLOURS = 'aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|rebeccapurple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen'; @@ -200,10 +219,6 @@ class DesignGuard /** @var list */ protected array $forbiddenColours = []; - protected bool $forbiddenAbsolutes = false; - - protected bool $forbiddenOpacityInk = false; - protected ?string $cssEntry = null; /** @var array|null */ @@ -235,35 +250,6 @@ class DesignGuard return $this; } - /** - * Also ban the two absolutes. M3 paints with roles only: the white of a light page is - * `surface-container-lowest`, the black of a dark one `surface-dim`, and the ink on a filled - * button is its `on-` role. Off by default, because a logo, a scrim or a print stylesheet - * sometimes does mean the absolute — and because, until an application finishes leaving - * Tailwind, `bg-white`/`text-black` may still be sitting in an untouched view. - */ - public function forbidAbsolutes(): static - { - $this->forbiddenAbsolutes = true; - - return $this; - } - - /** - * Also ban opacity as emphasis on ink: M3 says secondary text is a role — on-surface-variant - * for supporting text, outline for the quietest — never a faded on-surface. It reserves two - * opacities for the disabled state, 38 % on content and 12 % on a container, and the - * package's own disabled styles are written with those two, which is why this is off by - * default; an application that draws its disabled states from the components does not need - * either opacity of its own. - */ - public function forbidOpacityInk(): static - { - $this->forbiddenOpacityInk = true; - - return $this; - } - /** * Any further pattern, matched line by line. */ @@ -491,38 +477,15 @@ class DesignGuard } /** - * Colours written as a value rather than a role: an arbitrary one always, the two absolutes - * and opacity on ink when the application asks for them. + * Colours written as a value rather than a role: an arbitrary hex, function or mix. * * @return list */ protected function colourValues(string $text): array { - $found = []; - preg_match_all(self::ARBITRARY_COLOUR, $text, $matches); - foreach ($matches[0] as $class) { - $found[] = "arbitrary colour `{$class}`, use an M3 role"; - } - - if ($this->forbiddenAbsolutes) { - preg_match_all(self::ABSOLUTE_COLOUR, $text, $matches); - - foreach ($matches[0] as $class) { - $found[] = "absolute colour `{$class}`, use an M3 role"; - } - } - - if ($this->forbiddenOpacityInk) { - preg_match_all(self::OPACITY_INK, $text, $matches); - - foreach ($matches[0] as $class) { - $found[] = "opacity on ink `{$class}`, use a role (`text-on-surface-variant`, `text-outline`)"; - } - } - - return $found; + return array_map(fn (string $class): string => "arbitrary colour `{$class}`, use an M3 role", $matches[0]); } /** @@ -585,23 +548,35 @@ class DesignGuard return "Tailwind's `{$token}` compiles to nothing — use `".self::TEXT_LAYOUT_UTILITY[$token].'` (text.css)'; } - if (preg_match('/^text-(?on-surface(?:-variant)?|outline|primary|error|success|warning|info|inverse-on-surface)$/', $token, $m) === 1) { - return "Tailwind ink role `{$token}` compiles to nothing — use `".self::INK_ROLE[$m['role']].'` (text.css)'; + return $this->colourUtilityHint($token); + } + + /** + * A colour utility's 2.0.0 replacement (see `COLOUR_UTILITY`), or null for anything else: an + * ink on plain text is its `md-ink-*` class, a line `` or ``, a + * tonal background ``, white and black a role, opacity a role or the state + * layer, and every other role its `var(--md-sys-color-*)` in the application's own CSS. + */ + protected function colourUtilityHint(string $token): ?string + { + if (preg_match(self::COLOUR_UTILITY, $token, $m, PREG_UNMATCHED_AS_NULL) !== 1) { + return null; } - if (preg_match('/^(?:border(?:-[trblxyse])?|divide|ring)-outline(?:-variant)?$/', $token) === 1) { - return "Tailwind line role `{$token}` compiles to nothing — use `` or `` for a line, not a border utility"; - } + $utility = $m['utility']; + $role = self::INK_ALIASES[$m['role']] ?? $m['role']; + $dead = "Tailwind colour utility `{$token}` compiles to nothing"; - if (preg_match('/^bg-(?surface(?:-dim|-bright|-container(?:-lowest|-low|-high|-highest)?)?)$/', $token, $m) === 1) { - return "Tailwind surface role `{$token}` compiles to nothing — use ``"; - } - - if (preg_match('/^(?:bg|border(?:-[trblxyse])?|ring|fill|stroke|divide|decoration|accent|caret|outline)-(?:on-)?(?:primary|secondary|tertiary|error|success|warning|info)(?:-container)?$/', $token) === 1) { - return "Tailwind role utility `{$token}` compiles to nothing — use `var(--md-sys-color-*)` in your own CSS"; - } - - return null; + return match (true) { + in_array($role, ['white', 'black'], true) => "{$dead} — M3 paints with roles, never white or black: `` 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', + $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}) %, 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 `` or ``, not a border utility", + $utility === 'bg' && in_array($role === 'background' ? 'surface' : $role, self::SURFACE_LEVELS, true) => "{$dead} — use ``', + default => "{$dead} — use `var(--md-sys-color-{$role})` in your own CSS", + }; } /** @@ -617,7 +592,7 @@ class DesignGuard $violations = []; foreach ($this->applicationStylesheets() as $file) { - $css = $this->withoutTokenFunctions($this->withoutCssComments((string) file_get_contents($file))); + $css = $this->withoutTokenFunctions($this->maskedCss((string) file_get_contents($file))); $where = $this->relative($file); foreach ([ @@ -633,25 +608,43 @@ class DesignGuard } /** - * Every literal colour in `$css`: a hex code, a colour function, or a named colour other than - * `transparent`/`currentColor` written where a value is expected (after `:`, `,` or `(`) — - * which is what keeps a selector like `.red-banner` from matching. + * Every literal colour in a declaration's value: a hex code, a colour function, or a named + * colour other than `transparent`/`currentColor`, anywhere in the value (`border: 1px solid + * white` as much as `color: white`). Only values are read, so a selector like `.red-banner` or + * an id like `#add` never matches. * * @return list */ protected function literalColours(string $css): array { - $pattern = '/(?declarations($css) as [, $value, $offset]) { + preg_match_all('/(? [ - substr_count(substr($css, 0, $match[1]), "\n") + 1, - "literal colour `{$match[0]}` — use `var(--md-sys-color-*)`", - ], - $matches[0], - ); + foreach ($matches[0] as [$colour, $at]) { + $found[] = [ + substr_count(substr($css, 0, $offset + $at), "\n") + 1, + "literal colour `{$colour}` — use `var(--md-sys-color-*)`", + ]; + } + } + + return $found; + } + + /** + * Every declaration in `$css` — a property, custom properties included, and its value up to + * the `;` or `}` that ends it — with the value's byte offset. A selector (`a:hover {`) or an + * at-rule condition (`@media (prefers-color-scheme: dark) {`) ends in `{` and is never one. + * + * @return list + */ + protected function declarations(string $css): array + { + preg_match_all('/(?--[\w-]+|-?[a-zA-Z][\w-]*)\s*:\s*(?[^;{}]*)(?=[;}])/', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); + + return array_map(fn (array $match): array => [strtolower($match['property'][0]), $match['value'][0], $match['value'][1]], $matches); } /** @@ -769,13 +762,17 @@ class DesignGuard } /** - * `$css` with every `/* … *\/` comment blanked out, line breaks kept. + * `$css` with every `/* … *\/` comment, the inside of every quoted string and of every + * unquoted `url(…)` blanked out, line breaks kept — so neither a class check nor a value check + * reads `content: ".flex"`, a font name or a data URI's `fill='white'`. */ - protected function withoutCssComments(string $css): string + protected function maskedCss(string $css): string { return (string) preg_replace_callback( - '/\/\*.*?\*\//s', - fn (array $match): string => str_repeat("\n", substr_count($match[0], "\n")), + '/\/\*.*?\*\/|"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\'|(?<=url\()[^)\'"]*(?=\))/is', + fn (array $match): string => str_starts_with($match[0], '/*') + ? str_repeat("\n", substr_count($match[0], "\n")) + : (string) preg_replace('/[^\n]/', ' ', $match[0]), $css, ); } @@ -1097,7 +1094,7 @@ class DesignGuard $classes = []; foreach ($this->applicationStylesheets() as $file) { - $css = $this->withoutCssComments((string) file_get_contents($file)); + $css = $this->maskedCss((string) file_get_contents($file)); preg_match_all('/(?toBe([ 'app/Status.php:14 colour the theme does not declare `text-red-500`', "views/page.blade.php:2 Tailwind spacing utility `p-4` compiles to nothing — use a layout component's `gap`/`padding` prop (``, ``), or `var(--md-sys-measurement-space*)` in your own CSS", + 'views/page.blade.php:6 Tailwind colour utility `text-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS', 'views/page.blade.php:8 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale; write the literal length in your own CSS, or use ``/`` where it fits', 'views/page.blade.php:3 unknown Material Symbol `o-home`', 'views/page.blade.php:4 unknown Material Symbol `not_a_symbol`', @@ -150,6 +151,12 @@ it('sends an easing and a duration to the motion tokens', function () { it('finds a colour written as a value', function () { expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->violations()))->toBe([ + 'colour/badge.blade.php:3 Tailwind colour utility `bg-white` compiles to nothing — M3 paints with roles, never white or black: `` 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', + 'colour/badge.blade.php:3 Tailwind colour utility `text-black` compiles to nothing — M3 paints with roles, never white or black: `` 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', + 'colour/badge.blade.php:3 Tailwind colour utility `border-white` compiles to nothing — M3 paints with roles, never white or black: `` 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', + "colour/badge.blade.php:4 Tailwind colour utility `text-on-surface/60` compiles to nothing — M3's quieter text is a role, not a faded one: `md-ink-variant` or `md-ink-quiet` (text.css)", + "colour/badge.blade.php:4 Tailwind colour utility `bg-on-surface/12` compiles to nothing — M3's hover, focus and press overlays are `md-state-layer`; any other tint is `color-mix(in srgb, var(--md-sys-color-on-surface) %, transparent)` in your own CSS", + "colour/badge.blade.php:4 Tailwind colour utility `border-outline/38` compiles to nothing — M3's hover, focus and press overlays are `md-state-layer`; any other tint is `color-mix(in srgb, var(--md-sys-color-outline) %, transparent)` in your own CSS", 'colour/badge.blade.php:1 arbitrary colour `bg-[#1d7afc]`, use an M3 role', 'colour/badge.blade.php:1 arbitrary colour `text-[rgb(0_0_0)]`, use an M3 role', 'colour/badge.blade.php:1 arbitrary colour `border-[hsl(210_80%_50%)]`, use an M3 role', @@ -158,14 +165,27 @@ it('finds a colour written as a value', function () { ]); }); +it('sends 1.x ink names, the state-layer opacities and every other role to their 2.0.0 replacement', function () { + $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/inks.blade.php'))->violations()); + + expect($violations)->toBe([ + 'tailwind/inks.blade.php:1 Tailwind colour utility `text-meta` compiles to nothing — use `md-ink-variant` (text.css)', + 'tailwind/inks.blade.php:1 Tailwind colour utility `text-quiet` compiles to nothing — use `md-ink-quiet` (text.css)', + 'tailwind/inks.blade.php:1 Tailwind colour utility `border-divider` compiles to nothing — a line is `` or ``, not a border utility', + 'tailwind/inks.blade.php:2 Tailwind colour utility `text-on-primary-container` compiles to nothing — use `var(--md-sys-color-on-primary-container)` in your own CSS', + 'tailwind/inks.blade.php:2 Tailwind colour utility `bg-background` compiles to nothing — use ``', + 'tailwind/inks.blade.php:2 Tailwind colour utility `text-current` compiles to nothing — write `currentColor` in your own CSS', + ]); +}); + it('names the md-ink-*, divider/surface or token replacement for a dead M3 role utility', function () { $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/roles.blade.php'))->violations()); expect($violations)->toBe([ - 'tailwind/roles.blade.php:1 Tailwind ink role `text-on-surface-variant` compiles to nothing — use `md-ink-variant` (text.css)', - 'tailwind/roles.blade.php:1 Tailwind line role `border-outline-variant` compiles to nothing — use `` or `` for a line, not a border utility', - 'tailwind/roles.blade.php:2 Tailwind surface role `bg-surface-container` compiles to nothing — use ``', - 'tailwind/roles.blade.php:2 Tailwind role utility `bg-primary` compiles to nothing — use `var(--md-sys-color-*)` in your own CSS', + 'tailwind/roles.blade.php:1 Tailwind colour utility `text-on-surface-variant` compiles to nothing — use `md-ink-variant` (text.css)', + 'tailwind/roles.blade.php:1 Tailwind colour utility `border-outline-variant` compiles to nothing — a line is `` or ``, not a border utility', + 'tailwind/roles.blade.php:2 Tailwind colour utility `bg-surface-container` compiles to nothing — use ``', + 'tailwind/roles.blade.php:2 Tailwind colour utility `bg-primary` compiles to nothing — use `var(--md-sys-color-primary)` in your own CSS', ]); }); @@ -258,38 +278,6 @@ it('reports an arbitrary [] value once, whatever utility it modifies, and strips ]); }); -it('bans the two absolutes when asked', function () { - $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->forbidAbsolutes()->violations()); - - expect($violations)->toContain( - 'colour/badge.blade.php:3 absolute colour `bg-white`, use an M3 role', - 'colour/badge.blade.php:3 absolute colour `text-black`, use an M3 role', - 'colour/badge.blade.php:3 absolute colour `border-white`, use an M3 role', - ); -}); - -it('leaves the two absolutes alone by default', function () { - $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->violations()); - - expect($violations)->not->toContain('colour/badge.blade.php:3 absolute colour `bg-white`, use an M3 role'); -}); - -it('bans opacity as emphasis on ink when asked', function () { - $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->forbidOpacityInk()->violations()); - - expect($violations)->toContain( - 'colour/badge.blade.php:4 opacity on ink `text-on-surface/60`, use a role (`text-on-surface-variant`, `text-outline`)', - 'colour/badge.blade.php:4 opacity on ink `bg-on-surface/12`, use a role (`text-on-surface-variant`, `text-outline`)', - 'colour/badge.blade.php:4 opacity on ink `border-outline/38`, use a role (`text-on-surface-variant`, `text-outline`)', - ); -}); - -it('leaves opacity on ink alone by default, as the package\'s own disabled styles use it', function () { - $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->violations()); - - expect($violations)->not->toContain('colour/badge.blade.php:4 opacity on ink `text-on-surface/60`, use a role (`text-on-surface-variant`, `text-outline`)'); -}); - it('bans the roles an application leaves out', function () { $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/views')) ->forbidColours(['tertiary']) @@ -414,6 +402,8 @@ it('finds every literal design value and off-scale media query in the applicatio 'app-css/app.css:3 literal colour `#ff0000` — use `var(--md-sys-color-*)`', 'app-css/app.css:4 literal colour `rgb(0, 0, 0)` — use `var(--md-sys-color-*)`', 'app-css/app.css:6 literal colour `rgba(0, 0, 0, 0.2)` — use `var(--md-sys-color-*)`', + 'app-css/app.css:24 literal colour `white` — use `var(--md-sys-color-*)`', + 'app-css/app.css:25 literal colour `#fff` — use `var(--md-sys-color-*)`', 'app-css/app.css:5 literal radius `border-radius: 12px` — use `var(--md-sys-shape-corner-*)`', 'app-css/app.css:6 literal shadow `box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2)` — use `var(--md-sys-elevation-*)`', 'app-css/app.css:7 literal font size `font-size: 14px` — use `var(--md-sys-typescale-*)`, which sets size, line height and weight together', @@ -422,7 +412,7 @@ it('finds every literal design value and off-scale media query in the applicatio 'app-css/app.css:10 literal letter spacing `letter-spacing: 0.02em` — use `var(--md-sys-typescale-*-tracking)`', 'app-css/app.css:11 literal easing in `transition: opacity 200ms ease-in-out` — use `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)`, paired with its `-duration`', 'app-css/app.css:11 literal duration in `transition: opacity 200ms ease-in-out` — use `var(--md-sys-motion-…-duration)`, paired with its easing', - "app-css/app.css:21 media query width `700px` is not one of M3's breakpoints — use 600, 840, 1200 or 1600px (medium, expanded, large, extra-large)", + "app-css/app.css:34 media query width `700px` is not one of M3's breakpoints — use 600, 840, 1200 or 1600px (medium, expanded, large, extra-large)", ]); }); diff --git a/tests/Fixtures/design-guard/app-css/app.css b/tests/Fixtures/design-guard/app-css/app.css index a832ac0b..2fd68024 100644 --- a/tests/Fixtures/design-guard/app-css/app.css +++ b/tests/Fixtures/design-guard/app-css/app.css @@ -18,6 +18,19 @@ border: 0; } +/* white in a comment is ignored, and so is #fff */ +.quiet::before { + content: "white"; + border: 1px solid white; + outline-color: #fff; + background: url(data:image/svg+xml;utf8,); +} + +#add-button, .red-banner { + color: transparent; + fill: currentColor; +} + @media (min-width: 700px) { .card { display: flex; diff --git a/tests/Fixtures/design-guard/tailwind/inks.blade.php b/tests/Fixtures/design-guard/tailwind/inks.blade.php new file mode 100644 index 00000000..630057f4 --- /dev/null +++ b/tests/Fixtures/design-guard/tailwind/inks.blade.php @@ -0,0 +1,2 @@ +

+