Files
livewire-material/docs/plans/livewire-material.md
T
Andreas Reinhold / reiniandClaude Opus 5 9b53891a8e
tests / lint (push) Successful in 1m14s
tests / feature (8.4) (push) Successful in 56s
tests / feature (8.5) (push) Successful in 56s
tests / browser (chrome, chromium) (push) Successful in 2m9s
tests / browser (firefox, firefox) (push) Successful in 1m34s
tests / browser (safari, webkit) (push) Successful in 1m41s
Scaffold the Livewire Material package
The skeleton for nonameweb/livewire-material: service provider and config
(unprefixed components, blade-icons' own <x-icon> switched off, an opt-in
showcase at /material), the Testbench Workbench with its Vite build, the
CSS and JS entry points applications import, the Boost guideline and skill
with a drift test, and Gitea CI running Pint, feature tests on PHP 8.4 and
8.5, and browser tests in Chrome, Firefox and Safari.

The plan moved here from SealShare: docs/plans/livewire-material.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 04:59:21 +02:00

409 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Livewire Material: a shared Material 3 Expressive component library
> Split on 2026-09-13 from SealShare's `docs/plans/livewire-material.md`, where the plan was
> made. SealShare's adoption (Phase 11) stays in SealShare's copy.
## Goal
ReStride has just left maryUI and daisyUI for **Material 3 Expressive on its own Blade
components** (`ReStride/docs/plans/material-expressive.md`, done 2026-09-10). Doing that
again, by hand, in every Laravel + Livewire app is what this package avoids.
**`nonameweb/livewire-material`** carries the whole current M3 Expressive component catalogue
as Blade components for Livewire, together with everything around them — colour scheme
generation, tokens, the theme script, Material Symbols, Google Sans Flex, motion, error pages,
a mail theme, a showcase, test helpers and AI guidelines. SealShare converts to it once it is
complete (`1.0.0`); ReStride adopts it later, in a plan of its own.
## Context
**Stacks.** SealShare: Laravel 13.31, Livewire 4.4, maryUI 2.9.10 (no prefix), daisyUI 5.7,
Tailwind 4.3, Pest 5.1, Octane on FrankenPHP, PHP 8.5; public on GitHub under MIT, image
published to `ghcr.io/surtic86/sealshare`. ReStride: same Laravel / Livewire / Tailwind / Pest,
private on `gitea.nonameweb.ch`, CI through Gitea act_runner.
**What ReStride already solved** (and the package generalises):
- 45 anonymous components in `resources/views/components/`, maryUI's names and props, M3
styling; class components only where PHP earns it.
- `resources/css/material/{color,shape,type,motion,elevation,field,menu,table,list}.css`;
`@theme` (never `inline`) mapping M3 roles to utilities; `--color-*: initial` so only tokens
compile; `:root` dark + `[data-theme=light]`, each with its own `color-scheme`.
- Material Symbols Rounded (400/0/24) as local SVGs registered as `blade-icons` sets
(`ms`/`msf`), `config/blade-icons.php` turning off blade-icons' own `<x-icon>`.
- Google Sans Flex, a 63 KB Latin subset (weight 400700, ROND 0100).
- Spring motion as CSS `linear()` curves; press shape-morph; Expressive shapes generated by
formula into `resources/svg/shapes`.
- `App\Livewire\Concerns\Toasts` (protected `success|error|warning|info`, dispatching a window
event) and a snackbar `<x-toast>`.
- ~30 component tests (`$this->blade()`), browser tests (Pest browser plugin), and guard tests
(`DesignLanguageTest`, `MaterialTokensTest`, `MaterialSymbolsTest`).
- The Livewire traps, recorded in `../ReStride/.ai/rules/ui.md`: `wire:ignore.self` on a
`showModal()` dialog; never pass `hidden` or a position to a component (wrap it); Blade
directives do not compile inside a component tag's attributes; `wire('model')->value()` is
`false`, not `null`, without `wire:model`; a field's states are CSS selectors (`:has()`),
marked with `data-invalid` / `data-floated` / `data-readonly` where the control cannot carry
them; the customizable select's traps (`../ReStride/.ai/rules/components-views-components.md`).
- Mail on M3 (`../ReStride/docs/plans/material-mail.md`): one theme CSS inlined onto bare
tags, light only, because `CssToInlineStyles` strips `@media`.
**What in ReStride is app-specific** and does not move: the doctrine (two button weights, no
tertiary / primary-container — enforced by its build), `restride-theme`, `$store.install` /
`$store.connection` in the shell, `--bottom-bar` set by its layout, sport / zone / route
tokens, `training-row`, `map-shell`, `map-chip`, `product-shot`, `star-rating`,
`share-button`, the unDraw repaint, the Ace code editor.
**Constraints found.**
- `gitea.nonameweb.ch` requires sign-in to view anything (`/explore/repos` → login, API 403).
A "public" repo there cannot be installed anonymously until that changes.
*(Resolved in step 1.)*
- Laravel's Markdown mail accepts a namespaced view as theme
(`Illuminate/Mail/Markdown.php:114`), so a package can render the theme CSS from data.
- Laravel **replaces** the `errors` view namespace at render time with
`config('view.paths')` + `/errors` and the framework's own
(`Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php`), so error views a package
adds with `addNamespace('errors', …)` are wiped; only a path in `view.paths` survives.
- M3 Expressive deprecates bottom app bar, navigation drawer, the original navigation bar,
segmented button, small FAB and medium/large top app bar (material-components-android docs).
- CSS anchor positioning is in Chrome 125+, Firefox 147+, Safari 18.4+ (flip via
`@position-try` from Safari 18.4).
- Boost loads a package's `resources/boost/guidelines/core.blade.php` and its skills on
`boost:install` / `boost:update --discover`.
## Decisions
- **A shared Composer package, SealShare its first consumer; ReStride adopts later in its own
plan** — SealShare's small surface proves the API; ReStride's three-day-old migration is not
put at risk.
- **Public repo on `gitea.nonameweb.ch`, MIT** — SealShare is public; its CI, Docker build,
manual install and fork PRs must install the package without credentials. SealShare
requires it through a `vcs` repository entry. Material Symbols (Apache-2.0), Google Sans
Flex (OFL) and material-web token values (Apache-2.0) are compatible; attributions ship in
`NOTICE`.
- **Prerequisite: Gitea allows anonymous reads** (`[service] REQUIRE_SIGNIN_VIEW = false`, or
`expensive` on 1.23+) — otherwise "public" is not installable.
- **Name `nonameweb/livewire-material`, namespace `NoNameWeb\LivewireMaterial`,
`config/livewire-material.php`, views `livewire-material::`, commands `material:*`** — says
what it is and what it is for.
- **Components unprefixed by default, prefix configurable** (maryUI's model, via
`Blade::anonymousComponentPath($path, $prefix)`) — ReStride's call sites keep their names;
SealShare's maryUI tags keep theirs. The provider sets `blade-icons.components.default` to
`null`, or blade-icons' class `<x-icon>` beats ours.
- **CSS and JS imported from `vendor/`** (`@import`, `@source`, `import`) — one dependency, one
version; consuming Dockerfiles install Composer packages before the Vite build.
`@source` covers the package's views and PHP, never its SVG folders.
- **The full Material Symbols Rounded set** (400 / 0 / 24, outlined and filled, ~7,800 SVGs,
~34 MB), produced by a maintenance script in the package repo, not fetched at runtime —
any name works in any app; `icons:cache` keeps lookups cheap under Octane.
- **Google Sans Flex bundled** (ReStride's subset) via `@font-face` in the package CSS; an app
overrides `--font-sans`.
- **Full M3, not ReStride's doctrine** — every variant, colour role (tertiary and
primary-container included) and container is available; each app enforces its own rules
through a configurable guard helper the package ships.
- **`variant` + `color` props** — `variant="filled|tonal|outlined|text|elevated"`,
`color="primary|secondary|tertiary|error|success|warning|info"`, default `text` in primary;
both validated against fixed lists, unknown values fall back to the default. Shorthands
kept: `primary` = filled primary, `danger` = filled error, `caution` = filled warning. The
same pattern for badge, alert, chip, icon button and FAB (`tone` stays an alias of `color`
where ReStride used it).
- **`php artisan material:scheme {seed} --variant=`** — runs Google's
`material-color-utilities` from a single prebundled Node script inside the package; writes
the app's `resources/css/material-scheme.css` (dark and light `--md-sys-color-*`, plus
`success`/`warning`/`info` custom colours, harmonisation off) and
`resources/css/material-scheme.json` (the light hexes, for the mail theme). Output is
committed.
- **Theme: a head script component, `light | dark | system`** — sets `data-theme` before paint;
CSS keys only on the attribute and never asks `prefers-color-scheme`; only the script reads
the OS (and follows its changes while `system`). `$store.theme` is the one state every
toggle shares. Config: `theme.default`, `theme.storage_key`, `theme.legacy_keys` (adopted
once, then removed).
- **The whole M3 Expressive catalogue, current components only** — the six deprecated ones are
skipped; their names alias where free (`<x-group>` renders a connected button group, FAB
`size="sm"` renders medium).
- **Plus the non-M3 pieces apps need** — data table, sort header, pagination views, file input,
password, stat, alert, empty state, collapse, section nav, account menu, theme toggle, an
adaptive app-shell composition, error pages, mail theme.
- **Modern browser floor, no third-party JS** — Chrome 125+, Firefox 147+, Safari 18.4+:
native `<dialog>`, Popover API, CSS anchor positioning, customizable `<select>` as a
progressive enhancement; Alpine (bundled with Livewire) for behaviour. Date and time
pickers, carousel and search are our own.
- **PHP ^8.4, Laravel ^13, Livewire ^4.** No request state in singletons (Octane).
- **Strings through `__()` with an `en` file**, publishable and overridable.
- **Accessibility: WAI-ARIA Authoring Practices patterns, WCAG 2.2 AA contrast, full keyboard.**
- **Showcase in the package, mounted twice** — by the Workbench for development and browser
tests, and opt-in inside an app (`showcase.enabled`, default only when `APP_ENV=local`) so
an app sees every component in its own scheme. Every variant, colour, size and state, a
light/dark/system switch, and the Blade snippet beside each example.
- **Tests: render tests for every component, browser tests against the Workbench showcase in
Chromium, Firefox and WebKit; no screenshot diffs.**
- **Error pages and a mail theme in the package** — the mail theme is the namespaced view
`livewire-material::mail.theme`, rendering CSS from the app's `material-scheme.json`, so
mail colours cannot drift from the app.
- **Boost guideline + `livewire-material-development` skill** in the package, and a test that
fails when a component is missing from the skill.
- **Versioning: `0.x` per wave, `1.0.0` when the catalogue is complete.**
- **Semantic ink and line utilities carried over from ReStride** (`text-body`, `text-meta`,
`text-quiet`, `border-structure`, `border-chrome`, `border-divider`) — cheap `@theme` names
ReStride's templates already use, derived from M3 roles.
## Out of scope
- ReStride adopting the package — its own plan, after `1.0.0`.
- A brand colour chosen at runtime (e.g. in SealShare's admin), dynamic colour, a PHP port of
the colour maths.
- M3 Expressive's deprecated components (bottom app bar, navigation drawer, original
navigation bar, segmented button, small FAB, medium/large top app bar).
- Screenshot / visual regression tests.
- Blaze optimisation of the components — a follow-up if rendering gets slow.
- ReStride-specific components and tokens (training row, maps, product shot, star rating, share
button, sport/zone/route tokens, unDraw repaint, Ace code editor).
- Publishing on Packagist — the `vcs` repository is enough; revisit if others adopt it.
- Translations beyond `en`.
- Browsers below the floor.
- Non-Livewire stacks (Inertia, React, Vue).
## Implementation steps
### Phase 0 — Prerequisites
1. **Gitea.** *Done 2026-09-13.* `GITEA__service__REQUIRE_SIGNIN_VIEW=false` in the Gitea
compose service (Gitea 1.26.4); `/explore/repos` and the API answer anonymously, and no
other repo or org is public. The package lives in the public org **`noNameWEB`**:
`https://gitea.nonameweb.ch/noNameWEB/livewire-material.git` (public, empty, `main`,
Actions on). SealShare's `vcs` repository entry uses that URL.
2. **Split the plan.** *Done 2026-09-13.* This file; SealShare keeps its adoption steps.
### Phase 1 — Package skeleton (`0.1.0`)
3. **Repository.** `composer.json` (`nonameweb/livewire-material`, PSR-4
`NoNameWeb\LivewireMaterial\`, requires `php ^8.4`, `laravel/framework ^13`,
`livewire/livewire ^4`, `blade-ui-kit/blade-icons ^1.10`; dev: `orchestra/testbench`,
`pestphp/pest`, `pestphp/pest-plugin-laravel`, `pestphp/pest-plugin-browser`,
`laravel/pint`), `LICENSE` (MIT), `NOTICE`, `pint.json`, `phpunit.xml`, `.gitattributes`
excluding `workbench/`, `tests/`, `bin/`, `docs/` and the Node/Vite dev files from dist
archives.
4. **Service provider** `LivewireMaterialServiceProvider`: merge config; register the
anonymous component path with the configured prefix; `loadViewsFrom` (`livewire-material`),
`loadTranslationsFrom`; set `blade-icons.components.default` to `null` in `register()`
(before blade-icons boots and registers its component); register the icon and shape sets;
append the package's error-view root (a directory holding only `errors/`) to
`view.paths` after the app's own, so Laravel's namespace replacement keeps it and an app's
`resources/views/errors` still wins; publishable config, lang, error views; commands; the
showcase routes when enabled. No request state anywhere in the container.
5. **Config** `config/livewire-material.php`: `prefix`, `theme.default`, `theme.storage_key`,
`theme.legacy_keys`, `showcase.enabled` (`env('MATERIAL_SHOWCASE', app()->isLocal())`),
`showcase.path` (`material`), `showcase.middleware` (`['web']`).
6. **Workbench** (`orchestra/workbench`): a Laravel app with Livewire, Vite and the package
CSS/JS built, serving the showcase at `/`; `composer serve` starts it.
7. **CI on Gitea act_runner:** Pint, Pest render tests, browser tests in Chromium, Firefox and
WebKit (Playwright installed with deps), on pushes and pull requests.
8. **Boost resources skeleton:** `resources/boost/guidelines/core.blade.php` and
`resources/boost/skills/livewire-material-development/SKILL.md`; the drift test (every
component file under `resources/views/components` is named in the skill).
**Phase 1 is done (2026-09-13).** What changed from the steps above:
- **Testbench 11.2** runs the Workbench (`testbench.yaml`: providers listed, `start: /material`,
`MATERIAL_SHOWCASE=true`). Vite builds into `workbench/public/build`; `composer serve` links
that into the skeleton's `public/build` through the Workbench `sync` option, and
`tests/TestCase.php` points `public_path()` at `workbench/public` so browser tests find the
manifest without the command. Verified: `composer serve` answers `/material` and its CSS.
- **The showcase is a plain view with `@extends`**, not a component: `<x-livewire-material::…>`
resolves under `components/`, which would have made the layout a public component.
- **blade-icons' `components.default` is set in a `booting` callback**, not in `register()`:
`mergeConfigFrom` is shallow, so a nested key written before blade-icons merges its defaults
wipes the rest of its `components` array. The test fails with the call removed.
- **The showcase switch is tested by rebooting** with `MATERIAL_SHOWCASE` in the environment:
Testbench's per-test attributes do not reach Pest closures.
- **The Boost guideline is wrapped in `@verbatim`**: it is rendered as Blade, and a
`<x-mary-*>` in its prose would compile as a component tag.
- **`phpunit.xml` trap:** a comment containing a double hyphen (`--browser`) is invalid XML and
PHPUnit refuses the file; Pest then reports an unrelated `Pest\Plugins\Tia` error on shutdown.
- **CI** (`.github/workflows/tests.yml`): Pint, feature tests on PHP 8.4 and 8.5, browser tests
per engine — Pest's `--browser chrome|firefox|safari`, Playwright's `chromium|firefox|webkit`.
Locally only Chromium was run; Firefox and WebKit first run on CI.
- **`NOTICE` moves to Phase 2**, when the first third-party assets (Symbols, font, tokens,
colour utilities) arrive — a notice with nothing to attribute would be wrong.
- **The scheme script is not in `bin/`:** `bin/` is export-ignored (maintenance scripts only),
but applications run the scheme script, so it lives at `resources/node/scheme.mjs`.
- **Found for Phase 2:** blade-icons calls `Factory::registerComponents()` whenever the view
factory resolves, registering one Blade component per icon unless `components.disabled`
and without `icons:cache` it scans the folders first. With ~7,800 symbols that is per
request under PHP-FPM. Decided before step 13.
### Phase 2 — Foundation (`0.2.0`)
9. **Scheme command.** `resources/node/scheme.mjs` built once in the package repo (esbuild bundle of
`@material/material-color-utilities`, committed, Apache-2.0 header); `material:scheme {seed}
{--variant=tonal-spot|vibrant|expressive|fidelity|content|neutral|monochrome}
{--success=} {--warning=} {--info=} {--output=}` runs it through `node` and writes
`resources/css/material-scheme.css` and `resources/css/material-scheme.json`. Fails with a
clear message when `node` is missing. A default scheme (M3 baseline `#6750A4`) ships inside
the package so a fresh install renders before the command is run.
10. **Tokens.** `resources/css/material.css` as the single entry, importing `tokens/shape.css`,
`type.css` (typescale utilities incl. emphasized), `motion.css` (six spring `linear()`
curves — spatial/effects × fast/default/slow — and reduced-motion overrides),
`elevation.css`, `state.css` (state layers, focus ring), and the `@theme` block mapping
**every** M3 role (primary/secondary/tertiary and their containers, surfaces, outlines,
inverse, error, custom success/warning/info with `inverse-*`) plus the semantic ink and
line utilities; `--color-*: initial` with white and black re-added. Each theme block
declares `color-scheme`. Values from material-web's token files, attributed.
11. **Font.** `resources/fonts/google-sans-flex/` (woff2 + OFL), `@font-face` and `--font-sans`
in the entry CSS.
12. **Theme.** `<x-theme-script />` (inline, before `@vite`): reads `storage_key`, adopts
`legacy_keys` (their values may be JSON-encoded — maryUI's `$persist` stores `"dark"`
with quotes), resolves `system` through `matchMedia`, writes `data-theme`; `$store.theme`
in `resources/js/material.js` with `set()` and a `matchMedia` listener while `system`.
13. **Icons.** `bin/fetch-symbols` (maintenance only) downloads Material Symbols Rounded
400/0/24 outlined and filled from `google/material-design-icons` into
`resources/svg/symbols/{outlined,filled}` with `fill="currentColor"` and no width/height;
`<x-icon name="…" filled>` resolves `ms`/`msf`; throws on an unknown name.
14. **Shapes.** The Expressive shape set generated by formula (ReStride's
`components/shape.blade.php` method) into `resources/svg/shapes`; `<x-shape>`.
15. **JS entry** `resources/js/material.js`: theme store, snackbar listener, `data-list-row`
rows, `x-figure` count-up directive, shared keyboard helpers. Imported by an app's `app.js`.
16. **`Toasts` concern** `NoNameWeb\LivewireMaterial\Concerns\Toasts`: protected
`success|error|warning|info(string $title, ?string $description = null, ?int $timeout = null, ?string $redirectTo = null)`,
dispatching a browser event and flashing across `redirectTo`.
17. **Guard helpers** (`NoNameWeb\LivewireMaterial\Testing\DesignGuard`, Pest-friendly):
scan given paths for maryUI tags, daisyUI component classes, colour utilities not declared
by the compiled tokens, unknown icon names, and app-configured banned roles/variants.
18. **Showcase shell**: layout, section navigation, theme switch, snippet renderer; a section
per foundation piece (colour roles in both themes, type scale, shapes, motion, icons
search).
### Phase 3 — Actions (`0.3.0`)
19. Primitives the actions need: `loading` (M3 Expressive loading indicator, contained and
not), plain `tooltip` (from a fine pointer only, not laid out while hidden), `menu` /
`menu-item` / `menu-separator` (Popover API + anchor positioning, Expressive vertical menu,
APG menu keyboard).
20. `button` — five variants × colours × sizes `xs|sm|md|lg|xl`, `icon`, `icon-right`,
`label`, `link` (+ `wire:navigate` unless `external` / `no-wire-navigate`), `spinner`,
`responsive`, `tooltip*`, `disabled` on links (`aria-disabled`), press shape-morph.
21. `icon-button` behaviour inside `button` (icon, no label): standard/filled/tonal/outlined,
`selected` toggle (`aria-pressed`), widths.
22. `button-group` (standard and connected; `<x-group>` alias with `wire:model` options),
`split-button`, `fab` (medium default, large; `size="sm"` → medium), extended FAB, and the
responsive `fab` prop (extended FAB below `sm`, filled header button above — one element),
`fab-menu`.
### Phase 4 — Communication (`0.4.0`)
23. `badge` (dot, count, label; variant/colour), `progress` (linear, circular, **wavy**,
determinate and indeterminate), `toast` (M3 snackbar, action, timeout, stacked), rich
`tooltip`, `alert` (tinted container, icon, actions slot), `stat` (figure with
`x-figure`), `empty-state`.
### Phase 5 — Containment (`0.5.0`)
24. `card` (elevated, filled, outlined; `title`, `subtitle`, `actions` slot; clickable row
contract), `divider`, `list` / `list-item` (one-, two-, three-line; leading/trailing;
selectable), `modal` (native `<dialog>`, `showModal()`, `wire:ignore.self`, writes back
`false`/`null`, `fullscreen` below `sm`, basic dialog with icon/headline/actions),
`bottom-sheet` (modal and standard, drag handle), `drawer` (side sheet: modal and
standard; `pane` for list-detail from `xl`; `width` prop), `carousel` (multi-browse, uncontained, hero,
full-screen on CSS scroll-snap), `collapse`.
### Phase 6 — Text inputs and selection (`0.6.0`)
25. `form`, `field` (the shared shell: **outlined and filled**, floating label via `:has()`,
notch, `hint` replaced by error, `aria-invalid` / `aria-describedby`, `data-*` state
marks), `input` (prefix/suffix, icons, `copyable` trailing button with a snackbar),
`password` (reveal toggle), `textarea` (auto-grow), `select` (native, customizable-select
enhancement with ReStride's traps), `checkbox` (incl. indeterminate), `radio`, `toggle`
(M3 switch, icons), `file` (native input inside the field, batch and per-file errors).
26. `chip` (assist, filter, input, suggestion), `choices` (filter chips, or a searchable
combobox with a menu when `searchable`), `slider` (standard, centered, range; native range
inputs, value label), `search` (search bar and search view, results through a Livewire
property).
### Phase 7 — Pickers (`0.7.0`)
27. `datepicker` (docked, modal, modal input; `Intl` month/day names and week start from the
app locale; `min`/`max`; single and range; `wire:model` stores `Y-m-d`), `timepicker`
(dial and input; 12/24h from locale; stores `H:i`). APG grid keyboard for the calendar.
### Phase 8 — Navigation (`0.8.0`)
28. `app-bar` (small, center-aligned, medium flexible, large flexible, search app bar; sticky,
scroll-elevation), `navigation-bar` (flexible), `navigation-rail` (collapsed, expanded,
modal; badges), `tabs` / `tab` (primary and secondary; server-rendered tablist, roving
tabindex), `toolbar` (docked and floating), `section-nav` (secondary tabs from `sm`, menu
picker below), `account-menu` (avatar trigger, slot for items, theme row), `theme-toggle`
(cycles or picks light/dark/system through `$store.theme`).
29. `app-shell` — a slot-based adaptive composition: app bar + navigation bar below `sm`, rail
`sm``lg`, expanded collapsible rail from `lg` (state in the store, applied before paint by
the theme script), content region with `wire:transition.navigate`, snackbar host. Nothing
app-specific inside; apps pass destinations and extra chrome as slots.
### Phase 9 — Data, pages, mail (`0.9.0`)
30. `table` (`.data-table`, descendant selectors, fine-pointer density, `position: relative`),
`sort-header` (`sortBy` array shape, `aria-sort`), Livewire and Laravel pagination views
(current page in `secondary-container`, "Page 2 of 7" on a phone).
31. Error pages: a layout and `403, 404, 419, 429, 500, 503` in the package's error-view root
(wired through `view.paths` in step 4); publishable for per-app wording.
32. Mail theme: `livewire-material::mail.theme` renders CSS from the app's
`material-scheme.json` (falling back to the default scheme); `html/header` and
`html/message` overrides; typescale on bare tags; filled primary button.
### Phase 10 — `1.0.0`
33. Showcase complete (every component, variant, colour, size and state, both themes); the
in-app mount verified inside a fresh Laravel app; Boost guideline and skill complete
(drift test green); README (install, CSS/JS wiring, scheme, theme, prefix, showcase,
guard, Docker ordering note); tag `1.0.0`.
### Phase 11 — SealShare 2.0.0
Tracked in SealShare's `docs/plans/livewire-material.md`, after `1.0.0`.
## Testing
- **Render tests** (`tests/Feature/Components/*Test.php`, Testbench, `$this->blade()`), per
component: each `variant` × `color` renders its classes and falls back on an unknown value;
shorthands (`primary`, `danger`, `caution`, `tone`) map correctly; sizes; ARIA
(`aria-pressed`, `aria-expanded`, `aria-current`, `aria-invalid` + `aria-describedby`,
labelled dialogs, `aria-sort`); `link` adds `wire:navigate` unless `external`; `wire:model`
normalisation (`false``null`); prefix config renames the tags.
- **Foundation tests:** every `--md-sys-color-*` role defined in both theme blocks and
different between them; each block declares `color-scheme`; no `prefers-color-scheme` in
the CSS; `@theme` never `inline`; the scheme command writes both files for a known seed
(snapshot of a few roles) and fails cleanly without Node; the theme script lands before
`@vite` and resolves `system`; `<x-icon>` throws on an unknown name; every shape fills the
100-unit box with `currentColor` only; `Toasts` dispatches and survives `redirectTo`;
`DesignGuard` catches each forbidden pattern on fixtures; the skill drift test; the mail
theme renders the JSON's hexes; a 404 renders the package's error view and an app's own
`errors/404.blade.php` still wins; the showcase route is 404 when disabled and 200 when
enabled; blade-icons' own `<x-icon>` is not registered.
- **Browser tests** against the Workbench showcase in Chromium, Firefox and WebKit: dialog
(open, Esc, morph survival, write-back), bottom and side sheets and the pane from `xl`,
menu and select (keyboard, anchoring, flip), tabs (arrows, Home/End), date and time pickers
(keyboard grid, locale, `wire:model` value), carousel (snap, keyboard), sliders, chips and
choices, search, snackbar timing and action, theme (light/dark/system, legacy adoption, OS
change followed), app shell at 393 / 768 / 1024 / 1512px, focus rings on keyboard focus,
and reduced motion leaving no running animations.
## Risks and open questions
- **Scope and time.** The whole catalogue (~45 components plus extras) comes before any app
uses it. Mitigation: waves tagged `0.x`, each reviewed in the showcase.
- **Accessibility is entirely ours** — menus, pickers, carousel, sheets. Mitigation: APG
patterns, native elements first, ARIA in render tests, keyboard in browser tests across
three engines.
- **Date and time pickers without a library** are the largest single components. Mitigation:
`Intl` for all locale data; the modal-input variant as the accessible baseline.
- **Full M3 invites inconsistency per app.** Mitigation: the configurable `DesignGuard`; each
app records its own rules.
- **Livewire morphing against Alpine state** inside components. Mitigation: ReStride's
recorded traps become package rules in the skill, with tests pinning each.
- **~7,800 SVGs** make dist archives and `vendor/` larger (~4 MB) and a cold icon lookup
slower. Mitigation: `.gitattributes` keeps dev files out, `icons:cache` in deploys, `@source`
never scans the SVG folders.
- **Maintenance of a public package** is one person's job; breaking changes need semver
discipline once ReStride also depends on it.
- **Runner scope.** The package's CI needs an act_runner registered for the instance or the
`noNameWEB` org, not only for ReStride — check before the first push in Phase 1.