tests / lint (push) Successful in 1m0s
tests / feature (8.4) (push) Successful in 1m0s
tests / feature (8.5) (push) Successful in 1m1s
tests / browser (safari, webkit) (push) Successful in 1m59s
tests / browser (chrome, chromium) (push) Successful in 1m49s
tests / browser (firefox, firefox) (push) Successful in 1m54s
Colour, shape, type, elevation and motion as tokens and Tailwind utilities; `php artisan material:scheme`, which generates an app's colour roles with Google's material-color-utilities (spec 2025); the theme head script with light, dark and system and its Alpine store; Google Sans Flex; every Material Symbol (4,135, outlined and filled) drawn by <x-icon> without blade-icons; all 35 M3 Expressive shapes, ported from androidx, as <x-shape>; the x-figure directive; the Toasts concern; DesignGuard for applications' tests; and a showcase with every token, both themes side by side and an icon search. Colour utilities are `@theme inline`, so a section with its own data-theme repaints; without it they resolve once on :root. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
453 lines
31 KiB
Markdown
453 lines
31 KiB
Markdown
# 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 400–700, ROND 0–100).
|
||
- 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, 4,135 symbols,
|
||
5.3 MB), fetched by a maintenance script in the package repo, never at runtime — any name works
|
||
in any app. **`<x-icon>` reads the SVG files itself; blade-icons is not used for them** (decided
|
||
after Phase 1): blade-icons registers one Blade component per icon whenever the view factory
|
||
resolves, ~8,000 registrations per request here. blade-icons stays a dev dependency, only to
|
||
test that the provider still keeps its `<x-icon>` from shadowing ours in apps that have it.
|
||
- **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 2 is done (2026-09-13).** What changed from the steps above:
|
||
|
||
- **Icons without blade-icons** (see Decisions): `NoNameWeb\LivewireMaterial\Support\SvgFile`
|
||
reads a symbol or shape file on first use and keeps it for the worker. An unknown name throws,
|
||
and a Heroicon-style name (`o-home`) throws with a pointer to the catalogue. `bin/fetch-symbols`
|
||
sparse-checks-out google/material-design-icons (5 GB, cloned without blobs) for the
|
||
`_24px` and `_fill1_24px` files: 4,135 symbols, byte-identical to ReStride's hand-picked ones.
|
||
The `@material-symbols/svg-400` npm package was rejected: its "rounded" SVGs are optical size 48.
|
||
- **All 35 M3 Expressive shapes**, ported from androidx `MaterialShapes.kt` and graphics-shapes
|
||
into `bin/shapes.mjs` (commit in its header), each fitted to 2–98 of a 100-unit box; eight had
|
||
control points outside the box and are split into more segments along the same outline.
|
||
- **The scheme script** lives at `resources/node/scheme.mjs` (93 KB, esbuild bundle of
|
||
material-color-utilities 0.4.0, spec 2025 — M3 Expressive's colour; variants the 2025 spec
|
||
does not define fall back to 2021). The library's extensionless imports mean it cannot run
|
||
unbundled. 65 roles per theme plus `inverse-{error,success,warning,info}`. Default state
|
||
sources: success `#22a06b`, warning `#e2a400`, info `#1d7afc`. `config('livewire-material.node')`
|
||
names the binary. The package's own default is `#6750A4`, tonal-spot, in `tokens/scheme.css`.
|
||
- **Colour utilities are `@theme inline`** — contrary to ReStride's rule that `inline` breaks
|
||
theme switching. Verified in Chromium: without `inline`, `--color-primary` resolves once on
|
||
`:root`, so a `data-theme="dark"` section inside a light page keeps light colours; with it the
|
||
utility reads `var(--md-sys-color-primary)` on the element and both the page toggle and nested
|
||
themes work. Only blocks whose values are variables are inline. A browser test fails without it.
|
||
- **Tailwind emits only the theme variables that are used**, and cannot see a class assembled in
|
||
Blade (`type-{{ $style }}`): the showcase lists every class literally.
|
||
- **`dark:`** is keyed on `data-theme`, so it follows the page's theme rather than the OS.
|
||
- **Theme script** writes nothing for a visitor who never chose (a later change of
|
||
`theme.default` still reaches them); only an adopted legacy value is stored. `data-theme-choice`
|
||
and `data-theme-key` on `<html>` hand the state to `$store.theme` (`choice`, `resolved`, `set`,
|
||
`toggle`, `value`).
|
||
- **Moved out of Phase 2:** the snackbar listener goes to Phase 4 with `<x-toast>`; `data-list-row`
|
||
rows (JS and CSS) and the list keyboard go to Phase 5 with `list` and `card`, which they style.
|
||
`Toasts` dispatches `toast` with a 4 s default (M3's snackbar range is 4–10 s; ReStride used 3 s).
|
||
- **`DesignGuard`**: `secondary` is a valid role now that the package exposes full M3, so it is no
|
||
longer flagged as a daisyUI colour; icon attributes are checked per component tag (`icon` and
|
||
`icon-right` on the same tag).
|
||
- **Pest browser trap:** a bare `html` selector is taken as text to search for and times out;
|
||
assert on `document.documentElement` through `assertScript`. `@name` targets `data-test`.
|
||
- **Showcase**: colour roles in both themes side by side, type, corners and shapes, elevation,
|
||
springs and `x-figure`, and an icon search drawing matches as CSS masks from a showcase-only
|
||
symbol route (relative URLs — an absolute one carries `APP_URL` and misses the dev server's port).
|
||
|
||
### 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; colour blocks are `@theme 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.
|
||
- **~8,300 SVGs** make dist archives and `vendor/` larger (5.3 MB). Mitigation: each file is read
|
||
only when drawn and kept per worker; `.gitattributes` keeps dev files out; `@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.
|