From 9b53891a8e009eba899683dba5f0238ecfef45e3 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Sun, 13 Sep 2026 04:59:21 +0200 Subject: [PATCH] Scaffold the Livewire Material package The skeleton for nonameweb/livewire-material: service provider and config (unprefixed components, blade-icons' own 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) Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy --- .editorconfig | 18 + .gitattributes | 19 + .github/workflows/tests.yml | 111 ++ .gitignore | 9 + LICENSE | 21 + composer.json | 78 + config/livewire-material.php | 55 + docs/plans/livewire-material.md | 408 +++++ package-lock.json | 1578 +++++++++++++++++ package.json | 16 + phpunit.xml | 28 + pint.json | 3 + resources/boost/guidelines/core.blade.php | 9 + .../livewire-material-development/SKILL.md | 45 + resources/css/material.css | 10 + resources/js/material.js | 8 + resources/views/components/.gitkeep | 0 resources/views/showcase/index.blade.php | 12 + resources/views/showcase/layout.blade.php | 18 + routes/showcase.php | 5 + src/LivewireMaterialServiceProvider.php | 72 + testbench.yaml | 23 + tests/Browser/ShowcaseTest.php | 7 + tests/Feature/BoostResourcesTest.php | 30 + tests/Feature/ServiceProviderTest.php | 37 + tests/Feature/ShowcaseTest.php | 32 + tests/Pest.php | 5 + tests/TestCase.php | 22 + vite.config.js | 16 + .../Providers/WorkbenchServiceProvider.php | 28 + workbench/public/.gitkeep | 0 workbench/resources/css/app.css | 7 + workbench/resources/js/app.js | 1 + 33 files changed, 2731 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 config/livewire-material.php create mode 100644 docs/plans/livewire-material.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 pint.json create mode 100644 resources/boost/guidelines/core.blade.php create mode 100644 resources/boost/skills/livewire-material-development/SKILL.md create mode 100644 resources/css/material.css create mode 100644 resources/js/material.js create mode 100644 resources/views/components/.gitkeep create mode 100644 resources/views/showcase/index.blade.php create mode 100644 resources/views/showcase/layout.blade.php create mode 100644 routes/showcase.php create mode 100644 src/LivewireMaterialServiceProvider.php create mode 100644 testbench.yaml create mode 100644 tests/Browser/ShowcaseTest.php create mode 100644 tests/Feature/BoostResourcesTest.php create mode 100644 tests/Feature/ServiceProviderTest.php create mode 100644 tests/Feature/ShowcaseTest.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php create mode 100644 vite.config.js create mode 100644 workbench/app/Providers/WorkbenchServiceProvider.php create mode 100644 workbench/public/.gitkeep create mode 100644 workbench/resources/css/app.css create mode 100644 workbench/resources/js/app.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a186cd20 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..3051eefe --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..00bfcd5e --- /dev/null +++ b/.github/workflows/tests.yml @@ -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 }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fb0525dc --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..aa14b11a --- /dev/null +++ b/LICENSE @@ -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. diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..70f0b7fe --- /dev/null +++ b/composer.json @@ -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 +} diff --git a/config/livewire-material.php b/config/livewire-material.php new file mode 100644 index 00000000..c0926c56 --- /dev/null +++ b/config/livewire-material.php @@ -0,0 +1,55 @@ +, and so on; set a prefix such as 'm-' when a name + | clashes with one of the application's own components (). + | + */ + + 'prefix' => '', + + /* + |-------------------------------------------------------------------------- + | Theme + |-------------------------------------------------------------------------- + | + | The head script decides the theme before the first paint and writes it to + | . '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'], + ], + +]; diff --git a/docs/plans/livewire-material.md b/docs/plans/livewire-material.md new file mode 100644 index 00000000..cbe96dd7 --- /dev/null +++ b/docs/plans/livewire-material.md @@ -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 ``. +- 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 ``. +- ~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 `` 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 (`` 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 ``, Popover API, CSS anchor positioning, customizable `