Count a row written by hand in the design guard's stylesheet checks

`data-md-list-row` on an application's own `<li>`, `<div>`, `<tr>` or
`<x-row>` draws its hover, focus and press state layer, its cursor and
its `data-md-selected` fill from list-item.css, but missingStylesheets()
and unusedStylesheets() read only component tags and `->links()`.
ReStride had the stylesheet only because table.css imports it: had its
last `<x-table>` gone, every hand-made row would have lost its states
and its fill without a finding, and importing list-item.css for the
rows directly was reported unused.

A Blade view outside the package's own `resources/views` that writes a
hook in the new `HOOK_STYLESHEETS` now needs that hook's stylesheet in
both checks, named at the earliest line that needs it. The one entry is
`data-md-list-row` → `components/list-item.css`, except on `<x-card>`
(any spelling), whose row card.css draws; the card's opening tag is
blanked out before the hook is read, and a selector such as
`[data-md-list-row]` or a name in backticks writes no row. Nothing else
list-rows.js, the skill or the README has an application write needs a
stylesheet of its own: `data-md-list-open` and a row's
`data-md-selected` are drawn only through the row, a selected row in
`<x-table>` by table.css, `data-md-dragged` by foundation.css's
`md-state-layer` or card.css, `data-md-field-control` by field.css
inside `<x-field>`. The constant's docblock says so, and the guard's
docblocks, the README, the skill and UPGRADE.md describe the check.

A fixture test reads a hand-made `<li>` and `<tr>` row and a card-only
view (plain and namespaced `<x-card data-md-list-row>`, and a selector
in a script) against both checks; it fails without the change. A
second test scans the package's list-item and card views and expects
no row finding; it fails when package views are read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-17 08:05:33 +02:00
co-authored by Claude Opus 5
parent b62c83f244
commit 471d927e64
8 changed files with 208 additions and 47 deletions
@@ -514,7 +514,7 @@ The `illustration` slot draws the application's own artwork in place of the shap
`variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`, `heading` (the title's element, `h3` by default: pass `h2` for a card straight under the page's `h1`); slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). A card holds content and actions about one subject (M3); a table of many rows or a group of settings is a headed section, not a card. Choose its fill with `variant`, not a background of your own.
A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `<a>` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. `data-md-selected` on a row that is not a card — your own `<li>`, `<div>` or `<tr>`, the one whose sheet is open — fills it secondary-container with on-secondary-container ink, the state layer over the fill (`<x-list-item selected>` writes it itself); a card takes no selected fill.
A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `<a>` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. `data-md-selected` on a row that is not a card — your own `<li>`, `<div>` or `<tr>`, the one whose sheet is open — fills it secondary-container with on-secondary-container ink, the state layer over the fill (`<x-list-item selected>` writes it itself); a card takes no selected fill. A row that is not a card draws its state layer and that fill from `components/list-item.css`, so the CSS entry imports it even where no `<x-list-item>` or `<x-table>` renders (the design guard's `missingStylesheets()` asks for it).
```blade
<x-card variant="outlined" data-md-list-row wire:key="share-{{ $share->id }}">
@@ -1212,7 +1212,7 @@ In the `.css` files it is given (`material-scheme.css` skipped) it fails on a li
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.
`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::…>`) and `->links()`; 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, 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; `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.
## Conventions