Take Tailwind out of the package, and its detection out of the guard
tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
Tailwind left the stack in 2.0.0, but the package still carried about 330 mentions of it. What the guard's Tailwind detection protected — a class that compiles to nothing — is now protected by a check that does not care where a dead class came from. DesignGuard: about 500 lines of Tailwind tables, scales, palettes and "2.0.0 replacement" hints give way to one check — a class a view or PHP file writes that neither the application's stylesheets nor the package's own declare. It catches a utility of any framework, a typo and a class whose rules were deleted alike, so it also found two classes ReStride draws nothing with. A stylesheet has to be in reach for it: the `.css` files among the scanned paths, or what the `missingStylesheets()` entry imports. The class reader no longer mistakes an array index for a class list (`$block['base']`), and it reads the array a class helper is given, where it read nothing before. The package's own three Tailwind self-guards go with it. Only their one unique check stays, as a test of its own: every `matchMedia` width in resources/js is an M3 breakpoint. The pagination views are `material.blade.php` and `simple-material.blade.php`; only Laravel's and Livewire's default theme names ever made them `tailwind`. The provider sets `Paginator`'s default views and switches `livewire.pagination_theme` to `material` when it is still Livewire's own default, so no application can forget the config; a theme an application chose, and a component's own `$paginationTheme` or `paginationView()`, still win. The rest is prose: the layer-order guidance for an application that still builds Tailwind, the Tailwind wording in the README, the Boost guidelines and the development skill, and about 25 "this used to be a Tailwind utility" comments, along with every "plan step NN" pointer into a gitignored folder. The reset keeps its credit, and NOTICE now carries it too. Feature suite 1159 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
247c596c3a
commit
fb7007c976
@@ -52,6 +52,10 @@ Spring motion constants (resources/css/tokens/motion.css)
|
|||||||
From androidx Compose Material 3, tokens/ExpressiveMotionTokens.kt.
|
From androidx Compose Material 3, tokens/ExpressiveMotionTokens.kt.
|
||||||
Copyright The Android Open Source Project. Apache License 2.0.
|
Copyright The Android Open Source Project. Apache License 2.0.
|
||||||
|
|
||||||
|
CSS reset (resources/css/foundation/reset.css)
|
||||||
|
Derived rule for rule from Tailwind CSS 4's preflight, after modern-normalize.
|
||||||
|
Copyright Tailwind Labs. MIT License.
|
||||||
|
|
||||||
Material Design 3 documentation (resources/boost/guidelines/material-3.blade.php,
|
Material Design 3 documentation (resources/boost/guidelines/material-3.blade.php,
|
||||||
resources/boost/skills/material-3-design/SKILL.md, docs/reference/m3)
|
resources/boost/skills/material-3-design/SKILL.md, docs/reference/m3)
|
||||||
Rules, tables and wording condensed from https://m3.material.io (Foundations, Styles,
|
Rules, tables and wording condensed from https://m3.material.io (Foundations, Styles,
|
||||||
@@ -62,3 +66,4 @@ resources/boost/skills/material-3-design/SKILL.md, docs/reference/m3)
|
|||||||
Copyright The Android Open Source Project (Apache License 2.0).
|
Copyright The Android Open Source Project (Apache License 2.0).
|
||||||
|
|
||||||
A copy of the Apache License 2.0 is available at https://www.apache.org/licenses/LICENSE-2.0.
|
A copy of the Apache License 2.0 is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
A copy of the MIT License is available at https://opensource.org/licenses/MIT.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ composer require nonameweb/livewire-material
|
|||||||
|
|
||||||
The application's build imports from `vendor/`, so Composer packages must be installed before `npm run build` — in a Dockerfile, copy `composer.json`, run `composer install`, then build the assets.
|
The application's build imports from `vendor/`, so Composer packages must be installed before `npm run build` — in a Dockerfile, copy `composer.json`, run `composer install`, then build the assets.
|
||||||
|
|
||||||
The package's stylesheets are plain CSS, no build step of its own and no Tailwind anywhere in the stack. `all.css` brings the foundation and every component; an application can instead import `foundation.css` first and then the stylesheet of each component its views render (`resources/css/components/button.css`, `resources/css/layout/scaffold.css`, …), since each imports the stylesheets of the components it draws and Vite keeps a file several of them import once. The error pages need no import: they take the foundation, scheme and font from the application's build and inline their own layout's rules beside it.
|
The package's stylesheets are plain CSS, no build step of its own. `all.css` brings the foundation and every component; an application can instead import `foundation.css` first and then the stylesheet of each component its views render (`resources/css/components/button.css`, `resources/css/layout/scaffold.css`, …), since each imports the stylesheets of the components it draws and Vite keeps a file several of them import once. The error pages need no import: they take the foundation, scheme and font from the application's build and inline their own layout's rules beside it.
|
||||||
|
|
||||||
```css
|
```css
|
||||||
/* resources/css/app.css */
|
/* resources/css/app.css */
|
||||||
@@ -94,7 +94,7 @@ php artisan vendor:publish --tag=livewire-material-config
|
|||||||
- `motion.scheme` — M3's motion scheme: `expressive` (default) or `standard`, the restrained springs, written to `<html data-motion>`.
|
- `motion.scheme` — M3's motion scheme: `expressive` (default) or `standard`, the restrained springs, written to `<html data-motion>`.
|
||||||
- `profiles`, `profile` — colour profiles and the default one (see Colour profiles).
|
- `profiles`, `profile` — colour profiles and the default one (see Colour profiles).
|
||||||
- `fields.variant` — text fields `outlined` (default) or `filled`.
|
- `fields.variant` — text fields `outlined` (default) or `filled`.
|
||||||
- `pagination` — draw Laravel's and Livewire's paginators in M3 (default `true`).
|
- `pagination` — draw Laravel's and Livewire's paginators in M3 (default `true`); the provider also sets `livewire.pagination_theme` to `material` whenever it still reads as Livewire's own default (`tailwind`), unless an application's own theme, or a component's `$paginationTheme` property or `paginationView()` method, already chose one.
|
||||||
- `showcase.enabled`, `showcase.path`, `showcase.middleware`, `showcase.vite`.
|
- `showcase.enabled`, `showcase.path`, `showcase.middleware`, `showcase.vite`.
|
||||||
- `node` — the Node binary for `material:scheme`.
|
- `node` — the Node binary for `material:scheme`.
|
||||||
|
|
||||||
@@ -150,9 +150,9 @@ 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, a colour of the application's own Tailwind theme (`bg-brand`) included — none compiles, since the application carries no Tailwind — each with its replacement: 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. Markdown mail components (under `mail.markdown.paths`, `resources/views/vendor/mail` by default) are the mail theme's to style: their classes are not read as Tailwind, and a mail theme stylesheet there is neither checked for literals nor a source of exempt classes.
|
The guard reads every path it is given and fails, with `path:line`, on a class written in a view or PHP file that no stylesheet declares — not the application's own CSS, not the package's `md-*` — whatever it came from: a utility of a framework that isn't in the stack, a typo, or a class whose rule was deleted. Without a `.css` file among the scanned paths or a `missingStylesheets()` entry to supply the application's import graph, it has no declared class to compare against and reports every one of them. 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. Markdown mail components (under `mail.markdown.paths`, `resources/views/vendor/mail` by default) are the mail theme's to style: their classes are not read by the class check, and a mail theme stylesheet there is neither checked for literals nor a source of declared classes.
|
||||||
|
|
||||||
`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::…>`), `->links()` and the rows they write by hand (`data-md-list-row` on anything but `<x-card>` needs `components/list-item.css`), 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.
|
`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::…>`), `->links()` and the rows they write by hand (`data-md-list-row` on anything but `<x-card>` needs `components/list-item.css`), 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 — as long as `missingStylesheets()` is given an entry, its import graph still supplies the declared classes family (i) needs. `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.
|
||||||
|
|
||||||
## AI agents
|
## AI agents
|
||||||
|
|
||||||
|
|||||||
+19
-9
@@ -1,6 +1,6 @@
|
|||||||
# Upgrading
|
# Upgrading
|
||||||
|
|
||||||
## From 2.1.0 to 2.1.1
|
## From 2.1.0 to 2.2.0
|
||||||
|
|
||||||
- **Run `php artisan view:clear` after upgrading.** The components are no longer registered under a
|
- **Run `php artisan view:clear` after upgrading.** The components are no longer registered under a
|
||||||
view namespace named after a hash of their folder, which only views compiled before 1.0.1 still
|
view namespace named after a hash of their folder, which only views compiled before 1.0.1 still
|
||||||
@@ -28,14 +28,6 @@
|
|||||||
under that path is no longer checked for literal values, and its classes no longer exempt the
|
under that path is no longer checked for literal values, and its classes no longer exempt the
|
||||||
same names in the application's other views. A filter an application wrote around such a
|
same names in the application's other views. A filter an application wrote around such a
|
||||||
finding can go.
|
finding can go.
|
||||||
- **`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.
|
|
||||||
- **`DesignGuard`** counts a row written by hand. `data-md-list-row` on an `<li>`, a `<div>`, a
|
- **`DesignGuard`** counts a row written by hand. `data-md-list-row` on an `<li>`, a `<div>`, a
|
||||||
`<tr>` or `<x-row>` takes its hover, focus and press state layer and its selected fill from
|
`<tr>` or `<x-row>` takes its hover, focus and press state layer and its selected fill from
|
||||||
`components/list-item.css`, but `missingStylesheets()` and `unusedStylesheets()` read only tags
|
`components/list-item.css`, but `missingStylesheets()` and `unusedStylesheets()` read only tags
|
||||||
@@ -121,6 +113,24 @@
|
|||||||
itself from outside; a Tab onto the control, including the one that wraps round to it, and focus
|
itself from outside; a Tab onto the control, including the one that wraps round to it, and focus
|
||||||
moved within the layer still show them. An `autofocus` an application put on another control to
|
moved within the layer still show them. An `autofocus` an application put on another control to
|
||||||
keep the focus off the close button can go.
|
keep the focus off the close button can go.
|
||||||
|
- **`DesignGuard`** reports any class a view or PHP file writes that no stylesheet declares —
|
||||||
|
not the package's `md-*`, not the application's own CSS — instead of naming Tailwind's
|
||||||
|
utilities, variants and theme colours with their 2.0.0 replacement. It now catches a dead class
|
||||||
|
from any source: a typo, a utility of a framework that isn't in the stack, a class whose rule
|
||||||
|
was deleted — not only Tailwind's — but a class assembled in a PHP string (`'text-'.$tone`) is
|
||||||
|
still not seen. A test that asserted on the old Tailwind-shaped message needs the new one
|
||||||
|
instead.
|
||||||
|
- **The pagination views are `material.blade.php` and `simple-material.blade.php`**, no longer named
|
||||||
|
after Tailwind. The provider sets `Paginator::defaultView('pagination::material')` and
|
||||||
|
`defaultSimpleView('pagination::simple-material')`, and switches `livewire.pagination_theme` to
|
||||||
|
`material` whenever it still reads as Livewire's own default (`tailwind`, including when the key
|
||||||
|
is missing), so an application can no longer forget the config and silently render Livewire's own
|
||||||
|
Tailwind view. An application that already chose another theme, or a component with its own
|
||||||
|
`$paginationTheme` property or `paginationView()` method, is untouched — either still wins.
|
||||||
|
- **`foundation.css` no longer explains how to order layers beside Tailwind.** No consumer builds
|
||||||
|
Tailwind any more, so the paragraph about opening both entries with
|
||||||
|
`@layer properties, theme, base, material, components, utilities;` is gone; the layer statement
|
||||||
|
and the order it declares are otherwise unchanged.
|
||||||
|
|
||||||
## From 2.0.0 to 2.1.0
|
## From 2.0.0 to 2.1.0
|
||||||
|
|
||||||
|
|||||||
@@ -107,10 +107,11 @@ return [
|
|||||||
| Pagination
|
| Pagination
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Draw Laravel's and Livewire's paginators in M3: the package's views are
|
| Draw Laravel's and Livewire's paginators in M3: the package's views
|
||||||
| put in front of `pagination::tailwind` and `livewire::tailwind` (and
|
| become `Paginator::defaultView()`/`defaultSimpleView()`, and
|
||||||
| their simple versions). An application's own published pagination views
|
| `livewire.pagination_theme` is taken over while it is still Livewire's
|
||||||
| still win.
|
| own default. An application's own published pagination views still
|
||||||
|
| win, and so does a theme it chose deliberately.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@verbatim
|
@verbatim
|
||||||
## Livewire Material
|
## Livewire Material
|
||||||
|
|
||||||
This application uses `nonameweb/livewire-material`: Material 3 Expressive components for Laravel and Livewire, in plain CSS. It replaces UI kits such as maryUI, daisyUI and Flux, and Tailwind CSS, in this application.
|
This application uses `nonameweb/livewire-material`: Material 3 Expressive components for Laravel and Livewire, in plain CSS. No utility classes — Tailwind, daisyUI or any other — belong here: a class your CSS does not declare does nothing, and `DesignGuard` fails it.
|
||||||
|
|
||||||
- Components are anonymous Blade components, unprefixed unless `config/livewire-material.php` sets a `prefix`. Before writing or changing a view that uses them, activate the `livewire-material-development` skill for the props, slots and traps of each component.
|
- Components are anonymous Blade components, unprefixed unless `config/livewire-material.php` sets a `prefix`. Before writing or changing a view that uses them, activate the `livewire-material-development` skill for the props, slots and traps of each component.
|
||||||
- The CSS entry imports `foundation.css` first, then the stylesheet of each component the views render (or `all.css` for all of them). A component whose stylesheet is not imported renders unstyled; `DesignGuard::missingStylesheets()` names each missing `@import`.
|
- The CSS entry imports `foundation.css` first, then the stylesheet of each component the views render (or `all.css` for all of them). A component whose stylesheet is not imported renders unstyled; `DesignGuard::missingStylesheets()` names each missing `@import`.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Every view in this application is Material 3 Expressive (m3.material.io), through `nonameweb/livewire-material`. These rules decide what to write; the `material-3-design` skill carries the tables, the numbers and Google's source pages behind each one — activate it before designing a screen.
|
Every view in this application is Material 3 Expressive (m3.material.io), through `nonameweb/livewire-material`. These rules decide what to write; the `material-3-design` skill carries the tables, the numbers and Google's source pages behind each one — activate it before designing a screen.
|
||||||
|
|
||||||
The library is plain CSS on M3's tokens, and there are no utility classes: a Tailwind class, one of the library's 1.x utilities (bg-primary, type-body-md, medium:hidden) or a daisyUI class compiles to nothing, and so does a maryUI tag. A view is written three ways:
|
The library is plain CSS on M3's tokens. No utility classes — Tailwind, daisyUI or any other — belong here: a class your CSS does not declare does nothing, and `DesignGuard` fails it. A view is written three ways:
|
||||||
- Components and their props: `<x-button variant="filled">`, and the layout components `<x-row>`, `<x-stack>`, `<x-grid>`, `<x-feed>`, `<x-surface>` and `<x-pane>`, whose `gap` and `padding` take a spacing token (`space200`) and whose `hide-below`, `hide-from` and `stack-below` take a window size class.
|
- Components and their props: `<x-button variant="filled">`, and the layout components `<x-row>`, `<x-stack>`, `<x-grid>`, `<x-feed>`, `<x-surface>` and `<x-pane>`, whose `gap` and `padding` take a spacing token (`space200`) and whose `hide-below`, `hide-from` and `stack-below` take a window size class.
|
||||||
- A fixed set of classes for text and interaction on plain elements: `md-type-*`, `md-ink-*`, `md-text-*`, `md-truncate`, `md-tabular`, `md-visually-hidden`, `md-state-layer`, `md-focus-ring`, `md-touch-target` and `md-link`.
|
- A fixed set of classes for text and interaction on plain elements: `md-type-*`, `md-ink-*`, `md-text-*`, `md-truncate`, `md-tabular`, `md-visually-hidden`, `md-state-layer`, `md-focus-ring`, `md-touch-target` and `md-link`.
|
||||||
- The application's own CSS, named by the application, whose values are `--md-sys-*` custom properties.
|
- The application's own CSS, named by the application, whose values are `--md-sys-*` custom properties.
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile'));
|
|||||||
|
|
||||||
## Tokens
|
## Tokens
|
||||||
|
|
||||||
2.0.0's vocabulary is plain CSS. There are no utility classes: a Tailwind class, or one of 1.x's own colour, type, shape, elevation, motion or breakpoint utilities, compiles to nothing, and the design guard names the replacement for each (see Testing the design). `resources/css/foundation.css` is the one required import, first in the entry, followed by the stylesheets of the components the views render (see Setup; 1.x's `material.css` shortcut is gone). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, the page's `surface` and `on-surface` in the brand typeface smoothed in grayscale (drop Tailwind's `antialiased`), every `--md-sys-*` token, and the classes below. An application's views reach for four things, in this order:
|
2.0.0's vocabulary is plain CSS. There are no utility classes: one of 1.x's own colour, type, shape, elevation, motion or breakpoint utilities, or any other class no stylesheet declares, compiles to nothing, and the design guard fails on it (see Testing the design). `resources/css/foundation.css` is the one required import, first in the entry, followed by the stylesheets of the components the views render (see Setup; 1.x's `material.css` shortcut is gone). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, the page's `surface` and `on-surface` in the brand typeface smoothed in grayscale, every `--md-sys-*` token, and the classes below. An application's views reach for four things, in this order:
|
||||||
|
|
||||||
- **A component's own props** — `color`, `variant`, `size`, `dense` — before anything else.
|
- **A component's own props** — `color`, `variant`, `size`, `dense` — before anything else.
|
||||||
- **Layout components** for arrangement and spacing: `<x-row>`, `<x-stack>`, `<x-grid>`, `<x-feed>`, `<x-surface>` inside `<x-pane>`s (see Layout), with `gap` and `padding` as spacing tokens (`space25` … `space900`, 2–72px) and `hide-below`, `hide-from` and `stack-below` for the window size classes. Nothing arranges by class.
|
- **Layout components** for arrangement and spacing: `<x-row>`, `<x-stack>`, `<x-grid>`, `<x-feed>`, `<x-surface>` inside `<x-pane>`s (see Layout), with `gap` and `padding` as spacing tokens (`space25` … `space900`, 2–72px) and `hide-below`, `hide-from` and `stack-below` for the window size classes. Nothing arranges by class.
|
||||||
@@ -1164,7 +1164,7 @@ A data table: write plain `<thead>`, `<tr>`, `<th>`, `<td>` inside `<x-table>`;
|
|||||||
{{ $shares->links() }}
|
{{ $shares->links() }}
|
||||||
```
|
```
|
||||||
|
|
||||||
Pagination: `$paginator->links()` (Laravel and Livewire, full and simple/cursor) is drawn in M3 — current page in secondary-container, "Page 2 of 7" on a phone. Turn off with `config('livewire-material.pagination')` = `false`; published `vendor/pagination` or `vendor/livewire` views still win.
|
Pagination: `$paginator->links()` (Laravel and Livewire, full and simple/cursor) is drawn in M3 — current page in secondary-container, "Page 2 of 7" on a phone. Turn off with `config('livewire-material.pagination')` = `false`; published `vendor/pagination` or `vendor/livewire` views still win. The provider also sets `livewire.pagination_theme` to `material` whenever it still reads as Livewire's own default (`tailwind`); an application's own theme, or a component's `$paginationTheme` property or `paginationView()` method, still wins.
|
||||||
|
|
||||||
## Testing the design
|
## Testing the design
|
||||||
|
|
||||||
@@ -1180,13 +1180,13 @@ it('uses only what compiles', function () {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
It reads every path it is given and fails with `path:line` on unknown symbol names, Blade directives written inside a component tag (where they do not compile), and every Tailwind utility or variant — none compiles in a Tailwind-free application — each match printed beside its own 2.0.0 replacement (a layout component and prop, an `md-*` class, a `var(--md-sys-…)` token, or your own CSS), so run the check and read its output rather than a table here. A class the application's own stylesheets declare is exempt, and so is every `md-*` class.
|
It reads every path it is given and fails with `path:line` on unknown symbol names, Blade directives written inside a component tag (where they do not compile), and a class no stylesheet declares — not your own CSS, not the package's `md-*` — whatever it came from: a leftover utility, a typo, a class whose rule was deleted. Without a `.css` file among the scanned paths or a `missingStylesheets()` entry to supply your import graph, it has no declared class to compare against and reports every one of them.
|
||||||
|
|
||||||
In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, font size, weight, line height, letter spacing, easing or duration, and on a media query width off 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine.
|
In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, font size, weight, line height, letter spacing, easing or duration, and on a media query width off 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine.
|
||||||
|
|
||||||
Markdown mail components — whatever lies under `config('mail.markdown.paths')`, `resources/views/vendor/mail` by default — are drawn by the mail theme, not by your CSS entry, and its classes (`table`, `button`, `panel`, `break-all`) share Tailwind's names. So the guard skips the class checks there (it still reads their icon names, the directives in their component tags and your own bans), and a mail theme stylesheet under that path, which has to write literal values, is neither checked for literals nor a source of exempt classes.
|
Markdown mail components — whatever lies under `config('mail.markdown.paths')`, `resources/views/vendor/mail` by default — are drawn by the mail theme, not by your CSS entry. So the guard skips the class check there (it still reads their icon names, the directives in their component tags and your own bans), and a mail theme stylesheet under that path, which has to write literal values, is neither checked for literals nor a source of declared classes.
|
||||||
|
|
||||||
`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; `tailwindcss` or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or `<x-livewire-material::…>`), `->links()`, and a row a view of yours writes by hand (`data-md-list-row` on anything but `<x-card>`, which needs `components/list-item.css`; the package's own views are not read for it); each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens. `unusedStylesheets($cssEntry)` is the other way round: a package stylesheet the entry imports directly that no scanned view needs — no tag, `->links()` or row written by hand — even through a needed stylesheet's own imports, named at its `@import` line to remove (an entry importing `all.css` is left alone). `forbidColours([...])` fails wherever a left-out role (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 a pattern of your own.
|
`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; a package name or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or `<x-livewire-material::…>`), `->links()`, and a row a view of yours writes by hand (`data-md-list-row` on anything but `<x-card>`, which needs `components/list-item.css`; the package's own views are not read for it); each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens, as long as this entry is what `missingStylesheets()` reads — its import graph is still what supplies the declared classes above. `unusedStylesheets($cssEntry)` is the other way round: a package stylesheet the entry imports directly that no scanned view needs — no tag, `->links()` or row written by hand — even through a needed stylesheet's own imports, named at its `@import` line to remove (an entry importing `all.css` is left alone). `forbidColours([...])` fails wherever a left-out role (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 a pattern of your own.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/*
|
/*
|
||||||
* Livewire Material for an application that wants everything: the foundation, every layout
|
* Livewire Material for an application that wants everything: the foundation, every layout
|
||||||
* stylesheet and every component stylesheet, each imported once. The blocks below are the same
|
* stylesheet and every component stylesheet, each imported once. The blocks below are the same
|
||||||
* groups `components.css` and `layout.css` used before this file replaced both of them (plan step
|
* groups `components.css` and `layout.css` used before this file replaced both of them — one
|
||||||
* 37) — one block per group, so a stylesheet still lands in a predictable place — plus the
|
* block per group, so a stylesheet still lands in a predictable place — plus the Layout block
|
||||||
* Layout block those two files didn't share.
|
* those two files didn't share.
|
||||||
*
|
*
|
||||||
* This is not the recommended way to bring the package's CSS in: importing `all.css` pulls in
|
* This is not the recommended way to bring the package's CSS in: importing `all.css` pulls in
|
||||||
* every component's rules whether an application's views render them or not. The recommended path
|
* every component's rules whether an application's views render them or not. The recommended path
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
* The page carries this file itself: `src/Support/ErrorPage.php` inlines it, with its imports, into
|
* The page carries this file itself: `src/Support/ErrorPage.php` inlines it, with its imports, into
|
||||||
* every error page (`Stylesheets::bundle()`) — beside the application's Vite tags when there is a
|
* every error page (`Stylesheets::bundle()`) — beside the application's Vite tags when there is a
|
||||||
* build (`layoutStyles()`), inside the whole fallback stylesheet when there is none
|
* build (`layoutStyles()`), inside the whole fallback stylesheet when there is none
|
||||||
* (`fallbackStyles()`, plan step 40) — so an application imports nothing for its error pages
|
* (`fallbackStyles()`) — so an application imports nothing for its error pages. The body is still
|
||||||
* (plan step 46). The body is still matched only when it holds the layout
|
* matched only when it holds the layout (`body:has(> [data-md-error-page])`): an application that
|
||||||
* (`body:has(> [data-md-error-page])`): an application that imports `all.css` has this file in
|
* imports `all.css` has this file in its bundle beside every other page, where a bare `body` rule
|
||||||
* its bundle beside every other page, where a bare `body` rule would restyle them all.
|
* would restyle them all.
|
||||||
*
|
*
|
||||||
* The page's font is the foundation's, `--md-ref-typeface-brand` on `html` (foundation/base.css),
|
* The page's font is the foundation's, `--md-ref-typeface-brand` on `html` (foundation/base.css),
|
||||||
* as the old `font-sans` was; only the fallback, which has no `@font-face`, uses a system stack.
|
* as the old `font-sans` was; only the fallback, which has no `@font-face`, uses a system stack.
|
||||||
|
|||||||
@@ -78,14 +78,11 @@
|
|||||||
* A bar that has slid off the bottom of the window is no longer there to clear, so the offset
|
* A bar that has slid off the bottom of the window is no longer there to clear, so the offset
|
||||||
* everything pinned to the bottom reads drops to the bottom safe area and whatever the
|
* everything pinned to the bottom reads drops to the bottom safe area and whatever the
|
||||||
* application has docked on the bar — a `fab` button, the snackbar and the page's own bottom
|
* application has docked on the bar — a `fab` button, the snackbar and the page's own bottom
|
||||||
* padding all follow it down and come back up with it. Layered now, unlike before this
|
* padding all follow it down and come back up with it. `<x-scaffold>` publishes
|
||||||
* rewrite: <x-scaffold> used to publish --material-bottom-bar with a Tailwind utility, which
|
* `--material-bottom-bar` itself, in `material.layout` (layout/scaffold.css), a layer this
|
||||||
* no rule in any layer could outrank regardless of specificity; now scaffold.css publishes it
|
* file's own `material.components` always outranks by declaration order alone — the same
|
||||||
* itself in `material.layout` (layout/scaffold.css), a layer this file's own
|
* reason toolbar.css's and fab.css's overrides only have to beat another `material.components`
|
||||||
* `material.components` always outranks by declaration order alone — the same reason
|
* rule. Keyed on `data-md-scaffold`, the hook the scaffold renders on its root.
|
||||||
* toolbar.css's and fab.css's overrides only have to beat another `material.components` rule,
|
|
||||||
* not chase a Tailwind utility out of the cascade. Keyed on `data-md-scaffold`, the hook the
|
|
||||||
* scaffold renders on its root, not `data-app-shell`, which that rewrite also retired.
|
|
||||||
*/
|
*/
|
||||||
[data-md-scaffold]:has([data-md-navigation-bar][data-md-hide-on-scroll][data-md-hidden]) {
|
[data-md-scaffold]:has([data-md-navigation-bar][data-md-hide-on-scroll][data-md-hidden]) {
|
||||||
--material-bottom-bar: calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px));
|
--material-bottom-bar: calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px));
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* The paginators, Laravel's and Livewire's, drawn in M3 (resources/views/pagination/**): the package
|
* The paginators, Laravel's and Livewire's, drawn in M3 (resources/views/pagination/**): named
|
||||||
* puts its views in front of the frameworks' `tailwind` and `simple-tailwind` ones, whose names they
|
* `material`/`simple-material`, put in front of the frameworks' own namespaces and set as
|
||||||
* keep.
|
* Laravel's default view (`Paginator::defaultView()`/`defaultSimpleView()`) and, where Livewire
|
||||||
|
* still reads its own default, as its `pagination_theme`.
|
||||||
*
|
*
|
||||||
* The numbered paginator is a row: on its start, what the page holds in body-small on-surface-variant
|
* The numbered paginator is a row: on its start, what the page holds in body-small on-surface-variant
|
||||||
* — "21–30 of 95", or "Page 3 of 10" below `medium` (600px), where the page numbers give way and only
|
* — "21–30 of 95", or "Page 3 of 10" below `medium` (600px), where the page numbers give way and only
|
||||||
|
|||||||
Vendored
-4
@@ -24,10 +24,6 @@
|
|||||||
* therefore outranks every package rule, whatever the selector — an application's broad selector
|
* therefore outranks every package rule, whatever the selector — an application's broad selector
|
||||||
* (`button { … }`) restyles the components too. foundation/hidden.css keeps its two `!important`
|
* (`button { … }`) restyles the components too. foundation/hidden.css keeps its two `!important`
|
||||||
* rules outside the layers, so `hidden` and `x-cloak` hide an element whatever sets its display.
|
* rules outside the layers, so `hidden` and `x-cloak` hide an element whatever sets its display.
|
||||||
*
|
|
||||||
* An application still building Tailwind keeps it in a separate entry and opens both entries with
|
|
||||||
* `@layer properties, theme, base, material, components, utilities;`, so the `material` layers sit
|
|
||||||
* above Tailwind's preflight and below its utilities.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* Text is smoothed in grayscale where a platform offers the choice (macOS browsers): Google Sans
|
* Text is smoothed in grayscale where a platform offers the choice (macOS browsers): Google Sans
|
||||||
* Flex is drawn for it, and subpixel smoothing thickens its strokes on light text over a dark
|
* Flex is drawn for it, and subpixel smoothing thickens its strokes on light text over a dark
|
||||||
* surface. Every surface the package draws — the showcase, the error pages — is set this way,
|
* surface. Every surface the package draws — the showcase, the error pages — is set this way,
|
||||||
* and so is an application, which drops Tailwind's `antialiased` class with nothing to replace.
|
* and so is an application, with no class of its own needed.
|
||||||
*
|
*
|
||||||
* In `material.base`, above the reset and below every component and text class.
|
* In `material.base`, above the reset and below every component and text class.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* The reset: what a browser draws before any stylesheet, taken back to a clean start.
|
* The reset: what a browser draws before any stylesheet, taken back to a clean start.
|
||||||
*
|
*
|
||||||
* Rule for rule the reset the components were built on, Tailwind CSS 4's preflight (MIT,
|
* Rule for rule the reset the components were built on, Tailwind CSS 4's preflight (MIT,
|
||||||
* © Tailwind Labs, after modern-normalize), so a component looks the same whether an application
|
* © Tailwind Labs, after modern-normalize): every box `border-box` with a `0 solid` border and no
|
||||||
* still builds Tailwind or not: every box `border-box` with a `0 solid` border and no margin or
|
* margin or padding, headings and lists unstyled, form controls inheriting font, colour and
|
||||||
* padding, headings and lists unstyled, form controls inheriting font, colour and tracking with no
|
* tracking with no background or radius, media as blocks that never overflow their parent. Three
|
||||||
* background or radius, media as blocks that never overflow their parent. Three differences:
|
* differences:
|
||||||
*
|
*
|
||||||
* - no typeface here: foundation/base.css sets `--md-ref-typeface-brand` on <html>;
|
* - no typeface here: foundation/base.css sets `--md-ref-typeface-brand` on <html>;
|
||||||
* - a placeholder is `on-surface-variant`, the colour of M3's text field placeholder (androidx
|
* - a placeholder is `on-surface-variant`, the colour of M3's text field placeholder (androidx
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* way it goes, so it mirrors in a right-to-left document (§ Layout → Bidirectionality / RTL).
|
* way it goes, so it mirrors in a right-to-left document (§ Layout → Bidirectionality / RTL).
|
||||||
*
|
*
|
||||||
* In `material.layout`; `hide-below`/`hide-from` come from visibility.css. Imports app-bar.css and
|
* In `material.layout`; `hide-below`/`hide-from` come from visibility.css. Imports app-bar.css and
|
||||||
* button.css for the pane's own top app bar and its back button (plan step 36 carry-over).
|
* button.css for the pane's own top app bar and its back button.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
* <x-scaffold>: M3's scaffold — bars, then rails, then panes (docs/reference/m3/foundations.md
|
* <x-scaffold>: M3's scaffold — bars, then rails, then panes (docs/reference/m3/foundations.md
|
||||||
* § Layout → Scaffold) — as a column: an optional `banner` full width, a row below it holding the
|
* § Layout → Scaffold) — as a column: an optional `banner` full width, a row below it holding the
|
||||||
* adaptive navigation rail and the content region, the bottom navigation bar on a compact window,
|
* adaptive navigation rail and the content region, the bottom navigation bar on a compact window,
|
||||||
* and the snackbar host. Plan step 36 (navigation group) moves in everything the view drew with
|
* and the snackbar host.
|
||||||
* Tailwind; `data-md-scaffold-fab` and the content region's `--md-layout-margin` are step 35's,
|
|
||||||
* unchanged below.
|
|
||||||
*
|
*
|
||||||
* `--material-margin` is M3's window margin, which the content region pads itself with and an
|
* `--material-margin` is M3's window margin, which the content region pads itself with and an
|
||||||
* application's own rules can read: 16px below `medium`, 24px from it
|
* application's own rules can read: 16px below `medium`, 24px from it
|
||||||
@@ -23,11 +21,11 @@
|
|||||||
* `[data-md-scaffold-content]` — the rail's sibling — is always a flex column, so the page's own
|
* `[data-md-scaffold-content]` — the rail's sibling — is always a flex column, so the page's own
|
||||||
* bar, the FAB wrapper and `<main>` stack regardless of the row's own display.
|
* bar, the FAB wrapper and `<main>` stack regardless of the row's own display.
|
||||||
*
|
*
|
||||||
* `[data-md-scaffold-actions]`'s column layout is `rail-collapsed:flex-col`, the one Tailwind
|
* `[data-md-scaffold-actions]`'s column layout comes from a container style query on
|
||||||
* variant scaffold.blade.php still used (`resources/css/tailwind.css`, removed with this rule): the
|
* `--md-navigation-rail-value: collapsed`: the actions row is always inside a rail, never the rail
|
||||||
* actions row is always inside a rail, never the rail element itself, so it reads
|
* element itself, so it reads that property with `@container style()` like any other application
|
||||||
* `--md-navigation-rail-value` with `@container style()` like any other application content does
|
* content does (`resources/css/components/navigation-rail.css`), rather than copying the rail's
|
||||||
* (`resources/css/components/navigation-rail.css`), rather than copying the rail's own conditions.
|
* own conditions.
|
||||||
*
|
*
|
||||||
* `[data-md-scaffold-bar]` pins the navigation bar to the window's bottom edge below `medium`, at
|
* `[data-md-scaffold-bar]` pins the navigation bar to the window's bottom edge below `medium`, at
|
||||||
* z-index 30: over the page, under the rail's scrim (40) and the snackbar (50, toast.css). While a
|
* z-index 30: over the page, under the rail's scrim (40) and the snackbar (50, toast.css). While a
|
||||||
@@ -38,8 +36,7 @@
|
|||||||
* (toast.css), a placed toolbar (toolbar.css) and the content region's bottom padding. Without a
|
* (toast.css), a placed toolbar (toolbar.css) and the content region's bottom padding. Without a
|
||||||
* bar, or from `medium`, the scaffold publishes nothing and each reader falls back to 0px. A bar that has slid
|
* bar, or from `medium`, the scaffold publishes nothing and each reader falls back to 0px. A bar that has slid
|
||||||
* away on scroll drops its own height from the variable (navigation-bar.css); that rule wins
|
* away on scroll drops its own height from the variable (navigation-bar.css); that rule wins
|
||||||
* because `material.components` comes after this file's `material.layout`, where before this
|
* because `material.components` comes after this file's `material.layout` in the cascade.
|
||||||
* rewrite the variable was a Tailwind utility and the override had to sit outside every layer.
|
|
||||||
*
|
*
|
||||||
* The content region's `<main>` is matched as the content wrapper's own child, so a `<main>` an
|
* The content region's `<main>` is matched as the content wrapper's own child, so a `<main>` an
|
||||||
* application nests inside the page takes none of the region's padding or clipping.
|
* application nests inside the page takes none of the region's padding or clipping.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* The showcase's own chrome: what its pages draw beyond the layout components, the `md-*` text
|
* The showcase's own chrome: what its pages draw beyond the layout components, the `md-*` text
|
||||||
* classes and the shared interaction classes (plan step 38) — the rail's wordmark, the search box
|
* classes and the shared interaction classes — the rail's wordmark, the search box and its
|
||||||
* and its results, a keyboard key in running text, an example's card and its code sample, and the
|
* results, a keyboard key in running text, an example's card and its code sample, and the
|
||||||
* foundation pages' specimens: a colour swatch, a type-scale row, a corner swatch, an elevation
|
* foundation pages' specimens: a colour swatch, a type-scale row, a corner swatch, an elevation
|
||||||
* tile, a motion track and its dot, an icon search result and the Layout page's breakpoint cards.
|
* tile, a motion track and its dot, an icon search result and the Layout page's breakpoint cards.
|
||||||
*
|
*
|
||||||
|
|||||||
+5
-4
@@ -1,8 +1,9 @@
|
|||||||
{{-- Laravel's paginator (`$items->links()` outside Livewire), drawn in M3 as Livewire's is
|
{{-- Laravel's paginator (`$items->links()` outside Livewire), drawn in M3 as Livewire's is
|
||||||
(resources/views/pagination/livewire/tailwind.blade.php): links instead of actions, the current
|
(resources/views/pagination/livewire/material.blade.php): links instead of actions, the
|
||||||
page in secondary-container, "Page 2 of 7" below 600px. The package puts this in front of
|
current page in secondary-container, "Page 2 of 7" below 600px. The name is `material`; the
|
||||||
`pagination::tailwind` (config `livewire-material.pagination`); the name is Laravel's, the drawing
|
provider replaces Laravel's own default with
|
||||||
resources/css/components/pagination.css. --}}
|
`Paginator::defaultView('pagination::material')` (config `livewire-material.pagination`). The
|
||||||
|
drawing is resources/css/components/pagination.css. --}}
|
||||||
|
|
||||||
@if ($paginator->hasPages())
|
@if ($paginator->hasPages())
|
||||||
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-md-pagination>
|
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-md-pagination>
|
||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
{{-- Laravel's simple and cursor paginator, drawn in M3: previous and next as outlined buttons
|
{{-- Laravel's simple and cursor paginator, drawn in M3: previous and next as outlined buttons.
|
||||||
(the package puts this in front of `pagination::simple-tailwind`). --}}
|
The name is `simple-material`; the provider replaces Laravel's own default with
|
||||||
|
`Paginator::defaultSimpleView('pagination::simple-material')`. --}}
|
||||||
|
|
||||||
@if ($paginator->hasPages())
|
@if ($paginator->hasPages())
|
||||||
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-md-pagination>
|
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-md-pagination>
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{{-- Livewire's paginator, drawn in M3. The package puts this in front of Livewire's own
|
{{-- Livewire's paginator, drawn in M3. The name is `material`; Livewire resolves
|
||||||
`livewire::tailwind` (config `livewire-material.pagination`), whose gray and blue classes do
|
`livewire::` . config('livewire.pagination_theme') to find it, and the provider sets that
|
||||||
not exist in an M3 theme. The file keeps Livewire's view name; nothing in it is Tailwind — the
|
config to `material` (config `livewire-material.pagination`) while it still reads as
|
||||||
drawing is resources/css/components/pagination.css.
|
Livewire's own default. The drawing is resources/css/components/pagination.css.
|
||||||
|
|
||||||
The wiring is Livewire's, unchanged: `previousPage`, `nextPage` and `gotoPage` with the page
|
The wiring is Livewire's, unchanged: `previousPage`, `nextPage` and `gotoPage` with the page
|
||||||
name, and the scroll back to the top of whatever `scrollTo` names. A page number is drawn as a
|
name, and the scroll back to the top of whatever `scrollTo` names. A page number is drawn as a
|
||||||
+4
-2
@@ -1,5 +1,7 @@
|
|||||||
{{-- Livewire's simple and cursor paginator, drawn in M3: previous and next as outlined buttons
|
{{-- Livewire's simple and cursor paginator, drawn in M3: previous and next as outlined buttons.
|
||||||
(the package puts this in front of `livewire::simple-tailwind`). --}}
|
The name is `simple-material`; Livewire resolves `livewire::simple-` . config
|
||||||
|
('livewire.pagination_theme') to find it, which the provider sets to `material` while it
|
||||||
|
still reads as Livewire's own default. --}}
|
||||||
|
|
||||||
@php
|
@php
|
||||||
if (! isset($scrollTo)) {
|
if (! isset($scrollTo)) {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{{-- The live breakpoint readout: this window's width, and the M3 breakpoint it falls in, read
|
{{-- The live breakpoint readout: this window's width, and the M3 breakpoint it falls in, read
|
||||||
from the same numbers resources/js/breakpoints.js compares against. Shared by the Layout
|
from the same numbers resources/js/breakpoints.js compares against. Shared by the Layout
|
||||||
section's Overview and its three canonical layout pages (plan step 38). --}}
|
section's Overview and its three canonical layout pages. --}}
|
||||||
|
|
||||||
<x-livewire-material::surface
|
<x-livewire-material::surface
|
||||||
level="surface-container-low"
|
level="surface-container-low"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{-- The feed canonical layout on a page of its own (plan step 38): the same working demo the
|
{{-- The feed canonical layout on a page of its own: the same working demo the Layout section's
|
||||||
Layout section's Overview linked from its own navigation, built on the real <x-feed>, with
|
Overview linked from its own navigation, built on the real <x-feed>, with the live
|
||||||
the live breakpoint readout the Overview page has. --}}
|
breakpoint readout the Overview page has. --}}
|
||||||
|
|
||||||
@extends('livewire-material::showcase.layout')
|
@extends('livewire-material::showcase.layout')
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{-- The list-detail canonical layout on a page of its own (plan step 38): the same working demo
|
{{-- The list-detail canonical layout on a page of its own: the same working demo the Layout
|
||||||
the Layout section's Overview linked from its own navigation, built on the real
|
section's Overview linked from its own navigation, built on the real <x-list-detail>, with
|
||||||
<x-list-detail>, with the live breakpoint readout the Overview page has. --}}
|
the live breakpoint readout the Overview page has. --}}
|
||||||
|
|
||||||
@extends('livewire-material::showcase.layout')
|
@extends('livewire-material::showcase.layout')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{-- The supporting-pane canonical layout on a page of its own (plan step 38): the same working
|
{{-- The supporting-pane canonical layout on a page of its own: the same working demo the
|
||||||
demo the Layout section's Overview linked from its own navigation, built on the real
|
Layout section's Overview linked from its own navigation, built on the real
|
||||||
<x-supporting-pane>, with the live breakpoint readout the Overview page has. --}}
|
<x-supporting-pane>, with the live breakpoint readout the Overview page has. --}}
|
||||||
|
|
||||||
@extends('livewire-material::showcase.layout')
|
@extends('livewire-material::showcase.layout')
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
describing them.
|
describing them.
|
||||||
|
|
||||||
Then the layout components, and the way on to the three canonical layouts — list-detail,
|
Then the layout components, and the way on to the three canonical layouts — list-detail,
|
||||||
supporting pane, feed — each on a page of its own (plan step 38), reached through the
|
supporting pane, feed — each on a page of its own, reached through the section navigation
|
||||||
section navigation below. --}}
|
below. --}}
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$examples = [
|
$examples = [
|
||||||
|
|||||||
+2
-2
@@ -33,8 +33,8 @@ Route::get('mail', [ShowcasePageController::class, 'mail'])->name('mail');
|
|||||||
|
|
||||||
Route::get('search.json', [ShowcaseController::class, 'search'])->name('search');
|
Route::get('search.json', [ShowcaseController::class, 'search'])->name('search');
|
||||||
|
|
||||||
// The Layout section's three canonical layouts (plan step 38), each a page of its own rather
|
// The Layout section's three canonical layouts, each a page of its own rather than an
|
||||||
// than an anchor on the Layout section's Overview (`{section}` below).
|
// anchor on the Layout section's Overview (`{section}` below).
|
||||||
Route::get('layout/{page}', [ShowcaseController::class, 'layout'])
|
Route::get('layout/{page}', [ShowcaseController::class, 'layout'])
|
||||||
->whereIn('page', ['list-detail', 'supporting-pane', 'feed'])
|
->whereIn('page', ['list-detail', 'supporting-pane', 'feed'])
|
||||||
->name('layout');
|
->name('layout');
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The showcase's own CSS, served outside the application's Vite build so its chrome — the layout
|
* The showcase's own CSS, served outside the application's Vite build so its chrome — the layout
|
||||||
* components, the text classes, `showcase.css` — renders whatever an application's build contains
|
* components, the text classes, `showcase.css` — renders whatever an application's build
|
||||||
* (plan step 38): `all.css` (every package stylesheet) and `showcase.css`, prebuilt into
|
* contains: `all.css` (every package stylesheet) and `showcase.css`, prebuilt into
|
||||||
* resources/dist/showcase.css (`npm run build:stylesheets`), long-cached under a content hash, and
|
* resources/dist/showcase.css (`npm run build:stylesheets`), long-cached under a content hash, and
|
||||||
* the two package folders a relative `url()` in it points into (the brand's woff2, `menu.css`'s
|
* the two package folders a relative `url()` in it points into (the brand's woff2, `menu.css`'s
|
||||||
* check mark SVG). The stylesheet is served from `assets/css/`, beside `file()`'s `assets/fonts/`
|
* check mark SVG). The stylesheet is served from `assets/css/`, beside `file()`'s `assets/fonts/`
|
||||||
@@ -111,10 +111,10 @@ class ShowcaseAssetController
|
|||||||
* The application's Vite entries (`livewire-material.showcase.vite`) the showcase's own pages
|
* The application's Vite entries (`livewire-material.showcase.vite`) the showcase's own pages
|
||||||
* pass to `@vite()`: every one but a stylesheet — a path Laravel's Vite itself treats as CSS —
|
* pass to `@vite()`: every one but a stylesheet — a path Laravel's Vite itself treats as CSS —
|
||||||
* since those pages draw their CSS from `url()`'s bundle alone. An application's CSS entry
|
* since those pages draw their CSS from `url()`'s bundle alone. An application's CSS entry
|
||||||
* would load Tailwind for nothing, or the package's stylesheets a second time. Empty when the
|
* would load the application's own styling for nothing, or the package's stylesheets a second
|
||||||
* list names only stylesheets, and then the page skips `@vite()` altogether (it would read a
|
* time. Empty when the list names only stylesheets, and then the page skips `@vite()`
|
||||||
* build manifest for nothing). A JavaScript entry that imports a stylesheet itself still
|
* altogether (it would read a build manifest for nothing). A JavaScript entry that imports a
|
||||||
* brings it: Vite links a chunk's own CSS.
|
* stylesheet itself still brings it: Vite links a chunk's own CSS.
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ class ShowcaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One of the Layout section's own pages (plan step 38): a canonical layout on a page of its
|
* One of the Layout section's own pages: a canonical layout on a page of its own, sharing the
|
||||||
* own, sharing the Layout section's rail entry and title.
|
* Layout section's rail entry and title.
|
||||||
*/
|
*/
|
||||||
public function layout(string $page): View
|
public function layout(string $page): View
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace NoNameWeb\LivewireMaterial;
|
namespace NoNameWeb\LivewireMaterial;
|
||||||
|
|
||||||
use Illuminate\Contracts\View\Factory;
|
use Illuminate\Contracts\View\Factory;
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
@@ -133,10 +134,28 @@ class LivewireMaterialServiceProvider extends ServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Put the M3 paginators in front of Laravel's and Livewire's own. Prepended to their
|
* Put the M3 paginators in front of Laravel's and Livewire's own, and take over both
|
||||||
* namespaces rather than set as the default view, because Livewire sets its own default on
|
* frameworks' default theme so no application forgets the config and silently renders
|
||||||
* every render; an application's published `vendor/pagination` or `vendor/livewire` views are
|
* Laravel's or Livewire's own Tailwind view instead.
|
||||||
* looked up before any namespace path, so they still win.
|
*
|
||||||
|
* The namespaces still go through callAfterResolving('view', …): the Factory instance only
|
||||||
|
* exists once the `view` service resolves, which may happen before or after this method runs.
|
||||||
|
* `Paginator::defaultView()`/`defaultSimpleView()` and `livewire.pagination_theme`, in
|
||||||
|
* contrast, are a plain static and a config value — nothing needs resolving first — so they
|
||||||
|
* are set here directly, during this provider's boot(). Package providers boot before the
|
||||||
|
* application's own (its `bootstrap/providers.php` list is appended after the discovered
|
||||||
|
* package providers), so an application that calls `Paginator::defaultView()` again, or sets
|
||||||
|
* `livewire.pagination_theme` itself, in its own boot() still wins. Deferring them into the
|
||||||
|
* callback instead would risk the opposite: `view` can resolve arbitrarily late — the first
|
||||||
|
* render, possibly after the application's own boot() already set its override — and this
|
||||||
|
* call would silently overwrite it.
|
||||||
|
*
|
||||||
|
* `livewire.pagination_theme` is only taken over while it still reads as Livewire's own
|
||||||
|
* default, `tailwind` — including when the key is missing entirely, since
|
||||||
|
* `SupportPagination::paginationView()` reads it with that same fallback. An application that
|
||||||
|
* chose another theme, or already set `material`, keeps it. Either way, a component's own
|
||||||
|
* `$paginationTheme` property or `paginationView()` method still wins over the config, because
|
||||||
|
* Livewire checks those first.
|
||||||
*/
|
*/
|
||||||
protected function registerPagination(): void
|
protected function registerPagination(): void
|
||||||
{
|
{
|
||||||
@@ -148,6 +167,13 @@ class LivewireMaterialServiceProvider extends ServiceProvider
|
|||||||
$view->prependNamespace('pagination', __DIR__.'/../resources/views/pagination/laravel');
|
$view->prependNamespace('pagination', __DIR__.'/../resources/views/pagination/laravel');
|
||||||
$view->prependNamespace('livewire', __DIR__.'/../resources/views/pagination/livewire');
|
$view->prependNamespace('livewire', __DIR__.'/../resources/views/pagination/livewire');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Paginator::defaultView('pagination::material');
|
||||||
|
Paginator::defaultSimpleView('pagination::simple-material');
|
||||||
|
|
||||||
|
if (config('livewire.pagination_theme', 'tailwind') === 'tailwind') {
|
||||||
|
config(['livewire.pagination_theme' => 'material']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ class Sections
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Layout section's three canonical layouts (plan step 38): each is a page of its own
|
// The Layout section's three canonical layouts: each is a page of its own rather than an
|
||||||
// rather than an anchor on the Overview, so its search entry carries its own URL; the
|
// anchor on the Overview, so its search entry carries its own URL; the page's own source
|
||||||
// page's own source joins the Layout section's for the component-homes scan below, so
|
// joins the Layout section's for the component-homes scan below, so <x-list-detail>,
|
||||||
// <x-list-detail>, <x-supporting-pane> and <x-feed> still find a home.
|
// <x-supporting-pane> and <x-feed> still find a home.
|
||||||
foreach (static::layoutPages() as $key => $page) {
|
foreach (static::layoutPages() as $key => $page) {
|
||||||
if ($key === 'layout') {
|
if ($key === 'layout') {
|
||||||
continue;
|
continue;
|
||||||
@@ -90,11 +90,11 @@ class Sections
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Layout section's own pages (plan step 38): the overview and the three canonical
|
* The Layout section's own pages: the overview and the three canonical layouts, each on a
|
||||||
* layouts, each on a page of its own, sharing one `<x-section-nav>` and the live breakpoint
|
* page of its own, sharing one `<x-section-nav>` and the live breakpoint readout rather than
|
||||||
* readout rather than three anchors on the Overview. They are not top-level destinations of
|
* three anchors on the Overview. They are not top-level destinations of their own —
|
||||||
* their own — `Sections::all()` still has one `layout` entry for the rail and the overview
|
* `Sections::all()` still has one `layout` entry for the rail and the overview grid — so this
|
||||||
* grid — so this is their own small index instead.
|
* is their own small index instead.
|
||||||
*
|
*
|
||||||
* @return array<string, array{title: string, icon: string, url: string}>
|
* @return array<string, array{title: string, icon: string, url: string}>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ namespace NoNameWeb\LivewireMaterial\Support;
|
|||||||
* nested panel), the medium and high contrast levels under `[data-contrast]` beside it, and — when
|
* nested panel), the medium and high contrast levels under `[data-contrast]` beside it, and — when
|
||||||
* a caller gives one — every colour profile again under its own prefix (`[data-scheme='name']`).
|
* a caller gives one — every colour profile again under its own prefix (`[data-scheme='name']`).
|
||||||
*
|
*
|
||||||
* `SchemeCommand::handle()` writes this to the application's `material-scheme.css`; plan step 40
|
* `SchemeCommand::handle()` writes this to the application's `material-scheme.css`; the shape
|
||||||
* moved the shape itself here so `ErrorPage::fallbackStyles()` and the showcase's stylesheet can
|
* lives here so `ErrorPage::fallbackStyles()` and the showcase's stylesheet can draw the same
|
||||||
* draw the same scheme, from `Scheme::forStylesheet()` rather than a generated file, without a
|
* scheme, from `Scheme::forStylesheet()` rather than a generated file, without a second copy of
|
||||||
* second copy of the selectors to keep in step with the command's.
|
* the selectors to keep in step with the command's.
|
||||||
*/
|
*/
|
||||||
final class SchemeStylesheet
|
final class SchemeStylesheet
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ namespace NoNameWeb\LivewireMaterial\Support;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The package's stylesheet graph as `DesignGuard::missingStylesheets()` and `unusedStylesheets()`
|
* The package's stylesheet graph as `DesignGuard::missingStylesheets()` and `unusedStylesheets()`
|
||||||
* (plan step 41) read it: which files a CSS entry reaches through its relative `@import`s. The
|
* read it: which files a CSS entry reaches through its relative `@import`s. The stylesheets the
|
||||||
* stylesheets the package serves on its own are not bundled here but prebuilt, by
|
* package serves on its own are not bundled here but prebuilt, by `npm run build:stylesheets`
|
||||||
* `npm run build:stylesheets` (bin/stylesheets.mjs), into resources/dist/.
|
* (bin/stylesheets.mjs), into resources/dist/.
|
||||||
*/
|
*/
|
||||||
final class Stylesheets
|
final class Stylesheets
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Every file `$files` reaches, transitively, through their relative `@import`s — real paths,
|
* Every file `$files` reaches, transitively, through their relative `@import`s — real paths,
|
||||||
* `$files` themselves included — without concatenating anything. An application's CSS entry
|
* `$files` themselves included — without concatenating anything. An application's CSS entry
|
||||||
* may import what only a Vite build resolves — a package name (`tailwindcss`), a URL, an import
|
* may import what only a Vite build resolves — a package name (`@fontsource/roboto`), a URL,
|
||||||
* with a `layer()` or media condition — so this never throws: an import naming a URL, an
|
* an import with a `layer()` or media condition — so this never throws: an import naming a
|
||||||
* absolute path or a file that does not exist relative to its stylesheet is skipped, and a
|
* URL, an absolute path or a file that does not exist relative to its stylesheet is skipped,
|
||||||
* conditional import still counts as reaching its file.
|
* and a conditional import still counts as reaching its file.
|
||||||
*
|
*
|
||||||
* @param list<string> $files
|
* @param list<string> $files
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
|
|||||||
+152
-535
@@ -4,7 +4,6 @@ namespace NoNameWeb\LivewireMaterial\Testing;
|
|||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\View\Compilers\ComponentTagCompiler;
|
use Illuminate\View\Compilers\ComponentTagCompiler;
|
||||||
use NoNameWeb\LivewireMaterial\Support\Layout;
|
|
||||||
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
|
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
|
||||||
use NoNameWeb\LivewireMaterial\Support\SvgFile;
|
use NoNameWeb\LivewireMaterial\Support\SvgFile;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
@@ -12,18 +11,16 @@ use SplFileInfo;
|
|||||||
use Symfony\Component\Finder\Finder;
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds what compiles to nothing in a Tailwind-free application (plan step 41), and what a
|
* Finds what compiles to nothing in an application built on this package:
|
||||||
* Tailwind migration still leaves behind:
|
|
||||||
*
|
*
|
||||||
* (i) any Tailwind utility or variant in a view, PHP or JS file — a breakpoint prefix, a
|
* (i) any class a view or PHP file writes that no stylesheet declares — not the
|
||||||
* cleared scale (radius, shadow, type size/weight/leading/tracking, easing, duration), a
|
* application's own (`applicationClasses()`: the `.css` files among the scanned paths and
|
||||||
* layout, spacing, sizing, position, border, effect, interactivity, text or display
|
* whatever the `missingStylesheets()` entry imports outside the package), not the
|
||||||
* utility, a colour utility on a role, white or black (`bg-white`, `text-on-surface/60`)
|
* package's own (`packageClasses()`: every rule under its `resources/css`, `md-*` and the
|
||||||
* or on a colour the application's own Tailwind theme named (`bg-brand`,
|
* showcase's classes alike). So one of the two has to be in reach: a scan of views alone
|
||||||
* `text-sport-run/60`), a variant, or an arbitrary `[…]` value or property — each with its
|
* knows no application class and reports every one of them. A utility a migration left
|
||||||
* 2.0.0 replacement: a layout component and prop (`gap-4` → `gap="space200"`), an `md-*`
|
* behind, a class of a framework that is no longer built, a typo and a class whose rule
|
||||||
* class, or a token for the application's own CSS. A class the application's own
|
* was deleted all read the same here: the class paints nothing.
|
||||||
* stylesheets declare is exempt, and so is every `md-*` class.
|
|
||||||
* (ii) `missingStylesheets($cssEntry)`: a package component tag used in a view — unprefixed,
|
* (ii) `missingStylesheets($cssEntry)`: a package component tag used in a view — unprefixed,
|
||||||
* under the configured prefix, or `<x-livewire-material::…>` — whose stylesheet the
|
* under the configured prefix, or `<x-livewire-material::…>` — whose stylesheet the
|
||||||
* entry's relative `@import` graph does not reach (followed through every package file's
|
* entry's relative `@import` graph does not reach (followed through every package file's
|
||||||
@@ -42,182 +39,36 @@ use Symfony\Component\Finder\Finder;
|
|||||||
* line height, letter spacing, easing or duration, and a media query at a width other
|
* line height, letter spacing, easing or duration, and a media query at a width other
|
||||||
* than 600/840/1200/1600px — each with its token or breakpoint. A `var()`, or a `calc()`,
|
* than 600/840/1200/1600px — each with its token or breakpoint. A `var()`, or a `calc()`,
|
||||||
* `min()`, `max()` or `clamp()` built on one, is never flagged, whatever else it holds.
|
* `min()`, `max()` or `clamp()` built on one, is never flagged, whatever else it holds.
|
||||||
* (iv) Tailwind palette colours, icon names that are not Material Symbols, and Blade
|
* (iv) icon names that are not Material Symbols, and Blade directives written inside a
|
||||||
* directives written inside a component tag (where they do not compile) — plus whatever
|
* component tag (where they do not compile) — plus whatever an application bans on top
|
||||||
* an application bans on top with `forbidColours()` and `forbid()`. These, and the
|
* with `forbidColours()` and `forbid()`, which read a line at a time, whatever it holds.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* A Markdown mail component — a view under a path `mail.markdown.paths` names, Laravel's
|
* A Markdown mail component — a view under a path `mail.markdown.paths` names, Laravel's
|
||||||
* `resources/views/vendor/mail` by default — is drawn by the mail theme, not by the application's
|
* `resources/views/vendor/mail` by default — is drawn by the mail theme, not by the application's
|
||||||
* CSS entry, and the theme's own classes (`table`, `button`, `panel`, `break-all`) share a
|
* CSS entry, so its classes (`table`, `button`, `panel`, `break-all`) are declared where this
|
||||||
* Tailwind utility's name. So family (i) and the breakpoint, scale, palette and colour-value
|
* guard never looks. Family (i) therefore skips those views, while their icon names, the
|
||||||
* checks skip those views, while their icon names, the directives in their component tags and
|
* directives in their component tags and the application's own bans are still read; and a
|
||||||
* the application's own bans are still read; and a stylesheet under that path, a mail theme that
|
* stylesheet under that path, a mail theme that has to write literal values because mail clients
|
||||||
* has to write literal values because mail clients read no custom property, is neither check
|
* read no custom property, is neither check (iii)'s nor a source of declared classes.
|
||||||
* (iii)'s nor a source of exempt classes.
|
|
||||||
*
|
*
|
||||||
* expect(DesignGuard::scan([resource_path('views'), resource_path('js'), resource_path('css'), app_path()])
|
* expect(DesignGuard::scan([resource_path('views'), resource_path('js'), resource_path('css'), app_path()])
|
||||||
* ->missingStylesheets(resource_path('css/app.css'))
|
* ->missingStylesheets(resource_path('css/app.css'))
|
||||||
* ->forbidColours(['tertiary'])
|
* ->forbidColours(['tertiary'])
|
||||||
* ->violations())->toBe([]);
|
* ->violations())->toBe([]);
|
||||||
*
|
*
|
||||||
* Each violation is "path:line what", the path relative to the base path. False positives are
|
* Each violation is "path:line what", the path relative to the base path. Family (i) only ever
|
||||||
* kept low two ways: family (i)'s bare-word checks match only a class already isolated from a
|
* reads a class list a `.php` file writes out — a `class` attribute, Livewire's
|
||||||
* class list in a `.php` file (`class="…"`, `wire:loading.class`, `x-transition:enter`, `:class`,
|
* `wire:loading.class`, Alpine's `x-transition:enter` and `:class`, `@class`, `->class()`,
|
||||||
* `@class`, `->class()`, `Arr::toCssClasses()`, `'class' => '…'`), never a word scanned across a
|
* `Arr::toCssClasses()` and a `'class'` pair — never a word scanned across a whole line, which
|
||||||
* whole line — which keeps "this creates a grid of cards" from matching `grid` — and skip a string
|
* keeps "this creates a grid of cards" from reading as a class. Inside such a list it skips a
|
||||||
* a condition compares (`view === 'grid'`); a line-by-line match (the breakpoint, scale and
|
* string a condition compares (`view === 'grid'`) and one nested deeper than the list itself, an
|
||||||
* colour-value families, which also have to see a class assembled as a plain PHP or JS string, as
|
* array index or a call's argument (`$block['base']`, `in_array($size, ['xs', 'sm'])`). The
|
||||||
* an enum's own literal colour string does) requires the utility's actual shape — a digit, a
|
* trade-off: a class assembled at runtime (`'text-'.$tone`) is invisible to this guard, the
|
||||||
* known scale step or a colour function — never a bare word. The trade-off: a bare-word utility
|
* reason to write class names out whole; and an example of markup written in a PHP comment is
|
||||||
* (`flex`, `hidden`) and a colour of the application's own Tailwind theme (`bg-brand`) are
|
* read like the real thing, since only Blade comments are stripped.
|
||||||
* invisible to this guard anywhere but inside a class list.
|
|
||||||
*/
|
*/
|
||||||
class DesignGuard
|
class DesignGuard
|
||||||
{
|
{
|
||||||
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|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"\']*\]?/';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tailwind's breakpoint prefixes and the M3 window size class each names. Tailwind's
|
|
||||||
* 640/768/1024/1280/1536 are 40–88 px from M3's 600/840/1200/1600, so a prefix maps to the
|
|
||||||
* class that carries the same intent, never to the same pixel: both of its phone breakpoints
|
|
||||||
* are M3's medium. Since 2.0.0 the prefix itself compiles to nothing — there is no Tailwind
|
|
||||||
* left to read it — so the hint points at the layout components' props and a plain media
|
|
||||||
* query instead of another class.
|
|
||||||
*/
|
|
||||||
protected const array WINDOW_CLASSES = [
|
|
||||||
'sm' => 'medium',
|
|
||||||
'md' => 'medium',
|
|
||||||
'lg' => 'expanded',
|
|
||||||
'xl' => 'large',
|
|
||||||
'2xl' => 'extra-large',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Tailwind's radius scale and the M3 corner it replaces each (styles §Shape). */
|
|
||||||
protected const array CORNERS = [
|
|
||||||
'none' => 'none',
|
|
||||||
'xs' => 'xs',
|
|
||||||
'sm' => 'sm',
|
|
||||||
'md' => 'md',
|
|
||||||
'lg' => 'lg',
|
|
||||||
'xl' => 'xl',
|
|
||||||
'2xl' => 'xxl',
|
|
||||||
'3xl' => 'xxl',
|
|
||||||
'4xl' => 'xxl',
|
|
||||||
'full' => 'full',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Tailwind's shadow scale and the M3 elevation level it replaces each (styles §Elevation). */
|
|
||||||
protected const array ELEVATIONS = [
|
|
||||||
'2xs' => 1,
|
|
||||||
'xs' => 1,
|
|
||||||
'sm' => 1,
|
|
||||||
'md' => 2,
|
|
||||||
'lg' => 3,
|
|
||||||
'xl' => 4,
|
|
||||||
'2xl' => 5,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every other Tailwind utility family an application's views wrote, as [pattern, hint] pairs
|
|
||||||
* tried in order on a class-list token (so a bare word like `flex` is only ever matched there).
|
|
||||||
* `%s` in a hint is the token. The spacing, container and text families with a finer hint are
|
|
||||||
* handled before this table (see `tailwindFamilyHint()`); the breakpoint, scale and palette
|
|
||||||
* families are the line-by-line checks'.
|
|
||||||
*/
|
|
||||||
protected const array FAMILIES = [
|
|
||||||
// Tailwind's cleared easing and duration scale: matched only here, in a class list a
|
|
||||||
// Blade or PHP file writes literally (never line-by-line across every file, the way
|
|
||||||
// outsideTheScale() still reads its other scale steps) — a plain PHP or JS string such as
|
|
||||||
// `matchMedia(…) ? 'linear' : 'ease-out'` names a real CSS keyword, not a Tailwind class,
|
|
||||||
// and reading every line for the bare word flagged both that and the word appearing inside
|
|
||||||
// a stylesheet test's own regex literal (`ease-in`, `ease-out`) as if it were one.
|
|
||||||
['/^ease-(?:in-out|linear|in|out)$/', 'value outside the M3 scale `%s` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`'],
|
|
||||||
['/^duration-\d+$/', 'value outside the M3 scale `%s` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`'],
|
|
||||||
['/^[a-z][a-z-]*-\((?<property>--[\w-]+)\)$/', 'Tailwind custom-property utility `%s` compiles to nothing — write `var({property})` in your own CSS'],
|
|
||||||
|
|
||||||
['/^(?:static|fixed|absolute|relative|sticky)$/', "Tailwind position utility `%s` compiles to nothing — write `position` in your own CSS (a FAB goes in `<x-scaffold>`'s `fab` slot)"],
|
|
||||||
['/^-?(?:inset(?:-[xyse])?|top|right|bottom|left|start|end)-(?:\d+(?:\.\d+)?|px|full|auto|\d+\/\d+)$/', 'Tailwind inset utility `%s` compiles to nothing — write the offset in your own CSS, from `var(--md-sys-measurement-space*)` where it is a spacing step'],
|
|
||||||
['/^-?z-(?:\d+|auto)$/', 'Tailwind z-index utility `%s` compiles to nothing — write `z-index` in your own CSS'],
|
|
||||||
['/^(?:overflow|overscroll)(?:-[xy])?-(?:auto|hidden|clip|visible|scroll|contain|none)$/', 'Tailwind overflow utility `%s` compiles to nothing — write `overflow` in your own CSS'],
|
|
||||||
['/^(?:flex-(?:1|auto|initial|none|row-reverse|col-reverse|nowrap|wrap-reverse)|grow(?:-\d+)?|shrink(?:-\d+)?|basis-[\w.\/]+|order-(?:\d+|first|last|none)|contents|grid-(?:rows|cols)-(?:none|subgrid|\d+)|(?:col|row)-(?:span-(?:\d+|full)|start-\d+|end-\d+|auto)|grid-flow-[\w-]+|auto-(?:cols|rows)-[\w]+|justify-(?:around|evenly|stretch|normal|items-[\w-]+|self-[\w-]+)|(?:content|place-content|place-items|place-self|self)-(?:start|end|center|between|around|evenly|stretch|baseline|normal|auto|none))$/', "Tailwind flex/grid item utility `%s` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS"],
|
|
||||||
['/^(?: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]|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|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-]+|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)|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. */
|
|
||||||
protected const array DISPLAY_UTILITY = ['block', 'inline-block', 'inline', 'invisible', 'visible'];
|
|
||||||
|
|
||||||
/** Text utilities with a direct `md-*` replacement (text.css). */
|
|
||||||
protected const array TEXT_LAYOUT_UTILITY = [
|
|
||||||
'text-left' => 'md-text-start',
|
|
||||||
'text-start' => 'md-text-start',
|
|
||||||
'text-center' => 'md-text-center',
|
|
||||||
'text-right' => 'md-text-end',
|
|
||||||
'text-end' => 'md-text-end',
|
|
||||||
'truncate' => 'md-truncate',
|
|
||||||
'line-clamp-2' => 'md-line-clamp-2',
|
|
||||||
'line-clamp-3' => 'md-line-clamp-3',
|
|
||||||
'whitespace-nowrap' => 'md-nowrap',
|
|
||||||
'text-nowrap' => 'md-nowrap',
|
|
||||||
'sr-only' => 'md-visually-hidden',
|
|
||||||
'tabular-nums' => 'md-tabular',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** A colour utility's prefix, up to its dash; `COLOUR_UTILITY` and `THEME_COLOUR_UTILITY` share it. */
|
|
||||||
protected const string COLOUR_PREFIX = '/^(?<utility>bg|text|border(?:-(?:[trblxyse]|bs|be))?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-';
|
|
||||||
|
|
||||||
/** A colour utility's opacity modifier (`/60`, `/[0.32]`, `/(--alpha)`), to the end of the token. */
|
|
||||||
protected const string COLOUR_OPACITY = '(?:\/(?<opacity>\d{1,3}|\[[^\]]*\]|\(--[\w-]+\)))?$/';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A colour utility written on an M3 role, 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 = self::COLOUR_PREFIX
|
|
||||||
.'(?<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|white|black|current|transparent|inherit|initial)'
|
|
||||||
.self::COLOUR_OPACITY;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = self::COLOUR_PREFIX.'(?<name>[a-z][a-z\d]*(?:-[a-z\d]+)*)'.self::COLOUR_OPACITY;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The hooks an application writes on markup of its own, not through a component tag, whose
|
* The hooks an application writes on markup of its own, not through a component tag, whose
|
||||||
* rules live in a component's stylesheet, and that stylesheet (check ii). `data-md-list-row`
|
* rules live in a component's stylesheet, and that stylesheet (check ii). `data-md-list-row`
|
||||||
@@ -249,12 +100,6 @@ class DesignGuard
|
|||||||
'inverse-on-surface' => 'md-ink-inverse',
|
'inverse-on-surface' => 'md-ink-inverse',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** The roles `<x-surface level>` 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`. */
|
/** 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';
|
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';
|
||||||
|
|
||||||
@@ -312,8 +157,7 @@ class DesignGuard
|
|||||||
* Their on-roles and containers are forbidden with them, wherever 2.0.0 lets an application
|
* Their on-roles and containers are forbidden with them, wherever 2.0.0 lets an application
|
||||||
* write one: the `--md-sys-color-*` custom property (a `var()` in its CSS, an inline `style`, a
|
* write one: the `--md-sys-color-*` custom property (a `var()` in its CSS, an inline `style`, a
|
||||||
* script reading it), the `md-ink-*` class text.css has for it, and a component's `color` or
|
* script reading it), the `md-ink-*` class text.css has for it, and a component's `color` or
|
||||||
* `tone` prop (`color="tertiary"`, `:tone="'tertiary'"`). A Tailwind `bg-tertiary` left behind
|
* `tone` prop (`color="tertiary"`, `:tone="'tertiary'"`).
|
||||||
* is family (i)'s to report, as every colour utility is.
|
|
||||||
*
|
*
|
||||||
* @param list<string> $roles
|
* @param list<string> $roles
|
||||||
*/
|
*/
|
||||||
@@ -345,7 +189,8 @@ class DesignGuard
|
|||||||
* its first use. Only the imports are read here: the literal values in the application's own
|
* its first use. Only the imports are read here: the literal values in the application's own
|
||||||
* CSS are check (iii)'s, which reads the `.css` files `scan()` is given, entry or not — so an
|
* CSS are check (iii)'s, which reads the `.css` files `scan()` is given, entry or not — so an
|
||||||
* application part-way through its migration can check its imports before its stylesheets
|
* application part-way through its migration can check its imports before its stylesheets
|
||||||
* are on tokens. The classes the entry's own imports declare do join check (i)'s exemptions.
|
* are on tokens. The classes the entry's own imports declare do count as declared for check
|
||||||
|
* (i), though.
|
||||||
*/
|
*/
|
||||||
public function missingStylesheets(string $cssEntry): static
|
public function missingStylesheets(string $cssEntry): static
|
||||||
{
|
{
|
||||||
@@ -416,10 +261,8 @@ class DesignGuard
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($readsClasses && str_ends_with($file->getFilename(), '.php')) {
|
if ($readsClasses && str_ends_with($file->getFilename(), '.php')) {
|
||||||
foreach ($this->literalClasses($contents) as [$line, $class]) {
|
foreach ($this->undeclaredClasses($contents) as [$line, $class]) {
|
||||||
if (($hint = $this->tailwindFamilyHint($class)) !== null) {
|
$violations[] = "{$where}:{$line} class `{$class}` is declared in no stylesheet — the package declares `md-*`, the rest is your own CSS";
|
||||||
$violations[] = "{$where}:{$line} {$hint}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,13 +293,7 @@ class DesignGuard
|
|||||||
foreach (explode("\n", $contents) as $index => $text) {
|
foreach (explode("\n", $contents) as $index => $text) {
|
||||||
$line = $index + 1;
|
$line = $index + 1;
|
||||||
|
|
||||||
if ($readsClasses && preg_match_all($this->colourPattern(), $text, $matches)) {
|
foreach ($this->forbiddenRoles($text) as $what) {
|
||||||
foreach ($matches[0] as $class) {
|
|
||||||
$violations[] = "{$where}:{$line} Tailwind palette colour `{$class}` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ([...($readsClasses ? $this->offTheTokens($text) : []), ...$this->forbiddenRoles($text)] as $what) {
|
|
||||||
$violations[] = "{$where}:{$line} {$what}";
|
$violations[] = "{$where}:{$line} {$what}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,11 +437,6 @@ class DesignGuard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function colourPattern(): string
|
|
||||||
{
|
|
||||||
return '/(?<![\w-])'.self::UTILITY.'-'.self::PALETTE.'(?![\w-])/';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every place one line names a role `forbidColours()` left out: its `--md-sys-color-*` custom
|
* Every place one line names a role `forbidColours()` left out: its `--md-sys-color-*` custom
|
||||||
* property, or the `md-ink-*` class text.css draws it with.
|
* property, or the `md-ink-*` class text.css draws it with.
|
||||||
@@ -675,320 +507,6 @@ class DesignGuard
|
|||||||
return $found;
|
return $found;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Everything on one line that names a value the theme no longer carries, each with the M3
|
|
||||||
* token or component that replaces it. Line by line like the colour check, so a class inside
|
|
||||||
* a PHP or JS string is seen too, not only one inside a `class` attribute — the reach these
|
|
||||||
* three families have always needed (a plain PHP string returning a stray colour name), and the reason they
|
|
||||||
* stay a line-by-line match rather than moving to the class-token check every newer family
|
|
||||||
* uses (see the class header).
|
|
||||||
*
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
protected function offTheTokens(string $text): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
...$this->breakpointPrefixes($text),
|
|
||||||
...$this->outsideTheScale($text),
|
|
||||||
...$this->colourValues($text),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `sm:`, `max-2xl:` and the rest, stacked variants included. A prefix must be followed at
|
|
||||||
* once by the next variant or the utility itself, so a `md:` that is really an object key
|
|
||||||
* (`md: { … }` in a script) is left alone.
|
|
||||||
*
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
protected function breakpointPrefixes(string $text): array
|
|
||||||
{
|
|
||||||
static $pattern = null;
|
|
||||||
$pattern ??= '/(?<![\w-])(?<max>max-)?(?<name>'.$this->alternation(array_keys(self::WINDOW_CLASSES)).'):(?=[a-z\d!*\[(_-])/';
|
|
||||||
|
|
||||||
preg_match_all($pattern, $text, $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL);
|
|
||||||
|
|
||||||
return array_map(function (array $match): string {
|
|
||||||
$max = $match['max'] ?? '';
|
|
||||||
$name = self::WINDOW_CLASSES[$match['name']];
|
|
||||||
$px = Layout::BREAKPOINTS[$name];
|
|
||||||
$comparison = $max !== '' ? "width < {$px}px" : "width >= {$px}px";
|
|
||||||
|
|
||||||
return "Tailwind breakpoint `{$max}{$match['name']}:` compiles to nothing — M3's {$name} ({$px}px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media ({$comparison})` in your own CSS";
|
|
||||||
}, $matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The radius, shadow, type-size, weight, leading and tracking utilities Tailwind shipped and
|
|
||||||
* M3's own scales replace. The easing and duration steps of the same Tailwind scale are not
|
|
||||||
* read here — see the class header's note by the `ease-*`/`duration-*` entries of `FAMILIES`.
|
|
||||||
*
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
protected function outsideTheScale(string $text): array
|
|
||||||
{
|
|
||||||
static $pattern = null;
|
|
||||||
$pattern ??= '/(?<![\w-])(?:'
|
|
||||||
.'rounded(?<side>-(?:ss|se|ee|es|tl|tr|br|bl|t|r|b|l|s|e))?-(?<corner>'.$this->alternation(array_keys(self::CORNERS)).')'
|
|
||||||
.'|shadow-(?<elevation>'.$this->alternation(array_keys(self::ELEVATIONS)).')'
|
|
||||||
.'|text-(?<size>xs|sm|base|lg|xl|[2-9]xl)'
|
|
||||||
.'|font-(?<weight>thin|extralight|light|normal|medium|semibold|bold|extrabold|black)'
|
|
||||||
.'|leading-(?<leading>none|tight|snug|normal|relaxed|loose|\d+(?:\.\d+)?)'
|
|
||||||
.'|tracking-(?<tracking>tighter|tight|normal|wider|widest|wide)'
|
|
||||||
.')(?![\w-])/';
|
|
||||||
|
|
||||||
preg_match_all($pattern, $text, $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL);
|
|
||||||
|
|
||||||
return array_map(
|
|
||||||
fn (array $match): string => "value outside the M3 scale `{$match[0]}` — ".$this->scaleReplacement($match),
|
|
||||||
$matches,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 2.0.0 replacement for one match of the scale pattern: a token for the application's own
|
|
||||||
* CSS, or (for text) one of the `md-type-*` classes, which set size, line height, weight and
|
|
||||||
* tracking together — never a Tailwind utility, since none compiles any more.
|
|
||||||
*
|
|
||||||
* @param array<array-key, string|null> $match
|
|
||||||
*/
|
|
||||||
protected function scaleReplacement(array $match): string
|
|
||||||
{
|
|
||||||
return match (true) {
|
|
||||||
isset($match['corner']) => 'use `var(--md-sys-shape-corner-'.self::CORNERS[$match['corner']].')` in your own CSS, or `<x-surface corner="'.self::CORNERS[$match['corner']].'">`',
|
|
||||||
isset($match['elevation']) => 'use `var(--md-sys-elevation-'.self::ELEVATIONS[$match['elevation']].')` in your own CSS',
|
|
||||||
isset($match['weight']) => 'use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
default => 'use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Colours written as a value rather than a role: an arbitrary hex, function or mix.
|
|
||||||
*
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
protected function colourValues(string $text): array
|
|
||||||
{
|
|
||||||
preg_match_all(self::ARBITRARY_COLOUR, $text, $matches);
|
|
||||||
|
|
||||||
return array_map(fn (string $class): string => "arbitrary colour `{$class}`, use an M3 role", $matches[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Tailwind-shaped class token's 2.0.0 replacement, or null when the token is not this
|
|
||||||
* guard's to report: it is not Tailwind-shaped at all (an application's own class, an ARIA or
|
|
||||||
* data token, a plain word), it is on the small set nothing here ever flags (`md-*` — the
|
|
||||||
* text classes and the shared interaction hooks), the application's own CSS declares it
|
|
||||||
* (`applicationClasses()`), or a line-by-line check reports it already (a breakpoint prefix, a
|
|
||||||
* cleared scale step, an arbitrary or palette colour). Only ever called with a token
|
|
||||||
* `literalClasses()` already isolated from a class list, which is what makes it safe to match a
|
|
||||||
* bare word like `flex` or `hidden` (see the class header's false-positive note).
|
|
||||||
*/
|
|
||||||
protected function tailwindFamilyHint(string $token): ?string
|
|
||||||
{
|
|
||||||
if ($token === '' || str_starts_with($token, 'md-') || isset($this->applicationClasses()[$token])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = trim($token, '!');
|
|
||||||
|
|
||||||
if (preg_match(self::ARBITRARY_COLOUR, $token) === 1 || preg_match($this->colourPattern(), $token) === 1 || $this->outsideTheScale($token) !== []) {
|
|
||||||
return null; // colourValues(), the palette check and outsideTheScale() already report this, line by line.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^(?<lead>\[[^\]]*\]|[^:\[]+(?:\[[^\]]*\])?):/', $token, $variant) === 1) {
|
|
||||||
$bare = str_starts_with($variant['lead'], 'max-') ? substr($variant['lead'], 4) : $variant['lead'];
|
|
||||||
|
|
||||||
if (isset(self::WINDOW_CLASSES[$bare])) {
|
|
||||||
return null; // breakpointPrefixes() already names the M3 breakpoint for this one.
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Tailwind variant class `{$token}` compiles to nothing — ".$this->variantHint($variant['lead']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^\[(?<property>-{0,2}[a-z][\w-]*):[^\]]+\]$/', $token, $arbitrary) === 1) {
|
|
||||||
return "Tailwind arbitrary property `{$token}` compiles to nothing — write `{$arbitrary['property']}` in your own CSS";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^-?[a-z][\w-]*-\[[^\]]*\]$/', $token) === 1) {
|
|
||||||
return "Tailwind arbitrary value `{$token}` compiles to nothing — write the literal value in your own CSS, or use an M3 token";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (($hint = $this->containerHint($token) ?? $this->spacingHint($token)) !== null) {
|
|
||||||
return $hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($token === 'hidden') {
|
|
||||||
return "Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute (the reset keeps it hidden), or `x-show`";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($token, self::DISPLAY_UTILITY, true)) {
|
|
||||||
return "Tailwind display utility `{$token}` compiles to nothing — write the `display` rule in your own CSS";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset(self::TEXT_LAYOUT_UTILITY[$token])) {
|
|
||||||
return "Tailwind's `{$token}` compiles to nothing — use `".self::TEXT_LAYOUT_UTILITY[$token].'` (text.css)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (($hint = $this->colourUtilityHint($token)) !== null) {
|
|
||||||
return $hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (self::FAMILIES as [$pattern, $hint]) {
|
|
||||||
if (preg_match($pattern, $token, $match) === 1) {
|
|
||||||
return (string) preg_replace_callback(
|
|
||||||
'/\{(\w+)\}/',
|
|
||||||
fn (array $name): string => (string) ($match[$name[1]] ?? ''),
|
|
||||||
str_replace('%%', '%', str_replace('%s', $token, $hint)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 2.0.0 replacement for a Tailwind variant, by its first segment: a state, the theme, a
|
|
||||||
* structural pseudo-class, a direction, an M3 breakpoint, or an arbitrary selector.
|
|
||||||
*/
|
|
||||||
protected function variantHint(string $lead): string
|
|
||||||
{
|
|
||||||
$bare = str_starts_with($lead, 'max-') ? substr($lead, 4) : $lead;
|
|
||||||
|
|
||||||
return match (true) {
|
|
||||||
$bare !== 'compact' && isset(Layout::BREAKPOINTS[$bare]) => '`@media (width '.($bare === $lead ? '>=' : '<').' '.Layout::BREAKPOINTS[$bare]."px)` in your own CSS, or a layout component's `hide-below`/`hide-from`/`stack-below` prop",
|
|
||||||
$lead === 'dark' => "the roles already switch with the theme; a dark-only rule is `[data-theme='dark'] …` in your own CSS",
|
|
||||||
$lead === 'rail-collapsed' => "`:root[data-rail='collapsed'] …` in your own CSS",
|
|
||||||
in_array($lead, ['motion-reduce', 'motion-safe'], true) => '`@media (prefers-reduced-motion: reduce)` in your own CSS',
|
|
||||||
in_array($lead, ['rtl', 'ltr'], true) => "`:is([dir='rtl'], [dir='rtl'] *)` in your own CSS",
|
|
||||||
$lead === 'print' => '`@media print` in your own CSS',
|
|
||||||
preg_match('/^(?:hover|focus|focus-visible|focus-within|active|pressed)$/', $lead) === 1 => "M3's hover, focus and press states are `md-state-layer` and `md-focus-ring` (interaction.css); any other state rule is `:hover`/`:focus-visible` in your own CSS",
|
|
||||||
preg_match('/^(?:group|peer|has|not|in)(?:-|\/|$)/', $lead) === 1 => '`:has()` or a descendant selector in your own CSS',
|
|
||||||
preg_match('/^(?:aria|data)-/', $lead) === 1 => 'an `[aria-…]` or `[data-…]` attribute selector in your own CSS',
|
|
||||||
preg_match('/^(?:before|after|placeholder|file|marker|selection|backdrop|first-line|first-letter)$/', $lead) === 1 => 'a pseudo-element rule in your own CSS',
|
|
||||||
str_starts_with($lead, '[') => 'write the arbitrary selector as a rule in your own CSS',
|
|
||||||
default => 'write the state or condition as a selector or `@media` rule in your own CSS',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The layout component and prop for a Tailwind flex or grid container utility, or null.
|
|
||||||
*/
|
|
||||||
protected function containerHint(string $token): ?string
|
|
||||||
{
|
|
||||||
$dead = "Tailwind layout utility `{$token}` compiles to nothing";
|
|
||||||
|
|
||||||
return match (true) {
|
|
||||||
in_array($token, ['flex', 'inline-flex', 'flex-row'], true) => "{$dead} — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column",
|
|
||||||
$token === 'flex-col' => "{$dead} — use `<x-stack>` (`gap`, `align`)",
|
|
||||||
$token === 'flex-wrap' => "{$dead} — use `<x-row wrap>`",
|
|
||||||
in_array($token, ['grid', 'inline-grid'], true) => "{$dead} — use `<x-grid>` (`:columns` per breakpoint, `gap`, `min-item`), or `<x-feed>` for a grid of cards",
|
|
||||||
preg_match('/^grid-cols-(\d+)$/', $token, $m) === 1 => "{$dead} — use `<x-grid :columns=\"{$m[1]}\">`, or a per-breakpoint map (`:columns=\"['compact' => 1, 'medium' => {$m[1]}]\"`)",
|
|
||||||
preg_match('/^items-(start|end|center|stretch)$/', $token, $m) === 1 => "{$dead} — use `align=\"{$m[1]}\"` on `<x-row>` or `<x-stack>`",
|
|
||||||
$token === 'items-baseline' => "{$dead} — use `align=\"baseline\"` on `<x-row>`",
|
|
||||||
preg_match('/^justify-(start|end|center|between)$/', $token, $m) === 1 => "{$dead} — use `justify=\"{$m[1]}\"` on `<x-row>`",
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The M3 spacing step for a Tailwind gap, padding, margin or space-between utility, or null.
|
|
||||||
* Tailwind's spacing unit is 4px (`gap-4` is 16px, `space200`); a value between two of M3's
|
|
||||||
* steps names both neighbours.
|
|
||||||
*/
|
|
||||||
protected function spacingHint(string $token): ?string
|
|
||||||
{
|
|
||||||
if (preg_match('/^(?<negative>-)?(?<kind>gap(?:-[xy])?|[pm][trblxyse]?|space-[xy])-(?<value>\d+(?:\.\d+)?|px|auto|reverse)$/', $token, $m) !== 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$dead = "Tailwind spacing utility `{$token}` compiles to nothing";
|
|
||||||
|
|
||||||
if ($m['value'] === 'auto') {
|
|
||||||
return in_array($m['kind'], ['m', 'mx'], true)
|
|
||||||
? "{$dead} — `<x-pane width>` centres its content; otherwise `margin-inline: auto` in your own CSS"
|
|
||||||
: "{$dead} — an `auto` margin is a rule in your own CSS";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($m['value'] === 'reverse') {
|
|
||||||
return "{$dead} — a reversed row is a `flex-direction` rule in your own CSS";
|
|
||||||
}
|
|
||||||
|
|
||||||
$px = $m['value'] === 'px' ? 1.0 : (float) $m['value'] * 4;
|
|
||||||
$steps = array_combine(Layout::SPACING, array_map(
|
|
||||||
fn (string $step): float => (float) substr($step, 5) / 100 * 8,
|
|
||||||
Layout::SPACING,
|
|
||||||
));
|
|
||||||
$exact = array_search($px, $steps, true);
|
|
||||||
$below = array_key_last(array_filter($steps, fn (float $step): bool => $step < $px));
|
|
||||||
$above = array_key_first(array_filter($steps, fn (float $step): bool => $step > $px));
|
|
||||||
$pxText = rtrim(rtrim(number_format($px, 2, '.', ''), '0'), '.').'px';
|
|
||||||
|
|
||||||
$choice = fn (string $attribute): string => $exact !== false
|
|
||||||
? "`{$attribute}=\"{$exact}\"` ({$pxText})"
|
|
||||||
: implode(' or ', array_map(
|
|
||||||
fn (string $step): string => "`{$attribute}=\"{$step}\"` (".rtrim(rtrim(number_format($steps[$step], 2, '.', ''), '0'), '.').'px)',
|
|
||||||
array_values(array_filter([$below, $above])),
|
|
||||||
))." — {$pxText} is not an M3 spacing step";
|
|
||||||
$token = $exact !== false ? "`var(--md-sys-measurement-{$exact})`" : '`var(--md-sys-measurement-space*)`';
|
|
||||||
|
|
||||||
return match (true) {
|
|
||||||
$px === 0.0 => "{$dead} — leave it out: the reset zeroes margins and padding",
|
|
||||||
$m['negative'] === '-' => "{$dead} — a negative margin is `calc(-1 * ".trim($token, '`').')` in your own CSS',
|
|
||||||
str_starts_with($m['kind'], 'gap') => "{$dead} — use ".$choice('gap').' on `<x-row>`, `<x-stack>`, `<x-grid>` or `<x-feed>`',
|
|
||||||
$m['kind'] === 'space-y' => "{$dead} — use `<x-stack>` with ".$choice('gap'),
|
|
||||||
$m['kind'] === 'space-x' => "{$dead} — use `<x-row>` with ".$choice('gap'),
|
|
||||||
str_starts_with($m['kind'], 'p') => "{$dead} — use ".$choice('padding')." on `<x-surface>`, or {$token} in your own CSS",
|
|
||||||
default => "{$dead} — space between siblings is a layout component's ".$choice('gap')."; any other margin is {$token} in your own CSS",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 `<x-divider>` or `<x-surface outlined>`, a
|
|
||||||
* tonal background `<x-surface level>`, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$utility = $m['utility'];
|
|
||||||
$role = $m['role'];
|
|
||||||
$dead = "Tailwind colour utility `{$token}` compiles to nothing";
|
|
||||||
|
|
||||||
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', '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]|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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check (iii): literal design values in the application's own CSS. `var(--md-sys-…)` and
|
* Check (iii): literal design values in the application's own CSS. `var(--md-sys-…)` and
|
||||||
* `calc()` are blanked out first (see `withoutTokenFunctions()`), so a value built from tokens
|
* `calc()` are blanked out first (see `withoutTokenFunctions()`), so a value built from tokens
|
||||||
@@ -1285,14 +803,15 @@ class DesignGuard
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every class written out literally: in `class="…"`, Livewire's `wire:loading.class="…"` and
|
* Every class written out literally: in a `class` attribute, in Livewire's
|
||||||
* Alpine's `x-transition:enter="…"` lists, and in the strings of `:class`/`x-bind:class` (its
|
* `wire:loading.class` and Alpine's `x-transition:enter` lists, and in the strings of
|
||||||
* object keys too, quoted or not), `@class([...])`, `->class(...)`, `Arr::toCssClasses([...])`
|
* `:class`/`x-bind:class` (its object keys too, quoted or not), `@class([...])`,
|
||||||
* and a `'class' => '…'` pair. A string compared in a condition (`view === 'grid'`,
|
* `->class(...)`, `Arr::toCssClasses([...])` and a `'class'` pair. A string compared in a
|
||||||
* `$status === 'hidden'`) or passed to a call (`isActive('grid')`) is not a class; echoes
|
* condition (`view === 'grid'`, `$status === 'hidden'`) is not a class, and neither is one
|
||||||
* inside a list are skipped. A token is returned exactly as written, variant prefix (`sm:`,
|
* nested deeper than the list itself — an array index or a call's argument, which is where
|
||||||
* `hover:`) and all, since `tailwindFamilyHint()` reports the prefix itself, with the line it
|
* `$block['base']`, `$header['class']` and `in_array($size, ['xs', 'sm'])` keep their strings;
|
||||||
* sits on.
|
* echoes inside a list are skipped. A token is returned exactly as written, prefix and all,
|
||||||
|
* with the line it sits on.
|
||||||
*
|
*
|
||||||
* @return list<array{0: int, 1: string}>
|
* @return list<array{0: int, 1: string}>
|
||||||
*/
|
*/
|
||||||
@@ -1312,6 +831,13 @@ class DesignGuard
|
|||||||
$alpine = $binding['alpine'][0] !== null;
|
$alpine = $binding['alpine'][0] !== null;
|
||||||
[$body, $offset] = $binding['alpine'][0] !== null ? $binding['alpine'] : ($binding['php'][0] !== null ? $binding['php'] : $binding['pair']);
|
[$body, $offset] = $binding['alpine'][0] !== null ? $binding['alpine'] : ($binding['php'][0] !== null ? $binding['php'] : $binding['pair']);
|
||||||
|
|
||||||
|
// `:class` on a component is PHP, so it may wrap the very helpers `directiveInTag()`
|
||||||
|
// recommends: the array inside is the list, the call around it is not.
|
||||||
|
if ($alpine && preg_match('/^\s*[^\s(]*(?:Arr::toCssClasses|->class)\(\s*(\[.*\])\s*\)\s*$/s', $body, $call, PREG_OFFSET_CAPTURE) === 1) {
|
||||||
|
[$body, $offset] = [$call[1][0], $offset + $call[1][1]];
|
||||||
|
$alpine = false;
|
||||||
|
}
|
||||||
|
|
||||||
preg_match_all($alpine ? "/'([^']*)'|(?<=[{,])\\s*([A-Za-z_][\\w]*)\\s*:/" : "/'([^']*)'|\"([^\"]*)\"/", $body, $strings, PREG_OFFSET_CAPTURE | PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL);
|
preg_match_all($alpine ? "/'([^']*)'|(?<=[{,])\\s*([A-Za-z_][\\w]*)\\s*:/" : "/'([^']*)'|\"([^\"]*)\"/", $body, $strings, PREG_OFFSET_CAPTURE | PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL);
|
||||||
|
|
||||||
foreach ($strings as $string) {
|
foreach ($strings as $string) {
|
||||||
@@ -1319,7 +845,9 @@ class DesignGuard
|
|||||||
$before = substr($body, 0, $string[0][1]);
|
$before = substr($body, 0, $string[0][1]);
|
||||||
$after = substr($body, $string[0][1] + strlen($string[0][0]));
|
$after = substr($body, $string[0][1] + strlen($string[0][0]));
|
||||||
|
|
||||||
if (preg_match('/(?:[=!]==?|[<>]=?|\(|\?\?)\s*$/', $before) === 1 || preg_match('/^\s*(?:[=!]==?|[<>]=?)/', $after) === 1) {
|
if (preg_match('/(?:[=!]==?|[<>]=?|\(|\?\?)\s*$/', $before) === 1
|
||||||
|
|| preg_match('/^\s*(?:[=!]==?|[<>]=?)/', $after) === 1
|
||||||
|
|| $this->readsAValue($before)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1343,6 +871,57 @@ class DesignGuard
|
|||||||
return $found;
|
return $found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check (i): every class `literalClasses()` reads out of `$contents` that neither the
|
||||||
|
* application's own stylesheets nor the package's declare — a rule for it exists nowhere, so
|
||||||
|
* the class paints nothing, whatever it was once meant to do.
|
||||||
|
*
|
||||||
|
* @return list<array{0: int, 1: string}>
|
||||||
|
*/
|
||||||
|
protected function undeclaredClasses(string $contents): array
|
||||||
|
{
|
||||||
|
$found = [];
|
||||||
|
|
||||||
|
foreach ($this->literalClasses($contents) as [$line, $class]) {
|
||||||
|
if ($class === '' || isset($this->applicationClasses()[$class]) || isset($this->packageClasses()[$class])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$found[] = [$line, $class];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the string that follows `$before` — the text of a class list up to it — is a value
|
||||||
|
* the list reads rather than a class it writes: a call's argument (`in_array($size, ['xs',
|
||||||
|
* 'sm'])`) or an array index (`$block['base']`, `$header['class']`). Both open a delimiter
|
||||||
|
* that is still unclosed where the string sits, so this walks the text and keeps what is open:
|
||||||
|
* a `(` right after a name is a call, one after anything else groups an expression
|
||||||
|
* (`$cond ? 'a' : ($other ? 'b' : 'c')`), and a `[` right after a name, a `]` or a `)` reads
|
||||||
|
* an index, while any other `[` opens an array of classes.
|
||||||
|
*/
|
||||||
|
protected function readsAValue(string $before): bool
|
||||||
|
{
|
||||||
|
$open = [];
|
||||||
|
$length = strlen($before);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; $i++) {
|
||||||
|
$previous = rtrim(substr($before, 0, $i));
|
||||||
|
$name = $previous !== '' && preg_match('/[\w\]\)]$/', $previous) === 1;
|
||||||
|
|
||||||
|
match ($before[$i]) {
|
||||||
|
'(' => $open[] = $name ? 'call' : 'group',
|
||||||
|
'[' => $open[] = $name ? 'index' : 'array',
|
||||||
|
')', ']' => array_pop($open),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array('call', $open, true) || in_array('index', $open, true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Literal symbol names that do not exist: `<x-icon name="…">`, and an `icon="…"` or
|
* Literal symbol names that do not exist: `<x-icon name="…">`, and an `icon="…"` or
|
||||||
* `icon-right="…"` on any component.
|
* `icon-right="…"` on any component.
|
||||||
@@ -1686,9 +1265,7 @@ class DesignGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Every class an application's own stylesheets select on — the scanned ones and whatever the
|
* Every class an application's own stylesheets select on — the scanned ones and whatever the
|
||||||
* CSS entry imports outside the package — check (i)'s exemption list, so a class it defines is
|
* CSS entry imports outside the package — one half of what check (i) counts as declared.
|
||||||
* never reported as a dead Tailwind utility just because it happens to share a shape with one.
|
|
||||||
* An escaped selector (`.sm\:flex`) counts under its unescaped name.
|
|
||||||
*
|
*
|
||||||
* @return array<string, true>
|
* @return array<string, true>
|
||||||
*/
|
*/
|
||||||
@@ -1710,18 +1287,57 @@ class DesignGuard
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
$css = $this->maskedCss((string) file_get_contents($file));
|
$classes = [...$classes, ...$this->classSelectors((string) file_get_contents($file))];
|
||||||
|
|
||||||
preg_match_all('/(?<![\w.#\\\\-])\.(-?(?:[a-zA-Z_]|\\\\.)(?:[\w-]|\\\\.)*)/', $css, $matches);
|
|
||||||
|
|
||||||
foreach ($matches[1] as $class) {
|
|
||||||
$classes[stripslashes($class)] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->applicationClassesCache = $classes;
|
return $this->applicationClassesCache = $classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every class the package's own `resources/css` declares — `md-*`, the component rules and the
|
||||||
|
* showcase's own classes alike — the other half of check (i)'s "declared". Whether an
|
||||||
|
* application imports the stylesheet that holds the rule is check (ii)'s question, not this
|
||||||
|
* one's. Cached for the process: the package's CSS is the same for every scan in it.
|
||||||
|
*
|
||||||
|
* @return array<string, true>
|
||||||
|
*/
|
||||||
|
protected function packageClasses(): array
|
||||||
|
{
|
||||||
|
static $classes = null;
|
||||||
|
|
||||||
|
if ($classes !== null) {
|
||||||
|
return $classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
$classes = [];
|
||||||
|
$root = static::packagePath('css');
|
||||||
|
|
||||||
|
foreach ($root === '' ? [] : Finder::create()->files()->in($root)->name('*.css') as $file) {
|
||||||
|
$classes = [...$classes, ...$this->classSelectors((string) file_get_contents($file->getPathname()))];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The classes `$css` selects on, by their unescaped name (`.sm\:flex` counts as `sm:flex`).
|
||||||
|
* Comments and strings are blanked out first, so `content: ".flex"` declares nothing.
|
||||||
|
*
|
||||||
|
* @return array<string, true>
|
||||||
|
*/
|
||||||
|
protected function classSelectors(string $css): array
|
||||||
|
{
|
||||||
|
preg_match_all('/(?<![\w.#\\\\-])\.(-?(?:[a-zA-Z_]|\\\\.)(?:[\w-]|\\\\.)*)/', $this->maskedCss($css), $matches);
|
||||||
|
|
||||||
|
$classes = [];
|
||||||
|
|
||||||
|
foreach ($matches[1] as $class) {
|
||||||
|
$classes[stripslashes($class)] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $classes;
|
||||||
|
}
|
||||||
|
|
||||||
protected function isGeneratedScheme(string $file): bool
|
protected function isGeneratedScheme(string $file): bool
|
||||||
{
|
{
|
||||||
if (basename($file) === 'material-scheme.css') {
|
if (basename($file) === 'material-scheme.css') {
|
||||||
@@ -1733,7 +1349,8 @@ class DesignGuard
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether `$file` sits inside this package's own `resources/css` — the application's stylesheets
|
* Whether `$file` sits inside this package's own `resources/css` — the application's stylesheets
|
||||||
* (check iii) and its exempt classes never come from the package's own rules.
|
* (check iii) never come from the package's own rules, and its own classes are
|
||||||
|
* `packageClasses()`, read separately.
|
||||||
*/
|
*/
|
||||||
protected static function isPackageFile(string $file): bool
|
protected static function isPackageFile(string $file): bool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -687,11 +687,9 @@ it('toggles each choice of a multi selection group, down to none', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browser tests owed from docs/plans/material-3-browser-tests.md § Actions (Phase E), written on
|
* The tests below: a submenu opened and closed by keyboard or hover, a menu's filter field, its
|
||||||
* the data-md-* hooks of plan step 36's actions stream: submenu keyboard and hover-open, the
|
* sheet-at-compact form below 600px, a long menu's scroll and the FAB menu's own scroll behind
|
||||||
* filtering menu, the sheet-at-compact list, a long menu's scroll, and the FAB menu's own scroll
|
* its fixed close button. `LongMenuProbe` and `SheetMenuProbe` back the last three of those.
|
||||||
* behind its fixed close button. Written but not run — the actions group's Chromium run happens
|
|
||||||
* once every stream in the group has landed.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** A Livewire component whose menu has more items than the popover's 288px cap can show. */
|
/** A Livewire component whose menu has more items than the popover's 288px cap can show. */
|
||||||
@@ -872,10 +870,10 @@ it('filters a menu\'s items as its field is typed into, moves the highlight with
|
|||||||
|
|
||||||
it('opens a sheet-at-compact menu below 600px, focused on its first item, and the popover from 600px', function () {
|
it('opens a sheet-at-compact menu below 600px, focused on its first item, and the popover from 600px', function () {
|
||||||
$trigger = '#menus button:has-text("Photo")';
|
$trigger = '#menus button:has-text("Photo")';
|
||||||
// The sheet is <x-bottom-sheet>, rewritten (plan step 36) but still x-show-driven — its CSS
|
// The sheet is <x-bottom-sheet>, still x-show-driven — its CSS now animates open/close
|
||||||
// now animates open/close through @starting-style and transition-behavior: allow-discrete
|
// through @starting-style and transition-behavior: allow-discrete rather than Alpine's
|
||||||
// rather than Alpine's x-transition classes, but x-show still owns the element's display, so
|
// x-transition classes, but x-show still owns the element's display, so its openness shows
|
||||||
// its openness shows as visibility, not the Popover API's :popover-open.
|
// as visibility, not the Popover API's :popover-open.
|
||||||
$dialog = '[role="dialog"][aria-label="Photo actions"]';
|
$dialog = '[role="dialog"][aria-label="Photo actions"]';
|
||||||
$addToAlbum = '#menus [role="menuitem"]:has-text("Add to album")';
|
$addToAlbum = '#menus [role="menuitem"]:has-text("Add to album")';
|
||||||
|
|
||||||
|
|||||||
@@ -187,9 +187,9 @@ it('scrolls instantly and leaves every item unmasked under reduced motion', func
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('brings an item cut off at the row\'s edge into view when it is pressed under reduced motion', function () {
|
it('brings an item cut off at the row\'s edge into view when it is pressed under reduced motion', function () {
|
||||||
// Owed by the Chromium baseline (step 32, docs/plans/material-3-browser-tests.md): reduced
|
// Under reduced motion, the row writes a zero inset for every item, so a mask cannot say an
|
||||||
// motion writes a zero inset for every item, so a mask cannot say an item is only partly
|
// item is only partly shown; carousel.js reads the item's box against the row's instead, as
|
||||||
// shown; carousel.js reads the item's box against the row's instead, as for multi-aspect.
|
// for multi-aspect.
|
||||||
$page = carouselShowcase(['reducedMotion' => 'reduce'])
|
$page = carouselShowcase(['reducedMotion' => 'reduce'])
|
||||||
->assertScript(onCarousel(0, <<<'JS'
|
->assertScript(onCarousel(0, <<<'JS'
|
||||||
const row = scroller.getBoundingClientRect()
|
const row = scroller.getBoundingClientRect()
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ use Livewire\Component;
|
|||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The central claims of the `@layer material` design (plan step 42, docs/plans/material-3-alignment.md
|
* The central claims of the `@layer material` design — components carry their styles as
|
||||||
* Decisions § "Components carry their styles as `data-md-*` attributes inside `@layer material`"),
|
* `data-md-*` attributes inside `@layer material` — each on its own small probe page: an
|
||||||
* each on its own small probe page: an application's unlayered rule beats a component default;
|
* application's unlayered rule beats a component default; `[hidden]` beats a component's
|
||||||
* `[hidden]` beats a component's `display`; the layer order holds even when a component file is
|
* `display`; the layer order holds even when a component file is imported before the foundation;
|
||||||
* imported before the foundation; a `data-md-*` attribute survives a Livewire `$refresh`; an icon
|
* a `data-md-*` attribute survives a Livewire `$refresh`; an icon mirrors in a right-to-left
|
||||||
* mirrors in a right-to-left document. Every other stylesheet claim (layers, imports, breakpoints,
|
* document. Every other stylesheet claim (layers, imports, breakpoints) is StylesheetsTest.php's,
|
||||||
* no Tailwind) is StylesheetsTest.php's, asserted on the source rather than a rendered page.
|
* asserted on the source rather than a rendered page.
|
||||||
*/
|
*/
|
||||||
function cascadeReady(mixed $page): mixed
|
function cascadeReady(mixed $page): mixed
|
||||||
{
|
{
|
||||||
@@ -71,8 +71,8 @@ it('lets [hidden] beat a component\'s display', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the layer order even when a component file is imported before the foundation', function () {
|
it('keeps the layer order even when a component file is imported before the foundation', function () {
|
||||||
// Every package stylesheet opens with the same full `@layer` statement (plan step 33), so the
|
// Every package stylesheet opens with the same full `@layer` statement, so the order it
|
||||||
// order it establishes holds whichever file a page reaches first — the error layout's copy of
|
// establishes holds whichever file a page reaches first — the error layout's copy of
|
||||||
// it, reached here before the foundation's. The two prebuilt error-page stylesheets, in that
|
// it, reached here before the foundation's. The two prebuilt error-page stylesheets, in that
|
||||||
// deliberate order: the layout's bundle, whose button.css rules sit in `material.components`,
|
// deliberate order: the layout's bundle, whose button.css rules sit in `material.components`,
|
||||||
// physically precedes the fallback's foundation reset, in `material.reset` — and still loses
|
// physically precedes the fallback's foundation reset, in `material.reset` — and still loses
|
||||||
|
|||||||
@@ -1077,9 +1077,8 @@ it('slides the side sheet in from its own edge in a right-to-left page', functio
|
|||||||
|
|
||||||
// `side="end"` (the default) is the trailing edge — the *left* of a right-to-left page — so it
|
// `side="end"` (the default) is the trailing edge — the *left* of a right-to-left page — so it
|
||||||
// slides in from off the left, not the right (the CSS mirror reads `[dir='rtl']` rather than
|
// slides in from off the left, not the right (the CSS mirror reads `[dir='rtl']` rather than
|
||||||
// `:dir(rtl)`, plan step 39 — Vite's own minifier rewrote that to a `:lang()` list, same as
|
// `:dir(rtl)`, because Vite's own minifier rewrites that to a `:lang()` list): caught well
|
||||||
// Tailwind's build did): caught well before the 500ms spatial transition ends, it is still
|
// before the 500ms spatial transition ends, it is still off-screen on the left.
|
||||||
// off-screen on the left.
|
|
||||||
$page->wait(0.05);
|
$page->wait(0.05);
|
||||||
|
|
||||||
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ use Livewire\Component;
|
|||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The layout components at M3's breakpoints (plan step 35): each on a page of its own, measured at
|
* The layout components at M3's breakpoints: each on a page of its own, measured at the pixel
|
||||||
* the pixel either side of the breakpoint it changes at. The window is resized after the page has
|
* either side of the breakpoint it changes at. The window is resized after the page has loaded,
|
||||||
* loaded, which is what a person does; a media query answers at once, and a script listening to
|
* which is what a person does; a media query answers at once, and a script listening to one
|
||||||
* one (resources/js/layout.js) hears a change event.
|
* (resources/js/layout.js) hears a change event.
|
||||||
*/
|
*/
|
||||||
function layoutPage(string $body, int $width, int $height = 900, string $dir = 'ltr'): mixed
|
function layoutPage(string $body, int $width, int $height = 900, string $dir = 'ltr'): mixed
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ it('lifts the snackbar and the page above something docked on the phone\'s bar',
|
|||||||
* A page for the navigation bar and rail props the shell's own adaptive rail never exercises:
|
* A page for the navigation bar and rail props the shell's own adaptive rail never exercises:
|
||||||
* `tall`, `hide-on-scroll` (with a pinned snackbar), a narrow centred rail, and a rail that hides
|
* `tall`, `hide-on-scroll` (with a pinned snackbar), a narrow centred rail, and a rail that hides
|
||||||
* when collapsed. A wide, fixed viewport throughout, so none of M3's breakpoints interfere with
|
* when collapsed. A wide, fixed viewport throughout, so none of M3's breakpoints interfere with
|
||||||
* what each example's own props already decide (docs/plans/material-3-browser-tests.md § Navigation).
|
* what each example's own props already decide.
|
||||||
*/
|
*/
|
||||||
function navigationExtrasProbe(): mixed
|
function navigationExtrasProbe(): mixed
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The texts Boost copies into an application: the two guidelines every session reads, and the two
|
* The texts Boost copies into an application: the two guidelines every session reads, and the two
|
||||||
* skills. Each teaches the vocabulary an agent then writes, so none may name a class or variant
|
* skills. A fenced Blade, HTML, PHP or JS block goes through the design guard whole, so none may
|
||||||
* the package's stylesheets do not define.
|
* write a class no stylesheet declares; in prose, only an inline span that reads as an `md-*`
|
||||||
|
* class list is checked (`boostSpanIsClassList()`), so a name from another vocabulary — a prop, a
|
||||||
|
* role, a config key — is never held to that standard.
|
||||||
*/
|
*/
|
||||||
const BOOST_TEXTS = [
|
const BOOST_TEXTS = [
|
||||||
'resources/boost/guidelines/core.blade.php',
|
'resources/boost/guidelines/core.blade.php',
|
||||||
@@ -16,16 +17,10 @@ const BOOST_TEXTS = [
|
|||||||
'resources/boost/skills/material-3-design/SKILL.md',
|
'resources/boost/skills/material-3-design/SKILL.md',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* The one section that names Tailwind's utilities on purpose: the design guard's table of what it
|
|
||||||
* reports, each beside its replacement.
|
|
||||||
*/
|
|
||||||
const BOOST_SECTIONS_NAMING_WHAT_FAILS = ['Testing the design'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A text's code, line for line, as the design guard reads an application: fenced Blade, HTML, PHP
|
* A text's code, line for line, as the design guard reads an application: fenced Blade, HTML, PHP
|
||||||
* and JS into a view, fenced CSS into a stylesheet, and every inline span that reads as a class
|
* and JS into a view, fenced CSS and an inline rule into a stylesheet, and every inline span that
|
||||||
* list (`bg-primary`, `medium:hidden`, `md-ink-variant`) as a `class` attribute on its own line.
|
* reads as a class list (`md-ink-variant md-truncate`) as a `class` attribute on its own line.
|
||||||
* Prose, props, attributes and custom properties are left out.
|
* Prose, props, attributes and custom properties are left out.
|
||||||
*
|
*
|
||||||
* @return array{view: string, css: string}
|
* @return array{view: string, css: string}
|
||||||
@@ -35,7 +30,6 @@ function boostTextAsCode(string $text): array
|
|||||||
$view = [];
|
$view = [];
|
||||||
$css = [];
|
$css = [];
|
||||||
$fence = null;
|
$fence = null;
|
||||||
$skipping = false;
|
|
||||||
|
|
||||||
foreach (explode("\n", $text) as $line) {
|
foreach (explode("\n", $text) as $line) {
|
||||||
$viewLine = '';
|
$viewLine = '';
|
||||||
@@ -44,28 +38,28 @@ function boostTextAsCode(string $text): array
|
|||||||
if (preg_match('/^```\s*([a-z]*)/', $line, $open) === 1) {
|
if (preg_match('/^```\s*([a-z]*)/', $line, $open) === 1) {
|
||||||
$fence = $fence === null ? ($open[1] ?: 'text') : null;
|
$fence = $fence === null ? ($open[1] ?: 'text') : null;
|
||||||
} elseif ($fence !== null) {
|
} elseif ($fence !== null) {
|
||||||
if (! $skipping && $fence === 'css') {
|
if ($fence === 'css') {
|
||||||
$cssLine = $line;
|
$cssLine = $line;
|
||||||
} elseif (! $skipping && in_array($fence, ['blade', 'html', 'php', 'js'], true)) {
|
} elseif (in_array($fence, ['blade', 'html', 'php', 'js'], true)) {
|
||||||
$viewLine = $line;
|
$viewLine = $line;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (preg_match('/^## (.+)$/', $line, $heading) === 1) {
|
preg_match_all('/`([^`]+)`/', $line, $spans);
|
||||||
$skipping = in_array($heading[1], BOOST_SECTIONS_NAMING_WHAT_FAILS, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $skipping) {
|
// A span that is a rule of its own (`.sport-run-label { … }`) declares the class
|
||||||
preg_match_all('/`([^`]+)`/', $line, $spans);
|
// the example beside it writes, so it goes on the stylesheet side of the pair.
|
||||||
|
[$rules, $spans] = collect($spans[1])->partition(fn (string $span): bool => preg_match('/^\s*\.[\w-]+[^{]*\{[^}]*\}\s*$/', $span) === 1);
|
||||||
|
|
||||||
$viewLine = collect($spans[1])
|
$cssLine = $rules->implode(' ');
|
||||||
->map(fn (string $span): ?string => match (true) {
|
|
||||||
str_contains($span, '<') => $span,
|
$viewLine = $spans
|
||||||
boostSpanIsClassList($span) => '<div class="'.$span.'"></div>',
|
->map(fn (string $span): ?string => match (true) {
|
||||||
default => null,
|
str_contains($span, '<') => $span,
|
||||||
})
|
boostSpanIsClassList($span) => '<div class="'.$span.'"></div>',
|
||||||
->filter()
|
default => null,
|
||||||
->implode(' ');
|
})
|
||||||
}
|
->filter()
|
||||||
|
->implode(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
$view[] = $viewLine;
|
$view[] = $viewLine;
|
||||||
@@ -76,70 +70,22 @@ function boostTextAsCode(string $text): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The hyphenated names the texts also write alone in a span for something that is not a class:
|
* Whether an inline span reads as a class list: every token in it, and there has to be at least
|
||||||
* every property the package's stylesheets declare (`border-color`), every component prop
|
* one, has to be an `md-*` class — the only vocabulary this check holds prose to. A single bare
|
||||||
* (`placeholder-value`) and every colour role (`outline-variant`). Each is shaped like a colour
|
* word (`outline`, `link`, `hidden`), a property or prop name (`border-color`,
|
||||||
* utility on a name the guard does not know, which is what an application's own theme colour
|
* `placeholder-value`), a colour role (`outline-variant`), a CSS function (`var(…)`) and a shell
|
||||||
* (`bg-brand`) looks like.
|
* command (`php artisan …`) all fail that test on their own, with no list of names to keep here.
|
||||||
*
|
|
||||||
* @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 …`), 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
|
function boostSpanIsClassList(string $span): bool
|
||||||
{
|
{
|
||||||
$tokens = preg_split('/\s+/', trim($span));
|
$tokens = preg_split('/\s+/', trim($span));
|
||||||
|
|
||||||
if (count($tokens) === 1 && (preg_match('/[-:]/', $tokens[0]) !== 1 || isset(boostNamesThatAreNotClasses()[$tokens[0]]))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^(?:php|npm|npx|composer|git)\b|\b(?:var|calc|color-mix)\(|^[a-z-]+:\s/', $span) === 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($tokens as $token) {
|
foreach ($tokens as $token) {
|
||||||
if (preg_match('/^(?:--|data-|aria-|wire:|x-|@|\$)/', $token) === 1 || str_contains($token, '::')) {
|
// Every class the package declares is `md-*`, so that prefix is what tells a class list
|
||||||
return false;
|
// from the prop values, slot names and config keys the prose lists the same way
|
||||||
}
|
// (`top-start`, `rail-header`, `livewire-material.profiles`). A class of an application's
|
||||||
|
// own that an example names is nobody's to declare here.
|
||||||
if (preg_match('/^[a-z0-9][a-z0-9:\/.\[\]()#%_-]*$/', $token) !== 1) {
|
if (preg_match('/^(?:[a-z][\w-]*:)*md-[a-z\d-]+$/', $token) !== 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Breakpoints are M3's window size classes and only those: compact below `medium`, then medium 600,
|
|
||||||
* expanded 840, large 1200 and extra-large 1600 (docs/reference/m3/foundations.md § Layout,
|
|
||||||
* foundations-supplement.md § Breakpoints). Tailwind is gone from the whole stack (plan step 39),
|
|
||||||
* so a leftover `sm:` class or a media query at one of its screen widths no longer compiles to
|
|
||||||
* nothing — a browser just as silently ignores the invalid class or the wrong-width rule, so the
|
|
||||||
* mistake is exactly as easy to miss. These two scans are the guard: one for a Tailwind-shaped
|
|
||||||
* prefix (a habit, or an example pasted from elsewhere), one for the media queries a stylesheet or
|
|
||||||
* a script writes by hand.
|
|
||||||
*
|
|
||||||
* @return list<SplFileInfo>
|
|
||||||
*/
|
|
||||||
function breakpointSources(): array
|
|
||||||
{
|
|
||||||
return collect(['views', 'css', 'js'])
|
|
||||||
->flatMap(fn (string $directory): array => File::allFiles(__DIR__.'/../../resources/'.$directory))
|
|
||||||
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'css', 'js'], true))
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every line of a file that matches, as `path:line: the line`.
|
|
||||||
*
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
function breakpointOffenders(SplFileInfo $file, string $pattern): array
|
|
||||||
{
|
|
||||||
$lines = preg_split('/\R/', $file->getContents());
|
|
||||||
|
|
||||||
return collect($lines)
|
|
||||||
->filter(fn (string $line): bool => preg_match($pattern, $line) === 1)
|
|
||||||
->map(fn (string $line, int $index): string => $file->getRelativePathname().':'.($index + 1).': '.trim($line))
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
it('writes no Tailwind breakpoint prefix in any view, stylesheet or script', function () {
|
|
||||||
// A prefix, not a name: `--radius-corner-sm:` and slider.js's `sm: { corner: 8 }` are excluded
|
|
||||||
// by the character before and the one after, and `@md:` is a container query, a different
|
|
||||||
// thing — a component sized by the room it has, not by the window.
|
|
||||||
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
|
|
||||||
|
|
||||||
$offenders = collect(breakpointSources())
|
|
||||||
->flatMap(fn (SplFileInfo $file): array => breakpointOffenders($file, $pattern))
|
|
||||||
->values();
|
|
||||||
|
|
||||||
expect($offenders->all())->toBe([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('writes no media query at a Tailwind screen width', function () {
|
|
||||||
// 40rem/640px, 48rem/768px, 64rem/1024px, 80rem/1280px and the 39.99rem that stood for "below
|
|
||||||
// 640" — none of them is an M3 boundary. The M3 ones are 37.5, 52.5, 75 and 100rem, and a
|
|
||||||
// script asks resources/js/breakpoints.js for them instead of writing its own string.
|
|
||||||
$widths = '/(?<![\d.])(?:40|48|64|80|39\.99)rem|(?<![\d.])(?:640|768|1024|1280)px/';
|
|
||||||
|
|
||||||
$offenders = collect(breakpointSources())
|
|
||||||
->flatMap(function (SplFileInfo $file) use ($widths): array {
|
|
||||||
// Only the condition of a query, so `max-width: min(40rem, 70dvh)` — a real width, not
|
|
||||||
// a breakpoint — and a 640px cap on a bottom sheet are left alone.
|
|
||||||
preg_match_all('/@(?:media|container)\b[^{]*|matchMedia\(\s*[\'"`][^\'"`]*/', $file->getContents(), $matches);
|
|
||||||
|
|
||||||
return collect($matches[0])
|
|
||||||
->filter(fn (string $condition): bool => preg_match($widths, $condition) === 1)
|
|
||||||
->map(fn (string $condition): string => $file->getRelativePathname().': '.trim($condition))
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
})
|
|
||||||
->values();
|
|
||||||
|
|
||||||
expect($offenders->all())->toBe([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('scans the showcase example heredocs too, where the package teaches its own markup', function () {
|
|
||||||
$sections = collect(breakpointSources())
|
|
||||||
->filter(fn (SplFileInfo $file): bool => str_contains($file->getRelativePathname(), 'showcase'.DIRECTORY_SEPARATOR.'sections'))
|
|
||||||
->filter(fn (SplFileInfo $file): bool => str_contains($file->getContents(), "<<<'BLADE'"));
|
|
||||||
|
|
||||||
// Every section still teaches its examples through a heredoc nested inside an @php block
|
|
||||||
// (plan step 38 rewrote what is inside them, not the mechanism), so the two scans above —
|
|
||||||
// which read a file's raw text rather than its parsed Blade or PHP — still reach them.
|
|
||||||
expect($sections)->not->toBeEmpty();
|
|
||||||
|
|
||||||
// The premise itself, proven on a fixture now that plan step 38 has cleared every Tailwind
|
|
||||||
// breakpoint prefix out of the showcase (its own goal, so production content can no longer
|
|
||||||
// carry one): a real file, shaped exactly like a showcase section (an outer heredoc nesting an
|
|
||||||
// inner one, `@php … <<<'BLADE' … BLADE, … @endphp`), still gives up a prefix hiding two
|
|
||||||
// heredocs deep to the same pattern and helper the first test above uses.
|
|
||||||
$directory = sys_get_temp_dir().'/breakpoint-heredoc-probe-'.uniqid();
|
|
||||||
File::ensureDirectoryExists($directory);
|
|
||||||
|
|
||||||
// Built from parts, not written whole: the design guard's own "no Tailwind utility class in
|
|
||||||
// resources/ or tests/" scan (StylesheetsTest.php) reads this file's raw source too, and a
|
|
||||||
// real Tailwind breakpoint prefix sitting in a class attribute here would be exactly what
|
|
||||||
// that scan looks for — the placeholder keeps this file's own source clean of one.
|
|
||||||
File::put($directory.'/probe.blade.php', str_replace('__BREAKPOINT__', 'md:', <<<'OUTER'
|
|
||||||
@php
|
|
||||||
$examples = [
|
|
||||||
'Probe' => <<<'BLADE'
|
|
||||||
<div class="__BREAKPOINT__flex"></div>
|
|
||||||
BLADE,
|
|
||||||
];
|
|
||||||
@endphp
|
|
||||||
OUTER));
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
|
|
||||||
$probe = File::allFiles($directory)[0];
|
|
||||||
|
|
||||||
expect(breakpointOffenders($probe, $pattern))->not->toBeEmpty();
|
|
||||||
} finally {
|
|
||||||
File::deleteDirectory($directory);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A caller's `class` and `style` land on a component's root, untouched and once (the Phase F rule
|
* A caller's `class` and `style` land on a component's root, untouched and once: an application's
|
||||||
* in docs/plans/material-3-alignment-brief.md): an application's width or margin sizes the whole
|
* width or margin sizes the whole component, whichever element its other attributes reach. The
|
||||||
* component, whichever element its other attributes reach. The field family, the pickers and the
|
* field family, the pickers and the components that split their attributes between a root and an
|
||||||
* components that split their attributes between a root and an inner control are the ones that
|
* inner control are the ones that can get it wrong, so each is rendered here with both.
|
||||||
* can get it wrong, so each is rendered here with both.
|
|
||||||
*/
|
*/
|
||||||
it('lands a caller\'s class and style on the root, and only there', function (string $blade) {
|
it('lands a caller\'s class and style on the root, and only there', function (string $blade) {
|
||||||
$html = (string) $this->blade($blade);
|
$html = (string) $this->blade($blade);
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|||||||
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every component and layout-shared stylesheet, rewritten without Tailwind (plan step 36): each
|
* Every component and layout-shared stylesheet: each view renders `data-md-*` attributes and no
|
||||||
* view renders `data-md-*` attributes and no class list of its own beyond the interaction and text
|
* class list of its own beyond the interaction and text classes (tests/Support/ViewClasses.php),
|
||||||
* classes (tests/Support/ViewClasses.php), writes its values from the tokens and its breakpoints as
|
* writes its values from the tokens and its breakpoints as px range queries, and is imported from
|
||||||
* px range queries, and is imported from its own block of all.css. This used to be four near-copies
|
* its own block of all.css. This used to be four near-copies of the same checks, one per group
|
||||||
* of the same checks, one per group (Action/Containment/Input/Navigation), plus a fifth in
|
* (Action/Containment/Input/Navigation), plus a fifth in ErrorPagesTest.php for the error layout —
|
||||||
* ErrorPagesTest.php for the error layout — folded into this one file (plan cleanup) because the
|
* folded into this one file (plan cleanup) because the checks never differed in substance, only in
|
||||||
* checks never differed in substance, only in which names they ran over and, for the token check,
|
* which names they ran over and, for the token check, which box-shadow shape a group's own values
|
||||||
* which box-shadow shape a group's own values may take (see $boxShadow below). `icon` and `shape`
|
* may take (see $boxShadow below). `icon` and `shape` (no group's view renders them exclusively)
|
||||||
* (no group's view renders them exclusively) and `color` (its own ColorTest.php already covers it,
|
* and `color` (its own ColorTest.php already covers it, the shared colour-role table every
|
||||||
* the shared colour-role table every hue-taking component imports) keep their separate treatment.
|
* hue-taking component imports) keep their separate treatment.
|
||||||
*
|
*
|
||||||
* Four names in the dataset are not a component view of their own: `selection` (what checkbox,
|
* Four names in the dataset are not a component view of their own: `selection` (what checkbox,
|
||||||
* radio and toggle share), `navigation-item` (what the bar and the rail's items share), `pagination`
|
* radio and toggle share), `navigation-item` (what the bar and the rail's items share), `pagination`
|
||||||
@@ -103,9 +103,9 @@ dataset('scoped selectors', function (): Generator {
|
|||||||
it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) {
|
it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) {
|
||||||
$css = ComponentStylesheet::read($name);
|
$css = ComponentStylesheet::read($name);
|
||||||
|
|
||||||
// The header, the layer statement, the plain imports and freedom from Tailwind are
|
// The header, the layer statement and the plain imports are StylesheetsTest's, checked over
|
||||||
// StylesheetsTest's, checked over every file all.css reaches; only the block layer and the
|
// every file all.css reaches; only the block layer and the imports' existence are this
|
||||||
// imports' existence are this dataset's own to check.
|
// dataset's own to check.
|
||||||
expect($css->blocks())->each->toBe('@layer material.components');
|
expect($css->blocks())->each->toBe('@layer material.components');
|
||||||
|
|
||||||
foreach ($css->imports() as $import) {
|
foreach ($css->imports() as $import) {
|
||||||
@@ -173,10 +173,10 @@ it('scopes every element-wide selector to a data-md hook', function (string $nam
|
|||||||
// ---- pagination: its views live outside resources/views/components/ --------------------------
|
// ---- pagination: its views live outside resources/views/components/ --------------------------
|
||||||
|
|
||||||
dataset('pagination views', [
|
dataset('pagination views', [
|
||||||
'pagination/laravel/tailwind',
|
'pagination/laravel/material',
|
||||||
'pagination/laravel/simple-tailwind',
|
'pagination/laravel/simple-material',
|
||||||
'pagination/livewire/tailwind',
|
'pagination/livewire/material',
|
||||||
'pagination/livewire/simple-tailwind',
|
'pagination/livewire/simple-material',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
it('imports the stylesheet of every component pagination\'s views render', function (string $view) {
|
it('imports the stylesheet of every component pagination\'s views render', function (string $view) {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ it('hides on a scroll down and springs back on a scroll up', function () {
|
|||||||
->toBe(['translate' => '0 100%'])
|
->toBe(['translate' => '0 100%'])
|
||||||
// Reads --material-bottom-bar back from <x-scaffold>: layout/scaffold.css publishes it in
|
// Reads --material-bottom-bar back from <x-scaffold>: layout/scaffold.css publishes it in
|
||||||
// `material.layout`, which this rule's own `material.components` always outranks, so both
|
// `material.layout`, which this rule's own `material.components` always outranks, so both
|
||||||
// sit in a layer now (the era of an unlayered Tailwind utility is gone).
|
// sit in a layer.
|
||||||
->and($css->declarations('[data-md-scaffold]:has([data-md-navigation-bar][data-md-hide-on-scroll][data-md-hidden])'))
|
->and($css->declarations('[data-md-scaffold]:has([data-md-navigation-bar][data-md-hide-on-scroll][data-md-hidden])'))
|
||||||
->toBe(['--material-bottom-bar' => 'calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px))']);
|
->toBe(['--material-bottom-bar' => 'calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px))']);
|
||||||
|
|
||||||
|
|||||||
@@ -143,14 +143,14 @@ it('hides a collapsible or adaptive rail entirely when told to, and only those',
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Tailwind-era `rail-collapsed:` custom variant had seven branches for "this rail is drawn
|
* "This rail is drawn collapsed" has seven conditions in all; navigation-rail.css merges the three
|
||||||
* collapsed"; navigation-rail.css merges the three that carry no media query into one `:where()`
|
* that carry no media query into one `:where()` (provably the same match set: an element matches
|
||||||
* (provably the same match set: an element matches `X:where(A, B, C)` exactly when it matches
|
* `X:where(A, B, C)` exactly when it matches `X:where(A)` or `X:where(B)` or `X:where(C)`) and keeps
|
||||||
* `X:where(A)` or `X:where(B)` or `X:where(C)`) and keeps the four media-gated ones separate, since
|
* the four media-gated ones separate, since CSS cannot merge different `@media` queries into one
|
||||||
* CSS cannot merge different `@media` queries into one rule. This pins all five physical branches
|
* rule. This pins all five physical branches — and so every one of the seven original conditions —
|
||||||
* — and so every one of the seven original conditions — for the rail's own width, the property
|
* for the rail's own width, the property every branch sets, and for `--md-navigation-rail-value`,
|
||||||
* every branch sets, and for `--md-navigation-rail-value`, which each branch turns from `expanded`
|
* which each branch turns from `expanded` to `collapsed` so an application's style query answers
|
||||||
* to `collapsed` so an application's style query answers exactly when the rail's own shape does.
|
* exactly when the rail's own shape does.
|
||||||
*/
|
*/
|
||||||
it('holds a closing rail on screen for its exit rather than transitioning display', function () {
|
it('holds a closing rail on screen for its exit rather than transitioning display', function () {
|
||||||
$css = ComponentStylesheet::read('navigation-rail');
|
$css = ComponentStylesheet::read('navigation-rail');
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ it('draws a search bar whose combobox controls the view', function () {
|
|||||||
->toContain('materialSearch(false,')
|
->toContain('materialSearch(false,')
|
||||||
->not->toContain('data-md-search-results')
|
->not->toContain('data-md-search-results')
|
||||||
// Split so the string never reads as a literal class="..." attribute to the design guard's
|
// Split so the string never reads as a literal class="..." attribute to the design guard's
|
||||||
// own scan of this file — Tailwind's `relative` utility, which the field used to carry.
|
// own scan of this file — `relative`, the utility class the field used to carry.
|
||||||
->not->toContain('class="'.'relative"');
|
->not->toContain('class="'.'relative"');
|
||||||
|
|
||||||
// The state belongs to the combobox, never to the bare textbox ARIA gives it to.
|
// The state belongs to the combobox, never to the bare textbox ARIA gives it to.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||||
@@ -16,6 +15,15 @@ function fixtureRelative(array $violations): array
|
|||||||
return array_map(fn (string $violation): string => str_replace(realpath(GUARD_FIXTURES).'/', '', $violation), $violations);
|
return array_map(fn (string $violation): string => str_replace(realpath(GUARD_FIXTURES).'/', '', $violation), $violations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check (i)'s message for one class: it names no replacement, since a class no stylesheet declares
|
||||||
|
* is dead whatever it was once meant to do.
|
||||||
|
*/
|
||||||
|
function undeclared(string $class): string
|
||||||
|
{
|
||||||
|
return "class `{$class}` is declared in no stylesheet — the package declares `md-*`, the rest is your own CSS";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every check that only scans a fixture, optionally names the CSS entry `missingStylesheets()`
|
* Every check that only scans a fixture, optionally names the CSS entry `missingStylesheets()`
|
||||||
* should check against, and compares the result to an exact violations list — most of this file,
|
* should check against, and compares the result to an exact violations list — most of this file,
|
||||||
@@ -25,20 +33,58 @@ function fixtureRelative(array $violations): array
|
|||||||
* once) stays its own test, further down.
|
* once) stays its own test, further down.
|
||||||
*/
|
*/
|
||||||
dataset('design guard scans', [
|
dataset('design guard scans', [
|
||||||
|
// app/Status.php returns class names from a PHP `match`, and views/page.blade.php writes one in
|
||||||
|
// running text and in a Blade comment: none of the three is a class list, so none is read.
|
||||||
'finds what compiles to nothing' => [
|
'finds what compiles to nothing' => [
|
||||||
[realpath(GUARD_FIXTURES.'/app'), realpath(GUARD_FIXTURES.'/views')],
|
[realpath(GUARD_FIXTURES.'/app'), realpath(GUARD_FIXTURES.'/views')],
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
'app/Status.php:14 Tailwind palette colour `text-red-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
|
'views/page.blade.php:2 '.undeclared('p-4'),
|
||||||
'views/page.blade.php:2 Tailwind spacing utility `p-4` compiles to nothing — use `padding="space200"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS',
|
|
||||||
'views/page.blade.php:3 unknown Material Symbol `o-home`',
|
'views/page.blade.php:3 unknown Material Symbol `o-home`',
|
||||||
'views/page.blade.php:4 unknown Material Symbol `not_a_symbol`',
|
'views/page.blade.php:4 unknown Material Symbol `not_a_symbol`',
|
||||||
'views/page.blade.php:5 Tailwind palette colour `text-red-600` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
|
'views/page.blade.php:6 '.undeclared('focus-ring'),
|
||||||
'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:6 '.undeclared('text-tertiary'),
|
||||||
'views/page.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`',
|
'views/page.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`',
|
||||||
"views/page.blade.php:8 Tailwind sizing utility `size-4` 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",
|
'views/page.blade.php:8 '.undeclared('size-4'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'passes a class the application\'s own stylesheet declares, and reports the one it does not' => [
|
||||||
|
[realpath(GUARD_FIXTURES.'/declared/view.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')],
|
||||||
|
null,
|
||||||
|
['declared/view.blade.php:1 '.undeclared('mt-2')],
|
||||||
|
],
|
||||||
|
'reads a class list in an attribute, an Alpine binding and a PHP one, but not a string a condition compares or a call takes' => [
|
||||||
|
[realpath(GUARD_FIXTURES.'/declared/bindings.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')],
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
'declared/bindings.blade.php:1 '.undeclared('collapsed'),
|
||||||
|
'declared/bindings.blade.php:1 '.undeclared('dashboard-busy'),
|
||||||
|
'declared/bindings.blade.php:1 '.undeclared('is-open'),
|
||||||
|
'declared/bindings.blade.php:2 '.undeclared('dashboard-enter-start'),
|
||||||
|
'declared/bindings.blade.php:2 '.undeclared('dashboard-enter'),
|
||||||
|
'declared/bindings.blade.php:3 '.undeclared('share-list'),
|
||||||
|
'declared/bindings.blade.php:4 '.undeclared('dashboard-lead'),
|
||||||
|
'declared/bindings.blade.php:6 '.undeclared('share-list'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reads the keys of a class list, the array a class helper is given and every branch of a ternary, never a string that indexes an array' => [
|
||||||
|
[realpath(GUARD_FIXTURES.'/declared/lookups.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')],
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
'declared/lookups.blade.php:4 '.undeclared('product-shot-base'),
|
||||||
|
'declared/lookups.blade.php:8 '.undeclared('routes-show-run-chosen'),
|
||||||
|
'declared/lookups.blade.php:9 '.undeclared('shot-helper-one'),
|
||||||
|
'declared/lookups.blade.php:9 '.undeclared('shot-helper-two'),
|
||||||
|
'declared/lookups.blade.php:10 '.undeclared('shot-tern-a'),
|
||||||
|
'declared/lookups.blade.php:10 '.undeclared('shot-tern-b'),
|
||||||
|
'declared/lookups.blade.php:10 '.undeclared('shot-tern-c'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'passes every class the package itself declares, showcase included' => [
|
||||||
|
realpath(GUARD_FIXTURES.'/declared/package-classes.blade.php'),
|
||||||
|
null,
|
||||||
|
[],
|
||||||
|
],
|
||||||
'names what a component tag takes in place of each directive' => [
|
'names what a component tag takes in place of each directive' => [
|
||||||
realpath(GUARD_FIXTURES.'/directives'),
|
realpath(GUARD_FIXTURES.'/directives'),
|
||||||
null,
|
null,
|
||||||
@@ -46,309 +92,18 @@ dataset('design guard scans', [
|
|||||||
'directives/tags.blade.php:1 Blade directive `@js` inside a component tag, where it does not compile — use `{{ \\Illuminate\\Support\\Js::from(…) }}`',
|
'directives/tags.blade.php:1 Blade directive `@js` inside a component tag, where it does not compile — use `{{ \\Illuminate\\Support\\Js::from(…) }}`',
|
||||||
'directives/tags.blade.php:2 Blade directive `@json` inside a component tag, where it does not compile — use `{{ json_encode(…) }}`',
|
'directives/tags.blade.php:2 Blade directive `@json` inside a component tag, where it does not compile — use `{{ json_encode(…) }}`',
|
||||||
'directives/tags.blade.php:3 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`',
|
'directives/tags.blade.php:3 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`',
|
||||||
|
'directives/tags.blade.php:3 '.undeclared('is-open'),
|
||||||
'directives/tags.blade.php:4 Blade directive `@style` inside a component tag, where it does not compile — use `:style="\\Illuminate\\Support\\Arr::toCssStyles([…])"`',
|
'directives/tags.blade.php:4 Blade directive `@style` inside a component tag, where it does not compile — use `:style="\\Illuminate\\Support\\Arr::toCssStyles([…])"`',
|
||||||
"directives/tags.blade.php:5 Blade directive `@entangle` inside a component tag, where it does not compile — use `\$wire.entangle('…')` in the Alpine expression",
|
"directives/tags.blade.php:5 Blade directive `@entangle` inside a component tag, where it does not compile — use `\$wire.entangle('…')` in the Alpine expression",
|
||||||
'directives/tags.blade.php:6 Blade directive `@disabled` inside a component tag, where it does not compile — use `:disabled="…"`',
|
'directives/tags.blade.php:6 Blade directive `@disabled` inside a component tag, where it does not compile — use `:disabled="…"`',
|
||||||
'directives/tags.blade.php:7 Blade directive `@if` inside a component tag, where it does not compile — use a `:prop` binding or `{{ }}`, or move it to a plain element inside the slot',
|
'directives/tags.blade.php:7 Blade directive `@if` inside a component tag, where it does not compile — use a `:prop` binding or `{{ }}`, or move it to a plain element inside the slot',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'names the M3 breakpoint for a Tailwind prefix, with its 2.0.0 replacement' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/breakpoints'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `2xl:` compiles to nothing — M3's extra-large (1600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 1600px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `lg:` compiles to nothing — M3's expanded (840px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 840px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `sm:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `xl:` compiles to nothing — M3's large (1200px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 1200px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-2xl:` compiles to nothing — M3's extra-large (1600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 1600px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-lg:` compiles to nothing — M3's expanded (840px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 840px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 600px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-sm:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 600px)` in your own CSS",
|
|
||||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-xl:` compiles to nothing — M3's large (1200px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 1200px)` in your own CSS",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'names the 2.0.0 replacement for a pseudo-class variant, but leaves a breakpoint prefix to its own finding' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/variant.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"tailwind/variant.blade.php:1 Tailwind variant class `dark:opacity-50` compiles to nothing — the roles already switch with the theme; a dark-only rule is `[data-theme='dark'] …` in your own CSS",
|
|
||||||
"tailwind/variant.blade.php:1 Tailwind variant class `hover:underline` compiles to nothing — M3's hover, focus and press states are `md-state-layer` and `md-focus-ring` (interaction.css); any other state rule is `:hover`/`:focus-visible` in your own CSS",
|
|
||||||
"tailwind/variant.blade.php:2 Tailwind breakpoint `md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
|
||||||
"tailwind/variant.blade.php:2 Tailwind variant class `focus:md:gap-2` compiles to nothing — M3's hover, focus and press states are `md-state-layer` and `md-focus-ring` (interaction.css); any other state rule is `:hover`/`:focus-visible` in your own CSS",
|
|
||||||
'tailwind/variant.blade.php:2 Tailwind variant class `group-hover:flex` compiles to nothing — `:has()` or a descendant selector in your own CSS',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'leaves a breakpoint name that is not a variant alone' => [
|
|
||||||
__DIR__.'/../../resources/js/slider.js',
|
|
||||||
null,
|
|
||||||
[],
|
|
||||||
],
|
|
||||||
'names the M3 corner for a Tailwind radius' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/scale/corners.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-lg` — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
|
||||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-md` — use `var(--md-sys-shape-corner-md)` in your own CSS, or `<x-surface corner="md">`',
|
|
||||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-none` — use `var(--md-sys-shape-corner-none)` in your own CSS, or `<x-surface corner="none">`',
|
|
||||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-sm` — use `var(--md-sys-shape-corner-sm)` in your own CSS, or `<x-surface corner="sm">`',
|
|
||||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-xs` — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`',
|
|
||||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-2xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
||||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-3xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
||||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-4xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
||||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-full` — use `var(--md-sys-shape-corner-full)` in your own CSS, or `<x-surface corner="full">`',
|
|
||||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-xl` — use `var(--md-sys-shape-corner-xl)` in your own CSS, or `<x-surface corner="xl">`',
|
|
||||||
'scale/corners.blade.php:3 value outside the M3 scale `rounded-se-2xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
||||||
'scale/corners.blade.php:3 value outside the M3 scale `rounded-t-lg` — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'names the M3 elevation level for a Tailwind shadow' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/scale/elevation.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-2xs` — use `var(--md-sys-elevation-1)` in your own CSS',
|
|
||||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-md` — use `var(--md-sys-elevation-2)` in your own CSS',
|
|
||||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-sm` — use `var(--md-sys-elevation-1)` in your own CSS',
|
|
||||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-xs` — use `var(--md-sys-elevation-1)` in your own CSS',
|
|
||||||
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-2xl` — use `var(--md-sys-elevation-5)` in your own CSS',
|
|
||||||
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-lg` — use `var(--md-sys-elevation-3)` in your own CSS',
|
|
||||||
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-xl` — use `var(--md-sys-elevation-4)` in your own CSS',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends a size, a weight, a leading and a tracking to a type style' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/scale/type.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'scale/type.blade.php:1 value outside the M3 scale `text-2xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:1 value outside the M3 scale `text-9xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:1 value outside the M3 scale `text-base` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:1 value outside the M3 scale `text-lg` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:1 value outside the M3 scale `text-sm` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:1 value outside the M3 scale `text-xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:1 value outside the M3 scale `text-xs` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:2 value outside the M3 scale `font-extralight` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:2 value outside the M3 scale `font-light` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:2 value outside the M3 scale `font-medium` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:2 value outside the M3 scale `font-normal` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:2 value outside the M3 scale `font-thin` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:3 value outside the M3 scale `font-black` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:3 value outside the M3 scale `font-bold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:3 value outside the M3 scale `font-extrabold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:3 value outside the M3 scale `font-semibold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
||||||
'scale/type.blade.php:4 value outside the M3 scale `leading-6` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:4 value outside the M3 scale `leading-loose` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:4 value outside the M3 scale `leading-none` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:4 value outside the M3 scale `leading-normal` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:4 value outside the M3 scale `leading-relaxed` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:4 value outside the M3 scale `leading-snug` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:4 value outside the M3 scale `leading-tight` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-normal` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-tight` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-tighter` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-wide` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-wider` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-widest` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends an easing and a duration to the motion tokens' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/scale/motion.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-in-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
||||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-in` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
||||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-linear` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
||||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
||||||
'scale/motion.blade.php:2 value outside the M3 scale `duration-1000` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
|
||||||
'scale/motion.blade.php:2 value outside the M3 scale `duration-300` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
|
||||||
'scale/motion.blade.php:2 value outside the M3 scale `duration-75` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
// scale/motion.js reads a real CSS keyword out of matchMedia() into a variable named `easing`
|
|
||||||
// (`'linear'`/`'ease-out'`), the shape the plan calls out as a false positive: nothing here is
|
|
||||||
// a class list, so the class-context match above never sees it, and the family is off
|
|
||||||
// outsideTheScale()'s line scan since the fix (see DesignGuard::outsideTheScale()'s header).
|
|
||||||
'leaves an easing or a duration word alone in a JavaScript string outside a class list' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/scale/motion.js'),
|
|
||||||
null,
|
|
||||||
[],
|
|
||||||
],
|
|
||||||
'finds a colour written as a value' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/colour'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'colour/badge.blade.php:1 arbitrary colour `bg-[#1d7afc]`, use an M3 role',
|
|
||||||
'colour/badge.blade.php:1 arbitrary colour `border-[hsl(210_80%_50%)]`, use an M3 role',
|
|
||||||
'colour/badge.blade.php:1 arbitrary colour `text-[rgb(0_0_0)]`, use an M3 role',
|
|
||||||
'colour/badge.blade.php:2 arbitrary colour `fill-[oklch(0.7_0.1_250)]`, use an M3 role',
|
|
||||||
'colour/badge.blade.php:2 arbitrary colour `ring-[color-mix(in_oklab,var(--x)_50%,transparent)]`, use an M3 role',
|
|
||||||
'colour/badge.blade.php:3 Tailwind colour utility `bg-white` compiles to nothing — 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',
|
|
||||||
'colour/badge.blade.php:3 Tailwind colour utility `border-white` compiles to nothing — 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',
|
|
||||||
'colour/badge.blade.php:3 Tailwind colour utility `text-black` compiles to nothing — 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',
|
|
||||||
"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) <n>%, 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) <n>%, transparent)` in your own CSS",
|
|
||||||
"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)",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends the state-layer opacities and every other role to their 2.0.0 replacement' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/inks.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'tailwind/inks.blade.php:1 Tailwind colour utility `bg-background` compiles to nothing — use `<x-surface level="surface">`',
|
|
||||||
'tailwind/inks.blade.php:1 Tailwind colour utility `text-current` compiles to nothing — write `currentColor` in your own CSS',
|
|
||||||
'tailwind/inks.blade.php:1 Tailwind colour utility `text-on-primary-container` compiles to nothing — use `var(--md-sys-color-on-primary-container)` in your own CSS',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'names the md-ink-*, divider/surface or token replacement for a dead M3 role utility' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/roles.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'tailwind/roles.blade.php:1 Tailwind colour utility `border-outline-variant` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`, not a border utility',
|
|
||||||
'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:2 Tailwind colour utility `bg-primary` compiles to nothing — use `var(--md-sys-color-primary)` in your own CSS',
|
|
||||||
'tailwind/roles.blade.php:2 Tailwind colour utility `bg-surface-container` compiles to nothing — use `<x-surface level="surface-container">`',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends flex, grid and gap utilities to the layout component and prop that replaces each' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/layout.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'tailwind/layout.blade.php:1 Tailwind layout utility `flex-col` compiles to nothing — use `<x-stack>` (`gap`, `align`)',
|
|
||||||
'tailwind/layout.blade.php:1 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
||||||
'tailwind/layout.blade.php:1 Tailwind layout utility `grid` compiles to nothing — use `<x-grid>` (`:columns` per breakpoint, `gap`, `min-item`), or `<x-feed>` for a grid of cards',
|
|
||||||
'tailwind/layout.blade.php:1 Tailwind layout utility `inline-flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
||||||
"tailwind/layout.blade.php:2 Tailwind flex/grid item utility `col-span-2` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
||||||
"tailwind/layout.blade.php:2 Tailwind layout utility `grid-cols-3` compiles to nothing — use `<x-grid :columns=\"3\">`, or a per-breakpoint map (`:columns=\"['compact' => 1, 'medium' => 3]\"`)",
|
|
||||||
'tailwind/layout.blade.php:2 Tailwind layout utility `items-center` compiles to nothing — use `align="center"` on `<x-row>` or `<x-stack>`',
|
|
||||||
'tailwind/layout.blade.php:2 Tailwind layout utility `justify-between` compiles to nothing — use `justify="between"` on `<x-row>`',
|
|
||||||
'tailwind/layout.blade.php:2 Tailwind spacing utility `gap-4` compiles to nothing — use `gap="space200"` (16px) on `<x-row>`, `<x-stack>`, `<x-grid>` or `<x-feed>`',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends padding, margin and space-between utilities to the M3 spacing step they are (gap-4 is space200)' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/spacing.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"tailwind/spacing.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
|
||||||
'tailwind/spacing.blade.php:1 Tailwind spacing utility `p-4` compiles to nothing — use `padding="space200"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS',
|
|
||||||
'tailwind/spacing.blade.php:1 Tailwind spacing utility `px-2` compiles to nothing — use `padding="space100"` (8px) on `<x-surface>`, or `var(--md-sys-measurement-space100)` in your own CSS',
|
|
||||||
'tailwind/spacing.blade.php:1 Tailwind spacing utility `space-y-4` compiles to nothing — use `<x-stack>` with `gap="space200"` (16px)',
|
|
||||||
"tailwind/spacing.blade.php:2 Tailwind spacing utility `m-4` compiles to nothing — space between siblings is a layout component's `gap=\"space200\"` (16px); any other margin is `var(--md-sys-measurement-space200)` in your own CSS",
|
|
||||||
'tailwind/spacing.blade.php:2 Tailwind spacing utility `space-x-2` compiles to nothing — use `<x-row>` with `gap="space100"` (8px)',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends width and height utilities to a literal length, since M3 keeps no size scale' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/sizing.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `h-screen` 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",
|
|
||||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `max-w-md` 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",
|
|
||||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `min-w-0` 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",
|
|
||||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `w-full` 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",
|
|
||||||
"tailwind/sizing.blade.php:2 Tailwind sizing utility `size-4` 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",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends hidden to hide-below/hide-from and every other display utility to a plain CSS rule' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/display.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'tailwind/display.blade.php:1 Tailwind display utility `block` compiles to nothing — write the `display` rule in your own CSS',
|
|
||||||
"tailwind/display.blade.php:1 Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute (the reset keeps it hidden), or `x-show`",
|
|
||||||
'tailwind/display.blade.php:2 Tailwind display utility `inline-block` compiles to nothing — write the `display` rule in your own CSS',
|
|
||||||
'tailwind/display.blade.php:2 Tailwind display utility `invisible` compiles to nothing — write the `display` rule in your own CSS',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sends text-layout utilities to their md-* class' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/text.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"tailwind/text.blade.php:1 Tailwind's `text-center` compiles to nothing — use `md-text-center` (text.css)",
|
|
||||||
"tailwind/text.blade.php:1 Tailwind's `truncate` compiles to nothing — use `md-truncate` (text.css)",
|
|
||||||
"tailwind/text.blade.php:2 Tailwind's `line-clamp-2` compiles to nothing — use `md-line-clamp-2` (text.css)",
|
|
||||||
"tailwind/text.blade.php:2 Tailwind's `sr-only` compiles to nothing — use `md-visually-hidden` (text.css)",
|
|
||||||
"tailwind/text.blade.php:2 Tailwind's `tabular-nums` compiles to nothing — use `md-tabular` (text.css)",
|
|
||||||
"tailwind/text.blade.php:2 Tailwind's `whitespace-nowrap` compiles to nothing — use `md-nowrap` (text.css)",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'exempts a class the application\'s own CSS declares, even when it is Tailwind-shaped' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/exempt'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"exempt/view.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'reports an arbitrary [] value once, whatever utility it modifies, and strips a trailing !' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/arbitrary.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'tailwind/arbitrary.blade.php:1 Tailwind arbitrary value `grid-cols-[1fr_2fr]` compiles to nothing — write the literal value in your own CSS, or use an M3 token',
|
|
||||||
'tailwind/arbitrary.blade.php:1 Tailwind arbitrary value `top-[10px]` compiles to nothing — write the literal value in your own CSS, or use an M3 token',
|
|
||||||
'tailwind/arbitrary.blade.php:2 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
||||||
'tailwind/arbitrary.blade.php:4 Tailwind arbitrary property `[--material-bottom-bar:5rem]` compiles to nothing — write `--material-bottom-bar` in your own CSS',
|
|
||||||
"tailwind/arbitrary.blade.php:4 Tailwind arbitrary property `[font-variation-settings:'ROND'_100]` compiles to nothing — write `font-variation-settings` in your own CSS",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'leaves every role alone until an application forbids one' => [
|
'leaves every role alone until an application forbids one' => [
|
||||||
realpath(GUARD_FIXTURES.'/forbidden-colours'),
|
realpath(GUARD_FIXTURES.'/forbidden-colours'),
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
'forbidden-colours/page.blade.php:5 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
'forbidden-colours/page.blade.php:5 '.undeclared('bg-tertiary'),
|
||||||
],
|
|
||||||
],
|
|
||||||
'names the property a Tailwind custom-property utility stands for' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/custom-property.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
'tailwind/custom-property.blade.php:1 Tailwind custom-property utility `duration-(--md-sys-motion-spatial-fast-duration)` compiles to nothing — write `var(--md-sys-motion-spatial-fast-duration)` in your own CSS',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'reports position, border, effect, interactivity and text utilities with the rule to write instead' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/utilities.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `col-start-2` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
||||||
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `justify-self-end` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
||||||
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `shrink-0` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
||||||
'tailwind/utilities.blade.php:1 Tailwind inset utility `-top-2` compiles to nothing — write the offset in your own CSS, from `var(--md-sys-measurement-space*)` where it is a spacing step',
|
|
||||||
'tailwind/utilities.blade.php:1 Tailwind inset utility `inset-0` compiles to nothing — write the offset in your own CSS, from `var(--md-sys-measurement-space*)` where it is a spacing step',
|
|
||||||
'tailwind/utilities.blade.php:1 Tailwind overflow utility `overflow-hidden` compiles to nothing — write `overflow` in your own CSS',
|
|
||||||
"tailwind/utilities.blade.php:1 Tailwind position utility `relative` compiles to nothing — write `position` in your own CSS (a FAB goes in `<x-scaffold>`'s `fab` slot)",
|
|
||||||
'tailwind/utilities.blade.php:1 Tailwind z-index utility `z-10` compiles to nothing — write `z-index` in your own CSS',
|
|
||||||
"tailwind/utilities.blade.php:1 Tailwind's `container` compiles to nothing — use `<x-pane width>`, which sets M3's margins and a measure",
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind border utility `border-t-2` 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)`',
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind border utility `border` 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)`',
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind effect utility `-rotate-45` compiles to nothing — write the rule in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind interactivity utility `cursor-pointer` compiles to nothing — write the rule in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind interactivity utility `select-none` compiles to nothing — write the rule in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind motion utility `transition` compiles to nothing — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition` or `animation`',
|
|
||||||
"tailwind/utilities.blade.php:2 Tailwind opacity utility `opacity-50` compiles to nothing — write `opacity` in your own CSS (M3's disabled content is 38 %)",
|
|
||||||
"tailwind/utilities.blade.php:2 Tailwind outline utility `outline-none` compiles to nothing — M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS",
|
|
||||||
"tailwind/utilities.blade.php:2 Tailwind outline utility `ring-2` compiles to nothing — M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS",
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind radius utility `rounded` compiles to nothing — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`',
|
|
||||||
'tailwind/utilities.blade.php:2 Tailwind shadow utility `shadow` compiles to nothing — use `var(--md-sys-elevation-*)` in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:3 Tailwind background utility `bg-clip-text` compiles to nothing — write the rule in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:3 Tailwind display utility `table` compiles to nothing — write the `display` rule in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:3 Tailwind media utility `aspect-video` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:3 Tailwind media utility `object-cover` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:3 Tailwind text utility `list-disc` compiles to nothing — write the rule in your own CSS',
|
|
||||||
'tailwind/utilities.blade.php:3 Tailwind text utility `uppercase` compiles to nothing — write the rule in your own CSS',
|
|
||||||
"tailwind/utilities.blade.php:3 Tailwind's `antialiased` compiles to nothing — the foundation already smooths text in grayscale (base.css); drop it",
|
|
||||||
"tailwind/utilities.blade.php:3 Tailwind's `font-mono` compiles to nothing — put the value in `<code>`, `<kbd>` or `<samp>`, or use `md-mono` (text.css)",
|
|
||||||
"tailwind/utilities.blade.php:3 Tailwind's `font-sans` compiles to nothing — the foundation already sets the brand typeface; any other `font-family` is your own CSS",
|
|
||||||
"tailwind/utilities.blade.php:3 Tailwind's `text-end` compiles to nothing — use `md-text-end` (text.css)",
|
|
||||||
"tailwind/utilities.blade.php:3 Tailwind's `underline` compiles to nothing — `md-link` draws a link (interaction.css); any other decoration is your own CSS",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'reads class lists in Alpine, Livewire and PHP bindings, but not a string compared in a condition' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/tailwind/bindings.blade.php'),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"tailwind/bindings.blade.php:1 Tailwind opacity utility `opacity-50` compiles to nothing — write `opacity` in your own CSS (M3's disabled content is 38 %)",
|
|
||||||
"tailwind/bindings.blade.php:1 Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute (the reset keeps it hidden), or `x-show`",
|
|
||||||
'tailwind/bindings.blade.php:2 Tailwind effect utility `scale-95` compiles to nothing — write the rule in your own CSS',
|
|
||||||
'tailwind/bindings.blade.php:2 Tailwind motion utility `transition` compiles to nothing — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition` or `animation`',
|
|
||||||
'tailwind/bindings.blade.php:2 value outside the M3 scale `ease-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
||||||
'tailwind/bindings.blade.php:3 Tailwind spacing utility `gap-2` compiles to nothing — use `gap="space100"` (8px) on `<x-row>`, `<x-stack>`, `<x-grid>` or `<x-feed>`',
|
|
||||||
"tailwind/bindings.blade.php:4 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
|
||||||
'tailwind/bindings.blade.php:6 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'names the missing package stylesheet and the exact @import line to add' => [
|
'names the missing package stylesheet and the exact @import line to add' => [
|
||||||
@@ -441,11 +196,6 @@ dataset('design guard scans', [
|
|||||||
'shadows/rings.css:20 literal shadow `box-shadow: inset 0 0 4px 1px #000` — use `var(--md-sys-elevation-*)`',
|
'shadows/rings.css:20 literal shadow `box-shadow: inset 0 0 4px 1px #000` — use `var(--md-sys-elevation-*)`',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'never reads a Tailwind-shaped word out of running text' => [
|
|
||||||
realpath(GUARD_FIXTURES.'/running-text.blade.php'),
|
|
||||||
null,
|
|
||||||
[],
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
it('scans a fixture and reports exactly the expected violations', function (string|array $fixture, ?string $entry, array $expected) {
|
it('scans a fixture and reports exactly the expected violations', function (string|array $fixture, ?string $entry, array $expected) {
|
||||||
@@ -454,81 +204,32 @@ it('scans a fixture and reports exactly the expected violations', function (stri
|
|||||||
expect(fixtureRelative(($entry === null ? $guard : $guard->missingStylesheets($entry))->violations()))->toBe($expected);
|
expect(fixtureRelative(($entry === null ? $guard : $guard->missingStylesheets($entry))->violations()))->toBe($expected);
|
||||||
})->with('design guard scans');
|
})->with('design guard scans');
|
||||||
|
|
||||||
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('leaves a Markdown mail component\'s classes and its theme to the mail theme, but still reads its icons and directives', function () {
|
it('leaves a Markdown mail component\'s classes and its theme to the mail theme, but still reads its icons and directives', function () {
|
||||||
$mail = realpath(GUARD_FIXTURES.'/mail/views');
|
$mail = realpath(GUARD_FIXTURES.'/mail/views');
|
||||||
$table = 'Tailwind display utility `table` compiles to nothing — write the `display` rule in your own CSS';
|
|
||||||
$icon = 'mail/views/vendor/mail/html/sessions.blade.php:7 unknown Material Symbol `not_a_symbol`';
|
$icon = 'mail/views/vendor/mail/html/sessions.blade.php:7 unknown Material Symbol `not_a_symbol`';
|
||||||
$directive = 'mail/views/vendor/mail/html/sessions.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`';
|
$directive = 'mail/views/vendor/mail/html/sessions.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`';
|
||||||
|
|
||||||
config(['mail.markdown.paths' => [$mail.'/vendor/mail']]);
|
config(['mail.markdown.paths' => [$mail.'/vendor/mail']]);
|
||||||
|
|
||||||
// The theme's `.table` exempts nothing outside the mail components, and its literals are its own.
|
// The theme's `.table` declares nothing outside the mail components, and its literals are its own.
|
||||||
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
||||||
"mail/views/page.blade.php:1 {$table}",
|
'mail/views/page.blade.php:1 '.undeclared('table'),
|
||||||
$icon,
|
$icon,
|
||||||
$directive,
|
$directive,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Outside a mail component path the same files read as the application's own: the theme's
|
// Outside a mail component path the same files read as the application's own: the theme's
|
||||||
// classes as Tailwind, its literals as check (iii)'s, and its `.table` exempting `table`.
|
// classes against its own rules, which declare `table` and nothing else, and its literals as
|
||||||
|
// check (iii)'s.
|
||||||
config(['mail.markdown.paths' => []]);
|
config(['mail.markdown.paths' => []]);
|
||||||
|
|
||||||
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
||||||
'mail/views/vendor/mail/html/sessions.blade.php:3 value outside the M3 scale `text-sm` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
'mail/views/vendor/mail/html/sessions.blade.php:3 '.undeclared('sessions'),
|
||||||
'mail/views/vendor/mail/html/sessions.blade.php:4 Tailwind text utility `break-all` compiles to nothing — write the rule in your own CSS',
|
'mail/views/vendor/mail/html/sessions.blade.php:3 '.undeclared('text-sm'),
|
||||||
|
'mail/views/vendor/mail/html/sessions.blade.php:4 '.undeclared('break-all'),
|
||||||
$icon,
|
$icon,
|
||||||
$directive,
|
$directive,
|
||||||
|
'mail/views/vendor/mail/html/sessions.blade.php:8 '.undeclared('wide'),
|
||||||
'mail/views/vendor/mail/html/themes/default.css:2 literal colour `#3d4852` — use `var(--md-sys-color-*)`',
|
'mail/views/vendor/mail/html/themes/default.css:2 literal colour `#3d4852` — use `var(--md-sys-color-*)`',
|
||||||
'mail/views/vendor/mail/html/themes/default.css:3 literal font size `font-size: 14px` — set the whole style with `font: var(--md-sys-typescale-*)` and its `-tracking`, or an `md-type-*` class',
|
'mail/views/vendor/mail/html/themes/default.css:3 literal font size `font-size: 14px` — set the whole style with `font: var(--md-sys-typescale-*)` and its `-tracking`, or an `md-type-*` class',
|
||||||
]);
|
]);
|
||||||
@@ -547,7 +248,7 @@ it('bans the roles an application leaves out, wherever 2.0.0 writes one', functi
|
|||||||
"forbidden-colours/page.blade.php:3 `:color=\"\$failed ? 'error' : 'info'\"`: role `error` is not part of this application's palette",
|
"forbidden-colours/page.blade.php:3 `:color=\"\$failed ? 'error' : 'info'\"`: role `error` is not part of this application's palette",
|
||||||
"forbidden-colours/page.blade.php:4 `--md-sys-color-on-tertiary-container`: role `tertiary` is not part of this application's palette",
|
"forbidden-colours/page.blade.php:4 `--md-sys-color-on-tertiary-container`: role `tertiary` is not part of this application's palette",
|
||||||
"forbidden-colours/page.blade.php:4 `md-ink-error`: role `error` is not part of this application's palette",
|
"forbidden-colours/page.blade.php:4 `md-ink-error`: role `error` is not part of this application's palette",
|
||||||
'forbidden-colours/page.blade.php:5 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
'forbidden-colours/page.blade.php:5 '.undeclared('bg-tertiary'),
|
||||||
"forbidden-colours/page.blade.php:7 `--md-sys-color-primary-container`: role `primary-container` is not part of this application's palette",
|
"forbidden-colours/page.blade.php:7 `--md-sys-color-primary-container`: role `primary-container` is not part of this application's palette",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -576,7 +277,7 @@ it('names each stylesheet the entry imports that no scanned view needs, and leav
|
|||||||
|
|
||||||
it('follows an entry that imports what only a Vite build resolves, reports each missing stylesheet once, and leaves the entry\'s literals to scan()', function () {
|
it('follows an entry that imports what only a Vite build resolves, reports each missing stylesheet once, and leaves the entry\'s literals to scan()', function () {
|
||||||
$entry = realpath(GUARD_FIXTURES.'/stylesheets/foreign-imports.css');
|
$entry = realpath(GUARD_FIXTURES.'/stylesheets/foreign-imports.css');
|
||||||
$mt2 = "stylesheets/views-foreign/page.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS";
|
$mt2 = 'stylesheets/views-foreign/page.blade.php:1 '.undeclared('mt-2');
|
||||||
$button = "stylesheets/views-foreign/page.blade.php:2 `<x-button>` needs `components/button.css`, missing from stylesheets/foreign-imports.css — add `@import '../../../../resources/css/components/button.css';`";
|
$button = "stylesheets/views-foreign/page.blade.php:2 `<x-button>` needs `components/button.css`, missing from stylesheets/foreign-imports.css — add `@import '../../../../resources/css/components/button.css';`";
|
||||||
|
|
||||||
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views-foreign'))->missingStylesheets($entry)->violations()))
|
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views-foreign'))->missingStylesheets($entry)->violations()))
|
||||||
@@ -682,8 +383,7 @@ it('sorts every finding by path, then line, then message, whichever check produc
|
|||||||
// check (iii)'s application-CSS pass always runs last, after every view's own violations —
|
// check (iii)'s application-CSS pass always runs last, after every view's own violations —
|
||||||
// an unsorted result would put a.css's finding after z.blade.php's, even though "a.css"
|
// an unsorted result would put a.css's finding after z.blade.php's, even though "a.css"
|
||||||
// sorts before "z.blade.php" (Finder itself already walks views alphabetically, so this is
|
// sorts before "z.blade.php" (Finder itself already walks views alphabetically, so this is
|
||||||
// the one place natural order and sorted order actually diverge). One line also names a
|
// the one place natural order and sorted order actually diverge).
|
||||||
// palette colour and a spacing utility, from two checks that run in the opposite order.
|
|
||||||
File::put($dir.'/a.css', "body {\n color: #ff0000;\n}\n");
|
File::put($dir.'/a.css', "body {\n color: #ff0000;\n}\n");
|
||||||
File::put($dir.'/z.blade.php', '<div class="bg-red-500 p-4"></div>');
|
File::put($dir.'/z.blade.php', '<div class="bg-red-500 p-4"></div>');
|
||||||
|
|
||||||
@@ -695,8 +395,8 @@ it('sorts every finding by path, then line, then message, whichever check produc
|
|||||||
|
|
||||||
expect($violations)->toBe([
|
expect($violations)->toBe([
|
||||||
"{$dir}/a.css:2 literal colour `#ff0000` — use `var(--md-sys-color-*)`",
|
"{$dir}/a.css:2 literal colour `#ff0000` — use `var(--md-sys-color-*)`",
|
||||||
"{$dir}/z.blade.php:1 Tailwind palette colour `bg-red-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS",
|
"{$dir}/z.blade.php:1 ".undeclared('bg-red-500'),
|
||||||
"{$dir}/z.blade.php:1 Tailwind spacing utility `p-4` compiles to nothing — use `padding=\"space200\"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS",
|
"{$dir}/z.blade.php:1 ".undeclared('p-4'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -704,33 +404,11 @@ it('passes the package\'s own views, showcase and Workbench, every stylesheet im
|
|||||||
$root = realpath(__DIR__.'/../..');
|
$root = realpath(__DIR__.'/../..');
|
||||||
$paths = [$root.'/resources/views', $root.'/resources/js', $root.'/src', $root.'/workbench/resources'];
|
$paths = [$root.'/resources/views', $root.'/resources/js', $root.'/src', $root.'/workbench/resources'];
|
||||||
|
|
||||||
|
// The package's own Markdown mail components are the shape `mail.markdown.paths` names: their
|
||||||
|
// classes come from mail/theme.blade.php, a stylesheet Blade renders, not from a CSS entry.
|
||||||
|
config(['mail.markdown.paths' => [$root.'/resources/views/mail']]);
|
||||||
|
|
||||||
expect(DesignGuard::scan($paths)->missingStylesheets($root.'/resources/css/all.css')->violations())->toBe([])
|
expect(DesignGuard::scan($paths)->missingStylesheets($root.'/resources/css/all.css')->violations())->toBe([])
|
||||||
// …and the stylesheet check is not vacuous there: the showcase's plain tags do need imports.
|
// …and the stylesheet check is not vacuous there: the showcase's plain tags do need imports.
|
||||||
->and(DesignGuard::scan($root.'/resources/views/showcase')->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))->violations())->not->toBe([]);
|
->and(DesignGuard::scan($root.'/resources/views/showcase')->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))->violations())->not->toBe([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* No Tailwind-shaped class anywhere the package ships or tests itself: DesignGuard's own family
|
|
||||||
* table (check (i)) is the standing definition of a Tailwind utility or variant, so this is that
|
|
||||||
* guard turned on the package's own source. resources/views, resources/js and src are already
|
|
||||||
* covered by the previous test's scan (workbench/resources is an application fixture and carries
|
|
||||||
* Tailwind-shaped class names on purpose, so it is left out there too); this test adds what that
|
|
||||||
* one doesn't reach: tests/Browser and tests/Feature. This file and tests/Fixtures/design-guard/
|
|
||||||
* (the guard's own fixtures) carry Tailwind on purpose and are left out.
|
|
||||||
*/
|
|
||||||
it('writes no Tailwind-shaped class anywhere under tests/', function () {
|
|
||||||
$root = realpath(__DIR__.'/../..');
|
|
||||||
$designGuardTest = realpath(__DIR__.'/DesignGuardTest.php');
|
|
||||||
|
|
||||||
$paths = collect(['tests/Browser', 'tests/Feature'])
|
|
||||||
->flatMap(fn (string $directory): Collection => collect(File::allFiles($root.'/'.$directory)))
|
|
||||||
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'js', 'ts'], true))
|
|
||||||
->reject(fn (SplFileInfo $file): bool => (string) $file->getRealPath() === $designGuardTest)
|
|
||||||
->map(fn (SplFileInfo $file): string => (string) $file->getRealPath())
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
|
|
||||||
expect($paths)->not->toBeEmpty();
|
|
||||||
|
|
||||||
expect(DesignGuard::scan($paths)->violations())->toBe([]);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ it('refers to its own components through its namespace, which no prefix or app c
|
|||||||
// Comments document usage as an application writes it, and the showcase's examples
|
// Comments document usage as an application writes it, and the showcase's examples
|
||||||
// are that usage; neither is compiled as the package's own markup. An example heredoc
|
// are that usage; neither is compiled as the package's own markup. An example heredoc
|
||||||
// ends `BLADE,` as an array's value (most sections) or `BLADE;` assigned to its own
|
// ends `BLADE,` as an array's value (most sections) or `BLADE;` assigned to its own
|
||||||
// variable (a Layout page with a single example of its own, plan step 38).
|
// variable (a Layout page with a single example of its own).
|
||||||
$source = preg_replace(['/\{\{--.*?--\}\}/s', "/<<<'BLADE'.*?^\\s*BLADE[,;]/sm"], '', $file->getContents());
|
$source = preg_replace(['/\{\{--.*?--\}\}/s', "/<<<'BLADE'.*?^\\s*BLADE[,;]/sm"], '', $file->getContents());
|
||||||
|
|
||||||
preg_match_all('/<\/?x-('.$names.')(?=[\s\/>])/', $source, $matches);
|
preg_match_all('/<\/?x-('.$names.')(?=[\s\/>])/', $source, $matches);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
@@ -61,3 +64,61 @@ it('publishes the mail components where Laravel looks for them', function () {
|
|||||||
expect(File::files(LivewireMaterialServiceProvider::mailComponentPath().'/html'))->not->toBeEmpty()
|
expect(File::files(LivewireMaterialServiceProvider::mailComponentPath().'/html'))->not->toBeEmpty()
|
||||||
->and(File::files(LivewireMaterialServiceProvider::mailComponentPath().'/text'))->not->toBeEmpty();
|
->and(File::files(LivewireMaterialServiceProvider::mailComponentPath().'/text'))->not->toBeEmpty();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination: the provider takes over Laravel's and Livewire's default theme when
|
||||||
|
* `livewire-material.pagination` is on (the default), so `->links()` reaches the package's views
|
||||||
|
* without either framework being told which view to render.
|
||||||
|
*/
|
||||||
|
it('sets Paginator::defaultView() to the package\'s renamed views, so a plain ->links() call renders them', function () {
|
||||||
|
$pages = new LengthAwarePaginator(range(1, 10), 95, 10, 3, ['path' => '/shares']);
|
||||||
|
|
||||||
|
expect(Paginator::$defaultView)->toBe('pagination::material')
|
||||||
|
->and(Paginator::$defaultSimpleView)->toBe('pagination::simple-material')
|
||||||
|
->and((string) $pages->links())->toContain('data-md-pagination');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes over livewire.pagination_theme only while it still reads as Livewire\'s own default', function (?string $before, string $after) {
|
||||||
|
if ($before === null) {
|
||||||
|
// Livewire's own SupportPagination.php reads the key with a `'tailwind'` fallback, so a
|
||||||
|
// missing key must be treated the same as an explicit `tailwind` — this is the "missing"
|
||||||
|
// half of that. Config::offsetUnset() only sets the key to null (Repository::set()), so
|
||||||
|
// the key is removed for real by rebuilding the array without it.
|
||||||
|
config(['livewire' => Arr::except(config('livewire'), ['pagination_theme'])]);
|
||||||
|
} else {
|
||||||
|
config(['livewire.pagination_theme' => $before]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
|
||||||
|
(fn () => $this->registerPagination())->call($provider);
|
||||||
|
|
||||||
|
expect(config('livewire.pagination_theme'))->toBe($after);
|
||||||
|
})->with([
|
||||||
|
'Livewire\'s default, unset' => [null, 'material'],
|
||||||
|
'Livewire\'s default, explicit' => ['tailwind', 'material'],
|
||||||
|
'already material' => ['material', 'material'],
|
||||||
|
'another theme, left alone' => ['bootstrap', 'bootstrap'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('changes neither the default views nor the pagination theme when the config turns pagination off', function () {
|
||||||
|
$restoreView = Paginator::$defaultView;
|
||||||
|
$restoreSimpleView = Paginator::$defaultSimpleView;
|
||||||
|
|
||||||
|
try {
|
||||||
|
config(['livewire-material.pagination' => false, 'livewire.pagination_theme' => 'tailwind']);
|
||||||
|
Paginator::defaultView('pagination::tailwind');
|
||||||
|
Paginator::defaultSimpleView('pagination::simple-tailwind');
|
||||||
|
|
||||||
|
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
|
||||||
|
(fn () => $this->registerPagination())->call($provider);
|
||||||
|
|
||||||
|
expect(config('livewire.pagination_theme'))->toBe('tailwind')
|
||||||
|
->and(Paginator::$defaultView)->toBe('pagination::tailwind')
|
||||||
|
->and(Paginator::$defaultSimpleView)->toBe('pagination::simple-tailwind');
|
||||||
|
} finally {
|
||||||
|
// Both are global statics on Laravel's own class, outliving this test's app instance —
|
||||||
|
// left as 'tailwind' they would fail the file's other pagination tests that run after.
|
||||||
|
Paginator::defaultView($restoreView);
|
||||||
|
Paginator::defaultSimpleView($restoreSimpleView);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use NoNameWeb\LivewireMaterial\Support\Scheme;
|
|||||||
use NoNameWeb\LivewireMaterial\Support\SchemeStylesheet;
|
use NoNameWeb\LivewireMaterial\Support\SchemeStylesheet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The showcase's own stylesheet route (plan step 38): `all.css` plus `showcase.css`, prebuilt into
|
* The showcase's own stylesheet route: `all.css` plus `showcase.css`, prebuilt into
|
||||||
* resources/dist/showcase.css and served without the application's Vite build, and the two package
|
* resources/dist/showcase.css and served without the application's Vite build, and the two package
|
||||||
* folders its relative `url()`s point into.
|
* folders its relative `url()`s point into.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -34,27 +34,6 @@ it('resolves the files an entry imports, transitively, without bundling any cont
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves past what an application entry imports and only a Vite build resolves, without throwing', function () {
|
|
||||||
$dir = sys_get_temp_dir().'/livewire-material-resolved-'.Str::random(8);
|
|
||||||
File::makeDirectory($dir);
|
|
||||||
File::put($dir.'/theme.css', '.theme {}');
|
|
||||||
File::put($dir.'/app.css', implode("\n", [
|
|
||||||
"@import 'tailwindcss';",
|
|
||||||
"@import url('https://fonts.googleapis.com/css2?family=Roboto');",
|
|
||||||
"@import './theme.css' layer(app);",
|
|
||||||
"@import './missing.css';",
|
|
||||||
"@import '".stylesheetsBundlePath('components/icon.css')."';",
|
|
||||||
]));
|
|
||||||
|
|
||||||
try {
|
|
||||||
$files = Stylesheets::resolvedFiles([$dir.'/app.css']);
|
|
||||||
} finally {
|
|
||||||
File::deleteDirectory($dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect($files)->toBe([realpath(sys_get_temp_dir()).'/'.basename($dir).'/app.css', realpath(sys_get_temp_dir()).'/'.basename($dir).'/theme.css']);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deduplication keeps a file at its first position, so an override that ties a rule it imports on
|
* Deduplication keeps a file at its first position, so an override that ties a rule it imports on
|
||||||
* specificity (the navigation rail's header FAB over fab.css was one) still wins only while every
|
* specificity (the navigation rail's header FAB over fab.css was one) still wins only while every
|
||||||
@@ -170,11 +149,12 @@ function stylesheetsBundleLeafRules(string $css): array
|
|||||||
*
|
*
|
||||||
* The built CSS is minified, so this compares rules rather than files: no innermost rule may
|
* The built CSS is minified, so this compares rules rather than files: no innermost rule may
|
||||||
* appear twice under the same at-rules. A build that inlined every occurrence of a shared
|
* appear twice under the same at-rules. A build that inlined every occurrence of a shared
|
||||||
* stylesheet repeats hundreds of them (the Workbench's entry did while it shared one file with
|
* stylesheet repeats hundreds of them (the Workbench's entry did while `@tailwindcss/vite` was
|
||||||
* `@tailwindcss/vite`: button.css's hover and disabled rules thirteen times), while a file's plain
|
* still in its graph, inlining imports itself ahead of Vite's own postcss-import: button.css's
|
||||||
* root rule alone is no evidence — the minifier folds identical copies of that one together. In
|
* hover and disabled rules thirteen times), while a file's plain root rule alone is no evidence —
|
||||||
* the application entry, button.css must also keep its first position: before split-button.css,
|
* the minifier folds identical copies of that one together. In the application entry, button.css
|
||||||
* the first file that imports it, not at the end where the application named it again.
|
* must also keep its first position: before split-button.css, the first file that imports it, not
|
||||||
|
* at the end where the application named it again.
|
||||||
*/
|
*/
|
||||||
it('lands every rule once, at its first position, in a real Vite build of all.css and of an application entry', function () {
|
it('lands every rule once, at its first position, in a real Vite build of all.css and of an application entry', function () {
|
||||||
$node = config('livewire-material.node', 'node');
|
$node = config('livewire-material.node', 'node');
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ use NoNameWeb\LivewireMaterial\Support\Layout;
|
|||||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The package's stylesheets are plain CSS in `material.*` layers, with no Tailwind in them. Every
|
* The package's stylesheets are plain CSS in `material.*` layers. Every one opens with the same
|
||||||
* one opens with the same layer statement, so the order holds whichever file a bundler reaches
|
* layer statement, so the order holds whichever file a bundler reaches first; then plain imports
|
||||||
* first; then plain imports (an `@import` cannot sit inside a layer block, and `layer()` on it
|
* (an `@import` cannot sit inside a layer block, and `layer()` on it would nest the layer twice);
|
||||||
* would nest the layer twice); then its own `@layer material.<x>` blocks. The only rules outside
|
* then its own `@layer material.<x>` blocks. The only rules outside a layer are
|
||||||
* a layer are foundation/hidden.css's two `!important` ones.
|
* foundation/hidden.css's two `!important` ones.
|
||||||
*
|
*
|
||||||
* Every package component and layout stylesheet is plain CSS now (plan step 36), and Tailwind
|
* Every package component and layout stylesheet is plain CSS, so the shape and breakpoint checks
|
||||||
* itself is gone from the package (plan step 39), so the shape, Tailwind-free and breakpoint checks
|
|
||||||
* below run over the whole tree `all.css` reaches (foundation, layout and components together)
|
* below run over the whole tree `all.css` reaches (foundation, layout and components together)
|
||||||
* rather than the foundation alone, and — one file it does not reach — `showcase.css`, checked on
|
* rather than the foundation alone, and — one file it does not reach — `showcase.css`, checked on
|
||||||
* its own further down. A component's own values (colours as roles, shadows as elevation levels,
|
* its own further down. A component's own values (colours as roles, shadows as elevation levels,
|
||||||
@@ -64,7 +63,7 @@ function stylesheetImports(string $file): array
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A stylesheet and every stylesheet it reaches through `@import`, each once, in the order a
|
* A stylesheet and every stylesheet it reaches through `@import`, each once, in the order a
|
||||||
* bundler meets them. `tailwindcss` itself is a package, not a file, and is left out.
|
* bundler meets them. An import that is not relative (a package name, a URL) is left out.
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
@@ -105,9 +104,7 @@ function foundationStylesheets(): array
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Everything all.css reaches: the foundation, every layout stylesheet and every component
|
* Everything all.css reaches: the foundation, every layout stylesheet and every component
|
||||||
* stylesheet, each once. Every one of them is plain CSS (plan step 36), and Tailwind itself is
|
* stylesheet, each once. Every one of them is plain CSS.
|
||||||
* gone from the package (plan step 39) — `tailwindcss`, `tailwind.css` and its two token files no
|
|
||||||
* longer exist, so there is nothing left for this tree to exclude.
|
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
@@ -131,8 +128,7 @@ it('brings every foundation file and text.css into the foundation, and nothing e
|
|||||||
expect($parts->diff($foundation)->values()->all())->toBe([])
|
expect($parts->diff($foundation)->values()->all())->toBe([])
|
||||||
->and(array_map(fn (string $import): string => basename($import), stylesheetImports(stylesheetPath('foundation.css'))))
|
->and(array_map(fn (string $import): string => basename($import), stylesheetImports(stylesheetPath('foundation.css'))))
|
||||||
// In the order of their layers, the rules outside every layer beside the reset — the seven
|
// In the order of their layers, the rules outside every layer beside the reset — the seven
|
||||||
// token files imported directly (plan step 33: foundation/tokens.css held only these
|
// token files imported directly.
|
||||||
// imports, and only foundation.css reached it, so it was folded in here).
|
|
||||||
->toBe(['reset.css', 'hidden.css', 'scheme.css', 'shape.css', 'elevation.css', 'motion.css', 'type.css', 'state.css', 'spacing.css', 'base.css', 'interaction.css', 'text.css'])
|
->toBe(['reset.css', 'hidden.css', 'scheme.css', 'shape.css', 'elevation.css', 'motion.css', 'type.css', 'state.css', 'spacing.css', 'base.css', 'interaction.css', 'text.css'])
|
||||||
->and($foundation)
|
->and($foundation)
|
||||||
->toContain('tokens/scheme.css', 'tokens/shape.css', 'tokens/elevation.css', 'tokens/motion.css', 'tokens/type.css', 'tokens/state.css', 'tokens/spacing.css', 'tokens/font.css')
|
->toContain('tokens/scheme.css', 'tokens/shape.css', 'tokens/elevation.css', 'tokens/motion.css', 'tokens/type.css', 'tokens/state.css', 'tokens/spacing.css', 'tokens/font.css')
|
||||||
@@ -140,8 +136,8 @@ it('brings every foundation file and text.css into the foundation, and nothing e
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('paints the page in surface and on-surface, in the brand typeface, smoothed in grayscale', function () {
|
it('paints the page in surface and on-surface, in the brand typeface, smoothed in grayscale', function () {
|
||||||
// An application drops Tailwind's `antialiased` with nothing to replace it: the showcase and the
|
// An application gets it with no class of its own needed: the showcase and the error pages,
|
||||||
// error pages, which set it themselves before, read it from here too.
|
// which set it themselves before, read it from here too.
|
||||||
$base = (string) preg_replace('~/\*.*?\*/~s', '', (string) file_get_contents(stylesheetPath('foundation/base.css')));
|
$base = (string) preg_replace('~/\*.*?\*/~s', '', (string) file_get_contents(stylesheetPath('foundation/base.css')));
|
||||||
|
|
||||||
expect($base)->toMatch('/html \{\s*background-color: var\(--md-sys-color-surface\);\s*color: var\(--md-sys-color-on-surface\);\s*font-family: var\(--md-ref-typeface-brand\);\s*-webkit-font-smoothing: antialiased;\s*-moz-osx-font-smoothing: grayscale;\s*\}/')
|
expect($base)->toMatch('/html \{\s*background-color: var\(--md-sys-color-surface\);\s*color: var\(--md-sys-color-on-surface\);\s*font-family: var\(--md-ref-typeface-brand\);\s*-webkit-font-smoothing: antialiased;\s*-moz-osx-font-smoothing: grayscale;\s*\}/')
|
||||||
@@ -163,9 +159,9 @@ it('names the monospace faces once, as a typeface token the reset, the field and
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('leaves no stylesheet under resources/css/ outside all.css or showcase.css', function () {
|
it('leaves no stylesheet under resources/css/ outside all.css or showcase.css', function () {
|
||||||
// The full tree now that Tailwind is gone (plan step 39): every .css file on disk, not just the
|
// The full tree: every .css file on disk, not just the ones a bundler happens to reach — a
|
||||||
// ones a bundler happens to reach — a stray or orphaned file would show up here even though
|
// stray or orphaned file would show up here even though nothing imports it, which the checks
|
||||||
// nothing imports it, which the checks above (walking @import from all.css) cannot catch.
|
// above (walking @import from all.css) cannot catch.
|
||||||
$onDisk = collect(File::allFiles(stylesheetPath()))
|
$onDisk = collect(File::allFiles(stylesheetPath()))
|
||||||
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'css')
|
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'css')
|
||||||
->map(fn (SplFileInfo $file): string => stylesheetName((string) $file->getRealPath()))
|
->map(fn (SplFileInfo $file): string => stylesheetName((string) $file->getRealPath()))
|
||||||
@@ -257,15 +253,7 @@ it('defines x-cloak once, reached from both the foundation and all.css', functio
|
|||||||
->and(allStylesheets())->toContain(stylesheetPath('foundation/hidden.css'));
|
->and(allStylesheets())->toContain(stylesheetPath('foundation/hidden.css'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes no Tailwind directive anywhere all.css reaches', function () {
|
// A class no stylesheet declares, anywhere the package ships or tests itself, is
|
||||||
foreach (allStylesheets() as $file) {
|
|
||||||
expect(ComponentStylesheet::withoutComments(File::get($file)))
|
|
||||||
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/', stylesheetName($file))
|
|
||||||
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/', stylesheetName($file));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// No Tailwind-shaped class anywhere the package ships or tests itself is
|
|
||||||
// tests/Feature/DesignGuardTest.php's: one of its tests scans resources/views, resources/js, src
|
// tests/Feature/DesignGuardTest.php's: one of its tests scans resources/views, resources/js, src
|
||||||
// and the Workbench, another scans tests/Browser and tests/Feature, so nothing here repeats
|
// and the Workbench, another scans tests/Browser and tests/Feature, so nothing here repeats
|
||||||
// either.
|
// either.
|
||||||
@@ -309,6 +297,39 @@ it('writes a media query, anywhere all.css reaches, only at M3\'s breakpoints, i
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('writes every matchMedia width in resources/js at an M3 breakpoint', function () {
|
||||||
|
// The one check BreakpointsTest.php (deleted) covered that nothing else did: a script that
|
||||||
|
// widens or narrows window.matchMedia() by hand, at a width M3 does not know, rather than
|
||||||
|
// asking resources/js/breakpoints.js for the number. Isolated the same way that test isolated
|
||||||
|
// a media query's own condition — only the text inside matchMedia()'s call, up to its first
|
||||||
|
// closing quote or backtick — so a real width sitting elsewhere in the file, in a plain
|
||||||
|
// declaration rather than a query condition (a `min(40rem, 70dvh)`-style cap; none exists in
|
||||||
|
// resources/js today), is never mistaken for a breakpoint.
|
||||||
|
$files = collect(File::allFiles(__DIR__.'/../../resources/js'))
|
||||||
|
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'js');
|
||||||
|
|
||||||
|
expect($files)->not->toBeEmpty();
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
preg_match_all('/matchMedia\(\s*[\'"`][^\'"`]*/', $file->getContents(), $conditions);
|
||||||
|
|
||||||
|
foreach ($conditions[0] as $condition) {
|
||||||
|
preg_match_all('/(\d*\.?\d+)(rem|px)/i', $condition, $lengths, PREG_SET_ORDER);
|
||||||
|
|
||||||
|
foreach ($lengths as [, $number, $unit]) {
|
||||||
|
// breakpoints.js itself never writes a literal number — from() and upTo() build
|
||||||
|
// the string from the shared table (`${width(name)}px`) — so today's tree has none
|
||||||
|
// to check; this only guards whichever file writes one by hand next.
|
||||||
|
$allowed = strtolower($unit) === 'px' ? ['600', '840', '1200', '1600'] : ['37.5', '52.5', '75', '100'];
|
||||||
|
|
||||||
|
expect(in_array($number, $allowed, true))->toBeTrue(
|
||||||
|
$file->getRelativePathname().': '.trim($condition).' is not at an M3 breakpoint'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('declares M3\'s spacing scale as the reference gives it', function () {
|
it('declares M3\'s spacing scale as the reference gives it', function () {
|
||||||
preg_match_all(
|
preg_match_all(
|
||||||
'/^\|\s*\**space(\d+)\**\s*\|\s*\**([\d.]+)×\**\s*\|\s*\**(\d+)dp/m',
|
'/^\|\s*\**space(\d+)\**\s*\|\s*\**([\d.]+)×\**\s*\|\s*\**(\d+)dp/m',
|
||||||
@@ -526,10 +547,10 @@ it('imports, from every layout stylesheet, the stylesheet of each component its
|
|||||||
expect($checked)->toBeGreaterThan(0);
|
expect($checked)->toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Every layout stylesheet's header, layer statement and plain-imports shape, and its freedom from
|
// Every layout stylesheet's header, layer statement and plain-imports shape, and its media
|
||||||
// Tailwind and from a media query off an M3 breakpoint, are covered by the two all.css-wide tests
|
// queries off an M3 breakpoint, are covered by the two all.css-wide tests above (allStylesheets()
|
||||||
// above (allStylesheets() reaches every layout file too) — a dedicated layout-only version of
|
// reaches every layout file too) — a dedicated layout-only version of either would only repeat
|
||||||
// either would only repeat that same check on a subset already checked.
|
// that same check on a subset already checked.
|
||||||
|
|
||||||
it('keeps every layout rule in material.layout, and the visibility props in material.visibility', function () {
|
it('keeps every layout rule in material.layout, and the visibility props in material.visibility', function () {
|
||||||
// layoutStylesheets() follows every @import, so it now reaches component stylesheets a layout
|
// layoutStylesheets() follows every @import, so it now reaches component stylesheets a layout
|
||||||
@@ -607,10 +628,6 @@ it('hides an element below and from every breakpoint but compact, and nothing el
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('gives every md-type class exactly font, letter-spacing and font-variation-settings', function () {
|
it('gives every md-type class exactly font, letter-spacing and font-variation-settings', function () {
|
||||||
// Tailwind's tokens/utilities.css used to declare the same 30 styles as `type-*` utility
|
|
||||||
// blocks, and this test proved text.css's `.md-type-*` classes matched them; deleted with
|
|
||||||
// Tailwind (plan step 39), text.css is the only copy left, so the shape it gives each class is
|
|
||||||
// asserted directly rather than cross-checked against a second copy.
|
|
||||||
preg_match_all('/\.md-(type-[\w-]+) \{\n(.*?)\n {4}\}/s', File::get(stylesheetPath('text.css')), $matches, PREG_SET_ORDER);
|
preg_match_all('/\.md-(type-[\w-]+) \{\n(.*?)\n {4}\}/s', File::get(stylesheetPath('text.css')), $matches, PREG_SET_ORDER);
|
||||||
|
|
||||||
$classes = collect($matches)->mapWithKeys(fn (array $match): array => ["md-{$match[1]}" => ComponentStylesheet::flatDeclarations($match[2])]);
|
$classes = collect($matches)->mapWithKeys(fn (array $match): array => ["md-{$match[1]}" => ComponentStylesheet::flatDeclarations($match[2])]);
|
||||||
@@ -672,27 +689,16 @@ it('ships exactly the documented text classes, each ink an M3 role', function ()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// A test used to prove foundation/interaction.css's `.md-*` classes matched tokens/utilities.css's
|
it('opens the Workbench\'s one CSS entry with the layer statement, then all.css, showcase.css and the scheme', function () {
|
||||||
// utility blocks byte for byte apart from two deliberate differences, because the components
|
|
||||||
// still written in Tailwind read the utilities while every other component read the classes.
|
|
||||||
// Deleted with Tailwind (plan step 39): every component reads the classes now, there is no second
|
|
||||||
// copy left to drift from, and interaction.css's own StylesheetsTest coverage above (the layer it
|
|
||||||
// writes into, no Tailwind directive) already holds it to the same shape every other foundation
|
|
||||||
// file keeps to.
|
|
||||||
|
|
||||||
it('builds the package in the Workbench\'s one CSS entry, with no Tailwind left to sit between', function () {
|
|
||||||
$app = ComponentStylesheet::withoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css'));
|
$app = ComponentStylesheet::withoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css'));
|
||||||
|
|
||||||
// Opens by ordering the layers, so the order holds whichever the page links first — an
|
// Opens by ordering the layers, so the order holds whichever the page links first.
|
||||||
// application with Tailwind of its own still opens its own entry the same way (its header).
|
|
||||||
expect(trim($app))->toStartWith(MATERIAL_LAYER_STATEMENT)
|
expect(trim($app))->toStartWith(MATERIAL_LAYER_STATEMENT)
|
||||||
->toContain("@import '../../../resources/css/all.css';")
|
->toContain("@import '../../../resources/css/all.css';")
|
||||||
->toContain("@import '../../../resources/css/showcase.css';")
|
->toContain("@import '../../../resources/css/showcase.css';")
|
||||||
->toContain("@import './material-scheme.css';")
|
->toContain("@import './material-scheme.css';")
|
||||||
->not->toMatch('/@(?:tailwind|source|theme|utility|variant|custom-variant|apply|config|plugin|reference)\b/')
|
|
||||||
->and(File::get(__DIR__.'/../../vite.config.js'))
|
->and(File::get(__DIR__.'/../../vite.config.js'))
|
||||||
->toContain("input: ['workbench/resources/css/app.css', 'workbench/resources/js/app.js']")
|
->toContain("input: ['workbench/resources/css/app.css', 'workbench/resources/js/app.js']")
|
||||||
->not->toContain('tailwindcss')
|
|
||||||
->and(config('livewire-material.showcase.vite'))->toBe([
|
->and(config('livewire-material.showcase.vite'))->toBe([
|
||||||
'workbench/resources/css/app.css',
|
'workbench/resources/css/app.css',
|
||||||
'workbench/resources/js/app.js',
|
'workbench/resources/js/app.js',
|
||||||
@@ -705,10 +711,7 @@ it('shapes showcase.css like a package stylesheet, with its documented unlayered
|
|||||||
$items = ComponentStylesheet::items(ComponentStylesheet::withoutComments($css));
|
$items = ComponentStylesheet::items(ComponentStylesheet::withoutComments($css));
|
||||||
|
|
||||||
expect($css)->toStartWith('/*', 'showcase.css has no header comment')
|
expect($css)->toStartWith('/*', 'showcase.css has no header comment')
|
||||||
->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, 'showcase.css does not open with the layer statement')
|
->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, 'showcase.css does not open with the layer statement');
|
||||||
->and(ComponentStylesheet::withoutComments($css))
|
|
||||||
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/')
|
|
||||||
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/');
|
|
||||||
|
|
||||||
$blocks = false;
|
$blocks = false;
|
||||||
$unlayered = [];
|
$unlayered = [];
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ function typeStyles(): array
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The body of every `.md-type-…` class in text.css, keyed by class name without the `md-` prefix
|
* The body of every `.md-type-…` class in text.css, keyed by class name without the `md-` prefix
|
||||||
* (`type-display-lg`, `type-emphasized-display-lg`, …). Plain CSS now (plan step 39 deleted the
|
* (`type-display-lg`, `type-emphasized-display-lg`, …). Plain CSS; text.css is theirs alone.
|
||||||
* Tailwind utilities they used to be); text.css is theirs alone.
|
|
||||||
*
|
*
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
@@ -86,14 +85,9 @@ it('ships M3\'s medium and high contrast levels beside the standard one', functi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tailwind's theme.css used to turn every scheme role into a `--color-*` Tailwind colour, in an
|
// Every package rule reads a role's `--md-sys-color-*` custom property directly at its point of
|
||||||
// inline theme block so it resolved on the element and a nested data-theme still repainted it
|
// use: no second alias computed once on `:root` that a nested `data-theme` could leave stale. The
|
||||||
// rather than reading a value computed once on :root. Both went with Tailwind (plan step 39): the
|
// scheme test above already proves every role is `--md-sys-color-*`, light and dark.
|
||||||
// first fact is already the scheme test above (every role a `--md-sys-color-*` custom property,
|
|
||||||
// light and dark); the second no longer has anything to hold, because no file replaces theme.css's
|
|
||||||
// alias — every package rule reads `--md-sys-color-*` directly at its point of use, so there is no
|
|
||||||
// second custom property that could be computed once on :root and go stale under a nested
|
|
||||||
// data-theme in the first place.
|
|
||||||
it('never aliases a colour role to a second custom property resolved once on :root', function () {
|
it('never aliases a colour role to a second custom property resolved once on :root', function () {
|
||||||
foreach (File::allFiles(packageCss()) as $file) {
|
foreach (File::allFiles(packageCss()) as $file) {
|
||||||
expect($file->getContents())->not->toMatch('/--color-[\w-]+:\s*var\(--md-sys-color-/', $file->getRelativePathname());
|
expect($file->getContents())->not->toMatch('/--color-[\w-]+:\s*var\(--md-sys-color-/', $file->getRelativePathname());
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/*
|
/*
|
||||||
* An application's own CSS entry, the way plan step 37's claim is about: outside the package, the
|
* An application's own CSS entry: outside the package, the foundation first, then the stylesheet
|
||||||
* foundation first, then the stylesheet of each component its views render. Split button, app bar,
|
* of each component its views render. Split button, app bar, modal and pagination each import
|
||||||
* modal and pagination each import button.css (and icon.css, through it), and the application
|
* button.css (and icon.css, through it), and the application names button.css itself once more
|
||||||
* names button.css itself once more at the end. Built by tests/Fixtures/dedup.vite.config.mjs for
|
* at the end. Built by tests/Fixtures/dedup.vite.config.mjs for StylesheetsBundleTest.php, which
|
||||||
* StylesheetsBundleTest.php, which expects every shared rule once, at its first position.
|
* expects every shared rule once, at its first position.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import '../../resources/css/foundation.css';
|
@import '../../resources/css/foundation.css';
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// Builds two CSS entries with no plugin in the graph, isolating Vite's own bundled postcss-import,
|
// Builds two CSS entries with no plugin in the graph, isolating Vite's own bundled postcss-import,
|
||||||
// whose `skipDuplicates` option is the mechanism docs/plans/material-3-alignment.md's "Tailwind's
|
// whose `skipDuplicates` option is the deduplication StylesheetsBundleTest.php pins:
|
||||||
// footprint" paragraph measured and StylesheetsBundleTest.php pins: resources/css/all.css, and
|
// resources/css/all.css, and tests/Fixtures/dedup-app.css, an application-shaped entry importing
|
||||||
// tests/Fixtures/dedup-app.css, an application-shaped entry importing per-component stylesheets
|
// per-component stylesheets that share button.css. A stylesheet several files import lands once
|
||||||
// that share button.css. A stylesheet several files import lands once per entry, at its first
|
// per entry, at its first position.
|
||||||
// position.
|
|
||||||
//
|
//
|
||||||
// Not the Workbench's config: `@tailwindcss/vite` inlines the imports of any entry that uses
|
// Not the Workbench's config: this one carries no plugin, which is what isolates Vite's own
|
||||||
// Tailwind itself, without that deduplication, so a shared stylesheet repeats there. The Workbench
|
// postcss-import above. The Workbench's config carries laravel-vite-plugin, so its build is not
|
||||||
// keeps the package's CSS in an entry of its own (workbench/resources/css/package.css) for that
|
// this dedup test's to run.
|
||||||
// reason, until plan step 39 removes Tailwind.
|
|
||||||
//
|
//
|
||||||
// DEDUP_OUT_DIR is required: the test builds into a temporary directory and deletes it, and nothing
|
// DEDUP_OUT_DIR is required: the test builds into a temporary directory and deletes it, and nothing
|
||||||
// should land in the package's own tree, least of all workbench/public/build.
|
// should land in the package's own tree, least of all workbench/public/build.
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="sm:flex md:grid lg:hidden xl:block 2xl:contents">
|
|
||||||
<span class="max-sm:hidden max-md:flex max-lg:grid max-xl:block max-2xl:contents"></span>
|
|
||||||
</div>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<div class="bg-[#1d7afc] text-[rgb(0_0_0)] border-[hsl(210_80%_50%)]">
|
|
||||||
<span class="fill-[oklch(0.7_0.1_250)] ring-[color-mix(in_oklab,var(--x)_50%,transparent)]"></span>
|
|
||||||
<span class="bg-white text-black border-white"></span>
|
|
||||||
<span class="text-on-surface/60 bg-on-surface/12 border-outline/38"></span>
|
|
||||||
<span class="md-ink md-type-body-md">M3's own are fine</span>
|
|
||||||
</div>
|
|
||||||
+4
@@ -5,3 +5,7 @@
|
|||||||
.bg-brand {
|
.bg-brand {
|
||||||
background: var(--brand);
|
background: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-hero {
|
||||||
|
padding: var(--md-sys-measurement-space200);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div x-data="{ view: 'grid' }" :class="{ 'is-open': open, collapsed: !open, 'md-ink': view === 'grid' }" wire:loading.class="dashboard-busy"
|
||||||
|
x-transition:enter="dashboard-enter" x-transition:enter-start="dashboard-enter-start">
|
||||||
|
<ul @class(['dashboard-hero', 'share-list', 'md-ink-quiet' => $status === 'hidden', 'md-ink' => isActive('block')])
|
||||||
|
{{ $attributes->merge(['class' => 'dashboard-lead']) }}>
|
||||||
|
<li class="md-truncate
|
||||||
|
share-list">Row</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{{-- Real application lines: a quoted string that is an array index ($block['base'],
|
||||||
|
$header['class'], $tab['iconClass']) reads a value, not a class list. The keys around it and
|
||||||
|
both branches of a ternary are classes, and are read. --}}
|
||||||
|
<div @class(['product-shot-base' => $block['base']])></div>
|
||||||
|
<x-sort-header :sort-by="$sortBy" :column="$header['key']" :class="$header['class'] ?? null" />
|
||||||
|
<x-icon :name="$tab['icon']" size="20" :class="$tab['iconClass']" />
|
||||||
|
<x-row :class="$band['easy'] ? 'md-type-title-sm' : 'md-type-body-md'" />
|
||||||
|
<div @class(['routes-show-run-chosen' => $referenceRunId === $run['id']])></div>
|
||||||
|
<x-badge :class="\Illuminate\Support\Arr::toCssClasses(['shot-helper-one', 'shot-helper-two' => $on])" />
|
||||||
|
<span :class="$on ? 'shot-tern-a' : ($off ? 'shot-tern-b' : 'shot-tern-c')"></span>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p class="md-type-title-md md-ink-variant showcase-w-narrow">Every one of these is a package rule.</p>
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{{-- Running text is not a class list: none of Tailwind's bare-word utilities should be read out
|
|
||||||
of prose just because they share a spelling with one. --}}
|
|
||||||
<p>This creates a grid of cards, in a flex layout that stays hidden below medium and stacks into a
|
|
||||||
block once the pane narrows. Nothing here truncates or grows, and the sample is not a table.</p>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<div class="rounded-none rounded-xs rounded-sm rounded-md rounded-lg">
|
|
||||||
<span class="rounded-xl rounded-2xl rounded-3xl rounded-4xl rounded-full"></span>
|
|
||||||
<span class="rounded-t-lg rounded-se-2xl"></span>
|
|
||||||
<span class="md-type-body-md" style="border-radius: var(--md-sys-shape-corner-lg)">M3's own are fine</span>
|
|
||||||
</div>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<div class="shadow-2xs shadow-xs shadow-sm shadow-md">
|
|
||||||
<span class="shadow-lg shadow-xl shadow-2xl"></span>
|
|
||||||
<span class="md-type-body-md" style="box-shadow: var(--md-sys-elevation-1)">M3's own are fine</span>
|
|
||||||
</div>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<div class="ease-linear ease-in ease-out ease-in-out">
|
|
||||||
<span class="duration-75 duration-300 duration-1000"></span>
|
|
||||||
<span class="md-type-body-md">M3's own are fine</span>
|
|
||||||
</div>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// A plain CSS keyword read out of an API, not a Tailwind class: matches DesignGuard's easing and
|
|
||||||
// duration families only in a class list, never a bare JS string like this one.
|
|
||||||
const easing = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'linear' : 'ease-out';
|
|
||||||
const duration = matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 300;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<div class="text-xs text-sm text-base text-lg text-xl text-2xl text-9xl">
|
|
||||||
<span class="font-thin font-extralight font-light font-normal font-medium"></span>
|
|
||||||
<span class="font-semibold font-bold font-extrabold font-black"></span>
|
|
||||||
<span class="leading-none leading-tight leading-snug leading-normal leading-relaxed leading-loose leading-6"></span>
|
|
||||||
<span class="tracking-tighter tracking-tight tracking-normal tracking-wide tracking-wider tracking-widest"></span>
|
|
||||||
<span class="md-type-body-md md-type-title-lg md-type-emphasized-label-lg">M3's own are fine</span>
|
|
||||||
</div>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import 'tailwindcss';
|
@import '@fontsource/roboto';
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Roboto');
|
@import url('https://fonts.googleapis.com/css2?family=Roboto');
|
||||||
@import './app-theme.css' layer(app);
|
@import './app-theme.css' layer(app);
|
||||||
@import '../../../../resources/css/foundation.css';
|
@import '../../../../resources/css/foundation.css';
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<div class="top-[10px] grid-cols-[1fr_2fr]">
|
|
||||||
<span class="flex!"></span>
|
|
||||||
</div>
|
|
||||||
<div class="[--material-bottom-bar:5rem] [font-variation-settings:'ROND'_100]"></div>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<div x-data="{ view: 'grid' }" :class="{ 'is-open': open, hidden: !open, 'md-ink': view === 'grid' }" wire:loading.class="opacity-50"
|
|
||||||
x-transition:enter="transition ease-out" x-transition:enter-start="scale-95">
|
|
||||||
<ul @class(["gap-2", 'share-list', 'md-ink-quiet' => $status === 'hidden', 'md-ink' => isActive('block')])
|
|
||||||
{{ $attributes->merge(['class' => 'mt-2']) }}>
|
|
||||||
<li class="md-truncate
|
|
||||||
flex">Row</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<span class="duration-(--md-sys-motion-spatial-fast-duration)"></span>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="hidden block">
|
|
||||||
<span class="inline-block invisible"></span>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<span class="text-on-primary-container bg-background text-current md-ink-variant"></span>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="flex grid inline-flex flex-col">
|
|
||||||
<span class="grid-cols-3 col-span-2 items-center justify-between gap-4"></span>
|
|
||||||
</div>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<div class="text-on-surface-variant border-outline-variant"></div>
|
|
||||||
<span class="bg-surface-container bg-primary"></span>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="w-full h-screen min-w-0 max-w-md">
|
|
||||||
<span class="size-4"></span>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="p-4 px-2 mt-2 space-y-4">
|
|
||||||
<span class="m-4 space-x-2"></span>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<p class="text-center truncate">
|
|
||||||
<span class="sr-only whitespace-nowrap line-clamp-2 tabular-nums"></span>
|
|
||||||
</p>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<div class="relative inset-0 -top-2 z-10 overflow-hidden shrink-0 col-start-2 justify-self-end container">
|
|
||||||
<span class="border border-t-2 rounded shadow outline-none ring-2 transition opacity-50 -rotate-45 cursor-pointer select-none"></span>
|
|
||||||
<span class="aspect-video object-cover font-mono font-sans antialiased underline uppercase text-end list-disc table bg-clip-text"></span>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="hover:underline dark:opacity-50">
|
|
||||||
<span class="group-hover:flex focus:md:gap-2"></span>
|
|
||||||
</div>
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<x-icon name="o-home" />
|
<x-icon name="o-home" />
|
||||||
<x-button icon="calendar_month" icon-right="not_a_symbol" label="Fine" />
|
<x-button icon="calendar_month" icon-right="not_a_symbol" label="Fine" />
|
||||||
<span>text-red-600</span>
|
<span>text-red-600</span>
|
||||||
<p class="text-tertiary focus-ring">Tailwind's and ours</p>
|
<p class="text-tertiary focus-ring">Classes no stylesheet declares</p>
|
||||||
<x-icon :name="$dynamic" />
|
<x-icon :name="$dynamic" />
|
||||||
<x-icon name="home" @class(['size-4']) />
|
<x-icon name="home" @class(['size-4']) />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ if (($timeout = (int) getenv('BROWSER_TIMEOUT')) > 0) {
|
|||||||
/**
|
/**
|
||||||
* The named block of resources/css/all.css — the content between its `/* Heading *\/` comment and
|
* The named block of resources/css/all.css — the content between its `/* Heading *\/` comment and
|
||||||
* the next one, or the end of the file for the last block ("Navigation"). components.css and
|
* the next one, or the end of the file for the last block ("Navigation"). components.css and
|
||||||
* layout.css grouped their imports the same way before plan step 37 folded both into all.css; this
|
* layout.css grouped their imports the same way before all.css folded both together; this
|
||||||
* is the one place that block lookup lives now, so a stylesheet test asks "is this imported from
|
* is the one place that block lookup lives now, so a stylesheet test asks "is this imported from
|
||||||
* the Containment block" without repeating the search in every file that needs it. It is here,
|
* the Containment block" without repeating the search in every file that needs it. It is here,
|
||||||
* rather than in tests/Feature/StylesheetsTest.php, because Pest.php is always loaded, whichever
|
* rather than in tests/Feature/StylesheetsTest.php, because Pest.php is always loaded, whichever
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
namespace NoNameWeb\LivewireMaterial\Tests\Support;
|
namespace NoNameWeb\LivewireMaterial\Tests\Support;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The classes a package view rewritten without Tailwind may write (plan step 36; the brief's
|
* The classes a package view may write: the three interaction classes from
|
||||||
* "Interaction is the shared classes"): the three interaction classes from
|
* foundation/interaction.css, the fixed text classes resources/css/text.css defines, and a
|
||||||
* foundation/interaction.css, the fixed text classes resources/css/text.css defines (plan step 34,
|
* caller's own class handed on whole. Everything else a view draws comes from its `data-md-*`
|
||||||
* "The component views use the same set"), and a caller's own class handed on whole. Everything
|
* hooks.
|
||||||
* else a view draws comes from its `data-md-*` hooks.
|
|
||||||
*
|
*
|
||||||
* One rule for every group's stylesheet test, so the groups cannot drift apart.
|
* One rule for every group's stylesheet test, so the groups cannot drift apart.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class WorkbenchServiceProvider extends ServiceProvider
|
|||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
config(['livewire-material.showcase.vite' => [
|
config(['livewire-material.showcase.vite' => [
|
||||||
// The package's whole CSS in the Workbench's one entry now that Tailwind is gone
|
// The package's whole CSS in the Workbench's one entry, and the JavaScript.
|
||||||
// (plan step 39), and the JavaScript.
|
|
||||||
'workbench/resources/css/app.css',
|
'workbench/resources/css/app.css',
|
||||||
'workbench/resources/js/app.js',
|
'workbench/resources/js/app.js',
|
||||||
]]);
|
]]);
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
/* The package's CSS for the Workbench, built the way an application without Tailwind builds it
|
/* The package's CSS for the Workbench, built the way an application builds it: all.css (the
|
||||||
(plan step 39: Tailwind leaves the whole stack): all.css (the foundation, every layout and every
|
foundation, every layout and every component stylesheet), showcase.css (the showcase's own
|
||||||
component stylesheet), showcase.css (the showcase's own chrome, so a real Vite build — an error
|
chrome, so a real Vite build — an error page's fallback-free path, a browser-test probe page's
|
||||||
page's fallback-free path, a browser-test probe page's `@vite()` — carries it too, even though
|
`@vite()` — carries it too, even though the showcase's own pages take their CSS from the bundle
|
||||||
the showcase's own pages take their CSS from the bundle route instead) and the Workbench's
|
route instead) and the Workbench's scheme, in the one CSS entry the Workbench needs. Vite's own
|
||||||
scheme, in the one CSS entry the Workbench now needs. Vite's own CSS pipeline inlines the
|
CSS pipeline inlines the imports here and keeps a stylesheet several components import once, at
|
||||||
imports here and keeps a stylesheet several components import once, at its first position — the
|
its first position — the order an application gets.
|
||||||
order an application gets.
|
|
||||||
|
|
||||||
Opens with the same layer statement every package stylesheet does, so the `material` layers hold
|
Opens with the same layer statement every package stylesheet does, so the `material` layers hold
|
||||||
their place whichever file a page links first; an application with no Tailwind of its own needs
|
their place whichever file a page links first; an application needs nothing more than this. */
|
||||||
nothing more than this. */
|
|
||||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
@import '../../../resources/css/all.css';
|
@import '../../../resources/css/all.css';
|
||||||
@import '../../../resources/css/showcase.css';
|
@import '../../../resources/css/showcase.css';
|
||||||
|
|||||||
Reference in New Issue
Block a user