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
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -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 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, ~7,800 SVGs,
|
||||
~3–4 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.
|
||||
Generated
+1578
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::view('/', 'livewire-material::showcase.index')->name('showcase');
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
it('opens the showcase in a real browser', function () {
|
||||
visit('/material')
|
||||
->assertSee('Livewire Material')
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
use NoNameWeb\LivewireMaterial\Tests\TestCase;
|
||||
|
||||
pest()->extend(TestCase::class)->in('Feature', 'Browser');
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1 @@
|
||||
import '../../../resources/js/material.js'
|
||||
Reference in New Issue
Block a user