Scaffold the Livewire Material package
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

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
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 04:59:21 +02:00
co-authored by Claude Opus 5
commit 9b53891a8e
33 changed files with 2731 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4
+19
View File
@@ -0,0 +1,19 @@
* text=auto eol=lf
*.woff2 binary
# Development only: kept out of the archives Composer installs.
/.editorconfig export-ignore
/.gitattributes export-ignore
/.github export-ignore
/.gitignore export-ignore
/bin export-ignore
/docs export-ignore
/package-lock.json export-ignore
/package.json export-ignore
/phpunit.xml export-ignore
/pint.json export-ignore
/testbench.yaml export-ignore
/tests export-ignore
/vite.config.js export-ignore
/workbench export-ignore
+111
View File
@@ -0,0 +1,111 @@
name: tests
# Run by Gitea's act_runner. Nothing here needs a service container or a database.
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.5'
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Check formatting
run: vendor/bin/pint --test
feature:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Run feature tests
run: vendor/bin/pest --testsuite=Feature
# One job per engine: the floor is Chrome 125, Firefox 147 and Safari 18.4, and the
# components lean on platform features (dialog, popover, anchor positioning) that
# differ between them. Pest calls the engines chrome, firefox and safari; Playwright
# installs them as chromium, firefox and webkit.
browser:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- browser: chrome
playwright: chromium
- browser: firefox
playwright: firefox
- browser: safari
playwright: webkit
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.5'
tools: composer:v2
coverage: none
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22'
- name: Install dependencies
run: |
composer install --no-interaction --no-progress --prefer-dist
npm ci
- name: Build the Workbench assets
run: npm run build
- name: Install the browser
run: npx playwright install --with-deps ${{ matrix.playwright }}
- name: Run browser tests
run: vendor/bin/pest --testsuite=Browser --browser ${{ matrix.browser }}
+9
View File
@@ -0,0 +1,9 @@
/.phpunit.cache
/.phpunit.result.cache
/composer.lock
/node_modules
/vendor
/workbench/public/build
/workbench/public/hot
/tests/Browser/Screenshots
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 noNameWEB
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+78
View File
@@ -0,0 +1,78 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "nonameweb/livewire-material",
"description": "Material 3 Expressive components for Laravel and Livewire.",
"type": "library",
"license": "MIT",
"keywords": [
"laravel",
"livewire",
"blade",
"components",
"material",
"material-design",
"material-3",
"tailwindcss"
],
"homepage": "https://gitea.nonameweb.ch/noNameWEB/livewire-material",
"authors": [
{
"name": "Andreas Reinhold"
}
],
"require": {
"php": "^8.4",
"blade-ui-kit/blade-icons": "^1.10",
"laravel/framework": "^13.0",
"livewire/livewire": "^4.0"
},
"require-dev": {
"laravel/pint": "^1.32",
"orchestra/testbench": "^11.2",
"pestphp/pest": "^5.1",
"pestphp/pest-plugin-browser": "^5.0",
"pestphp/pest-plugin-laravel": "^5.0"
},
"autoload": {
"psr-4": {
"NoNameWeb\\LivewireMaterial\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"NoNameWeb\\LivewireMaterial\\Tests\\": "tests/",
"Workbench\\App\\": "workbench/app/"
}
},
"extra": {
"laravel": {
"providers": [
"NoNameWeb\\LivewireMaterial\\LivewireMaterialServiceProvider"
]
}
},
"scripts": {
"post-autoload-dump": [
"@clear",
"@prepare"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"build": "@php vendor/bin/testbench workbench:build --ansi",
"serve": [
"Composer\\Config::disableProcessTimeout",
"@build",
"@php vendor/bin/testbench serve --ansi"
],
"lint": "pint",
"test": "pest"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
+55
View File
@@ -0,0 +1,55 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Component prefix
|--------------------------------------------------------------------------
|
| Every component is an anonymous Blade component. Without a prefix they are
| <x-button>, <x-card> and so on; set a prefix such as 'm-' when a name
| clashes with one of the application's own components (<x-m-button>).
|
*/
'prefix' => '',
/*
|--------------------------------------------------------------------------
| Theme
|--------------------------------------------------------------------------
|
| The head script decides the theme before the first paint and writes it to
| <html data-theme>. 'default' is used until the visitor chooses: 'light',
| 'dark' or 'system' (follow the operating system). The choice is kept in
| localStorage under 'storage_key'; values found under 'legacy_keys' (an
| earlier theme toggle's key) are adopted once and then removed.
|
*/
'theme' => [
'default' => 'system',
'storage_key' => 'material-theme',
'legacy_keys' => [],
],
/*
|--------------------------------------------------------------------------
| Showcase
|--------------------------------------------------------------------------
|
| Every component in every variant, rendered in the application's own
| scheme. Off unless the application runs locally. 'vite' names the entry
| points that import this package's CSS and JavaScript.
|
*/
'showcase' => [
'enabled' => (bool) env('MATERIAL_SHOWCASE', env('APP_ENV', 'production') === 'local'),
'path' => 'material',
'middleware' => ['web'],
'vite' => ['resources/css/app.css', 'resources/js/app.js'],
],
];
+408
View File
@@ -0,0 +1,408 @@
# 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.
+1578
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
"laravel-vite-plugin": "^3.2.0",
"playwright": "^1.62.1",
"tailwindcss": "^4.3.3",
"vite": "^8.2.2"
}
}
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache"
>
<testsuites>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
<!-- Real browsers, driven by Playwright against the Workbench showcase. Needs
`npm run build` first; CI runs it once per engine (chrome, firefox, safari). -->
<testsuite name="Browser">
<directory>tests/Browser</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_KEY" value="base64:NgopikxwbRspVdKjfygJXbjOPjSgXAU2Nrf9STNYiew="/>
<env name="MATERIAL_SHOWCASE" value="true"/>
</php>
</phpunit>
+3
View File
@@ -0,0 +1,3 @@
{
"preset": "laravel"
}
@@ -0,0 +1,9 @@
@verbatim
## Livewire Material
This application uses `nonameweb/livewire-material`: Material 3 Expressive components for Laravel and Livewire, built on Tailwind CSS. It replaces UI kits such as maryUI, daisyUI and Flux in this application.
- 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.
- Never write maryUI tags (`<x-mary-*>`) or daisyUI classes (`btn`, `card`, `badge`, `bg-base-200`, `text-base-content`). They compile to nothing and fail silently.
- While the application runs locally, every component renders in the application's own scheme at `/material` (the showcase).
@endverbatim
@@ -0,0 +1,45 @@
---
name: livewire-material-development
description: Build Laravel and Livewire views with Livewire Material's Material 3 Expressive Blade components — props, slots, theming and the Livewire traps each component handles.
---
# Livewire Material Development
## When to use this skill
Use this skill when writing or changing any Blade view, Livewire component view or layout in an application that requires `nonameweb/livewire-material`, and when styling, theming or testing such views.
## Setup
The package ships CSS and JavaScript that the application imports from `vendor/`, after Tailwind:
```css
/* resources/css/app.css */
@import 'tailwindcss';
@import '../../vendor/nonameweb/livewire-material/resources/css/material.css';
@source '../../vendor/nonameweb/livewire-material/resources/views';
@source '../../vendor/nonameweb/livewire-material/src';
```
```js
// resources/js/app.js
import '../../vendor/nonameweb/livewire-material/resources/js/material.js'
```
Composer packages must be installed before the Vite build (in Dockerfiles and CI alike), or these imports have nothing to read.
## Conventions
- Components are anonymous Blade components: `<x-name>` without a prefix, or `<x-{prefix}name>` when `config('livewire-material.prefix')` is set.
- The showcase at `/material` (local only, `MATERIAL_SHOWCASE` to force it) renders every component with its Blade snippet.
## Components
Each component the package ships is listed here with its props and slots.
## Livewire traps
- Blade directives do not compile inside a component tag's attributes: `<x-foo x-show="ok(@js($value))">` reaches the browser as literal text. On a component tag use `{{ }}` and `:prop` bindings, or put the Alpine on a plain element inside the slot.
- Never pass `hidden`, a display utility or a position (`absolute`, `relative`) to a component: it is merged beside the component's own and whichever Tailwind emits last wins. Wrap the component in an element that carries it. A variant that only hides (`max-sm:hidden`) is safe.
- `$attributes->wire('model')->value()` is `false`, not `null`, when there is no `wire:model`, and `filled(false)` is true. Normalise with `?: null`.
- End every statement in a multi-line Alpine attribute with `;`: an inline `@if … @endif` inside it swallows the newline after it.
+10
View File
@@ -0,0 +1,10 @@
/*
* Livewire Material — the one stylesheet an application imports, after Tailwind:
*
* @import 'tailwindcss';
* @import '../../vendor/nonameweb/livewire-material/resources/css/material.css';
* @source '../../vendor/nonameweb/livewire-material/resources/views';
* @source '../../vendor/nonameweb/livewire-material/src';
*
* Colour roles, shape, type, motion, elevation and the component styles arrive in 0.2.0.
*/
+8
View File
@@ -0,0 +1,8 @@
/*
* Livewire Material — imported once from the application's app.js:
*
* import '../../vendor/nonameweb/livewire-material/resources/js/material.js'
*
* Alpine ships with Livewire, so this only registers onto it: the theme store, the
* snackbar listener and the list-row behaviour arrive in 0.2.0.
*/
View File
+12
View File
@@ -0,0 +1,12 @@
@extends('livewire-material::showcase.layout')
@section('content')
<main class="mx-auto max-w-5xl px-4 py-10">
<h1 class="text-3xl">Livewire Material</h1>
<p class="mt-2">
Every component, in every variant, colour, size and state, in this application's own scheme.
Sections appear here as the components land.
</p>
</main>
@endsection
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>@yield('title', 'Livewire Material')</title>
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body class="min-h-screen antialiased">
@yield('content')
@livewireScripts
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
Route::view('/', 'livewire-material::showcase.index')->name('showcase');
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace NoNameWeb\LivewireMaterial;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class LivewireMaterialServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/../config/livewire-material.php', 'livewire-material');
$this->disableBladeIconsComponent();
}
public function boot(): void
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'livewire-material');
$this->loadTranslationsFrom(__DIR__.'/../lang', 'livewire-material');
$this->registerComponents();
$this->registerShowcase();
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/livewire-material.php' => config_path('livewire-material.php'),
], 'livewire-material-config');
}
}
/**
* Register the components as anonymous Blade components under the configured prefix.
*/
protected function registerComponents(): void
{
Blade::anonymousComponentPath(
__DIR__.'/../resources/views/components',
filled(config('livewire-material.prefix')) ? config('livewire-material.prefix') : null,
);
}
/**
* blade-icons registers a class-based <x-icon> of its own, and Blade resolves a
* registered class alias before any anonymous component path, so ours would never
* render. It reads the key in its boot(); a booting callback runs after every
* register() — so after blade-icons merges its defaults over the key — and before
* any provider boots.
*/
protected function disableBladeIconsComponent(): void
{
$this->app->booting(function (): void {
config(['blade-icons.components.default' => null]);
});
}
/**
* Mount the showcase when it is enabled.
*/
protected function registerShowcase(): void
{
if (! config('livewire-material.showcase.enabled') || $this->app->routesAreCached()) {
return;
}
Route::middleware(config('livewire-material.showcase.middleware'))
->prefix(config('livewire-material.showcase.path'))
->name('livewire-material.')
->group(__DIR__.'/../routes/showcase.php');
}
}
+23
View File
@@ -0,0 +1,23 @@
laravel: '@testbench'
providers:
- Livewire\LivewireServiceProvider
- BladeUI\Icons\BladeIconsServiceProvider
- NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider
- Workbench\App\Providers\WorkbenchServiceProvider
env:
- APP_NAME="Livewire Material"
- MATERIAL_SHOWCASE=true
workbench:
start: '/material'
install: true
# `npm run build` writes into workbench/public/build; `composer serve` links it into
# the skeleton's public folder, which is where Laravel's Vite looks for the manifest.
sync:
- from: workbench/public/build
to: public/build
discovers:
web: false
views: false
+7
View File
@@ -0,0 +1,7 @@
<?php
it('opens the showcase in a real browser', function () {
visit('/material')
->assertSee('Livewire Material')
->assertNoJavaScriptErrors();
});
+30
View File
@@ -0,0 +1,30 @@
<?php
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
const SKILL_PATH = __DIR__.'/../../resources/boost/skills/livewire-material-development/SKILL.md';
it('gives the skill the frontmatter Boost requires', function () {
$skill = File::get(SKILL_PATH);
expect($skill)
->toStartWith("---\n")
->toContain("\nname: livewire-material-development\n")
->toMatch('/\ndescription: \S.+\n/');
});
it('names every component in the skill', function () {
$skill = File::get(SKILL_PATH);
$undocumented = collect(File::allFiles(__DIR__.'/../../resources/views/components'))
->filter(fn (SplFileInfo $file): bool => str_ends_with($file->getFilename(), '.blade.php'))
->map(fn (SplFileInfo $file): string => Str::of($file->getRelativePathname())
->beforeLast('.blade.php')
->replace(DIRECTORY_SEPARATOR, '.')
->toString())
->reject(fn (string $component): bool => str_contains($skill, "<x-{$component}"))
->values();
expect($undocumented)->toBeEmpty();
});
+37
View File
@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Blade;
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
/**
* The anonymous component paths Blade holds for this package's components.
*
* @return Collection<int, array{path: string, prefix: ?string, prefixHash: string}>
*/
function packageComponentPaths(): Collection
{
$components = realpath(__DIR__.'/../../resources/views/components');
return collect(Blade::getAnonymousComponentPaths())
->filter(fn (array $entry): bool => realpath($entry['path']) === $components)
->values();
}
it('registers the components without a prefix', function () {
expect(packageComponentPaths())->toHaveCount(1)
->and(packageComponentPaths()->first()['prefix'])->toBeNull();
});
it('registers the components under a configured prefix', function () {
config(['livewire-material.prefix' => 'm-']);
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
(fn () => $this->registerComponents())->call($provider);
expect(packageComponentPaths()->last()['prefix'])->toBe('m-');
});
it('keeps blade-icons from registering an <x-icon> that would shadow ours', function () {
expect(Blade::getClassComponentAliases())->not->toHaveKey('icon');
});
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* Reboot the application with the showcase switched on or off. The routes are mounted
* while the provider boots, so the flag has to be in the environment before that.
*/
function rebootWithShowcase(bool $enabled): void
{
$value = $enabled ? 'true' : 'false';
putenv("MATERIAL_SHOWCASE={$value}");
$_ENV['MATERIAL_SHOWCASE'] = $_SERVER['MATERIAL_SHOWCASE'] = $value;
test()->refreshApplication();
}
afterEach(function () {
rebootWithShowcase(true);
});
it('mounts the showcase when enabled', function () {
$this->withoutVite()
->get('/material')
->assertOk()
->assertSee('Livewire Material');
});
it('does not mount the showcase when disabled', function () {
rebootWithShowcase(false);
$this->get('/material')->assertNotFound();
});
+5
View File
@@ -0,0 +1,5 @@
<?php
use NoNameWeb\LivewireMaterial\Tests\TestCase;
pest()->extend(TestCase::class)->in('Feature', 'Browser');
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace NoNameWeb\LivewireMaterial\Tests;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase as Orchestra;
use function Orchestra\Testbench\workbench_path;
abstract class TestCase extends Orchestra
{
use WithWorkbench;
/**
* Serve the Workbench's built assets. `composer serve` links them into the skeleton
* instead, but a browser test boots the application without that command.
*/
protected function defineEnvironment($app): void
{
$app->usePublicPath(workbench_path('public'));
}
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import tailwindcss from '@tailwindcss/vite'
// Builds the Workbench, the Laravel app the showcase and the browser tests run in.
// Applications never use this file; they import the package's CSS and JS into their own build.
export default defineConfig({
plugins: [
laravel({
input: ['workbench/resources/css/app.css', 'workbench/resources/js/app.js'],
publicDirectory: 'workbench/public',
refresh: ['resources/views/**', 'workbench/resources/views/**'],
}),
tailwindcss(),
],
})
@@ -0,0 +1,28 @@
<?php
namespace Workbench\App\Providers;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use function Orchestra\Testbench\workbench_path;
class WorkbenchServiceProvider extends ServiceProvider
{
/**
* Point the showcase at the Workbench's own Vite entries and dev server.
*
* Set in boot() rather than register(): config merging is shallow, so a nested key
* written before the package merges its defaults would replace the whole
* `showcase` array.
*/
public function boot(): void
{
config(['livewire-material.showcase.vite' => [
'workbench/resources/css/app.css',
'workbench/resources/js/app.js',
]]);
Vite::useHotFile(workbench_path('public/hot'));
}
}
View File
+7
View File
@@ -0,0 +1,7 @@
/* The Workbench builds the package exactly as an application would, with explicit
sources so the plans in docs/ are not scanned for class names. */
@import 'tailwindcss' source(none);
@import '../../../resources/css/material.css';
@source '../../../resources/views';
@source '../../../src';
+1
View File
@@ -0,0 +1 @@
import '../../../resources/js/material.js'