diff --git a/README.md b/README.md new file mode 100644 index 00000000..5c8d4783 --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# Livewire Material + +Material 3 Expressive components for Laravel and Livewire, built on Tailwind CSS 4. + +- Anonymous Blade components for the current M3 Expressive catalogue: buttons and FABs, menus, chips, text fields, selection controls, sliders, pickers, dialogs and sheets, lists, cards, carousels, progress and loading indicators, snackbars, tabs, app bars, toolbars, navigation bars and rails, an adaptive app shell, data tables and pagination. +- A colour scheme generated from one seed colour with Google's colour science (`php artisan material:scheme`), light and dark, and a theme that is chosen before the first paint. +- The full Material Symbols Rounded set and the M3 Expressive shapes, drawn inline without an icon package. +- Error pages and a Markdown mail theme in the same scheme. +- A showcase of every component in the application's own scheme, a design guard for tests, and Laravel Boost guidelines and a skill for AI agents. + +No JavaScript libraries beyond the Alpine that ships with Livewire. Browsers: Chrome 125+, Firefox 147+, Safari 18.4+. + +## Requirements + +PHP 8.4+, Laravel 13, Livewire 4, Tailwind CSS 4 with Vite, and Node (for `material:scheme`). + +## Installation + +The package is served from Gitea. Add the repository and require it: + +```bash +composer config repositories.livewire-material vcs https://gitea.nonameweb.ch/noNameWEB/livewire-material.git +composer require nonameweb/livewire-material +``` + +### Stylesheet and script + +The application's build imports from `vendor/`, so Composer packages must be installed before `npm run build` — in a Dockerfile, copy `composer.json`, run `composer install`, then build the assets. + +```css +/* resources/css/app.css */ +@import 'tailwindcss'; +@import '../../vendor/nonameweb/livewire-material/resources/css/material.css'; +@import './material-scheme.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' +``` + +Do not install Alpine separately; Livewire provides it. + +### Layout + +The theme script goes in ``, before `@vite`, so the page paints in the visitor's theme: + +```blade + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + {{ $slot }} + + + +``` + +### Colour scheme + +Generate the scheme from a seed colour. It writes `resources/css/material-scheme.css` (imported above) and `material-scheme.json` (read by the mail theme): + +```bash +php artisan material:scheme "#4f46e5" --variant=tonal-spot +``` + +Variants: `tonal-spot`, `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--contrast` runs from -1 to 1; `--success`, `--warning` and `--info` seed the state colours. Regenerate instead of editing the file. + +### Configuration + +```bash +php artisan vendor:publish --tag=livewire-material-config +``` + +- `prefix` — components are ``, ``… Set `'m'` when a name clashes with the application's own components, and they become ``. `` always works. +- `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys` (an earlier toggle's localStorage keys, adopted once). +- `fields.variant` — text fields `outlined` (default) or `filled`. +- `pagination` — draw Laravel's and Livewire's paginators in M3 (default `true`). +- `showcase.enabled`, `showcase.path`, `showcase.middleware`, `showcase.vite`. +- `node` — the Node binary for `material:scheme`. + +## Usage + +```blade + + + + + + + + + Recipients lose access at once. + + + + + +``` + +```php +use NoNameWeb\LivewireMaterial\Concerns\Toasts; + +class Shares extends Component +{ + use Toasts; + + public function copy(): void + { + $this->success('Link copied'); + } +} +``` + +Every component, prop and slot is documented in the Boost skill (`resources/boost/skills/livewire-material-development/SKILL.md`) and shown in the showcase. + +## Showcase + +While the application runs locally (or with `MATERIAL_SHOWCASE=true`), `/material` renders every token and component, in every variant, in the application's own scheme and theme. + +## Testing the design + +```php +use NoNameWeb\LivewireMaterial\Testing\DesignGuard; + +it('uses only what compiles', function () { + expect(DesignGuard::scan([resource_path('views'), resource_path('js'), app_path()]) + ->forbidColours(['tertiary']) + ->violations())->toBe([]); +}); +``` + +The guard fails on maryUI tags, daisyUI classes, colours the theme does not declare, unknown Material Symbol names and Blade directives written inside component tags. + +## AI agents + +With [Laravel Boost](https://github.com/laravel/boost), `php artisan boost:install` (or `boost:update --discover`) picks up the package's guideline and the `livewire-material-development` skill. + +## Developing the package + +```bash +composer install && npm install +npm run build # the Workbench's assets (or `npm run dev` while working) +composer serve # the showcase at http://127.0.0.1:8000/material +vendor/bin/pest --testsuite=Feature +npx playwright install +vendor/bin/pest --testsuite=Browser --browser chrome # also firefox, safari +``` + +## Credits + +Material Symbols, the M3 Expressive shapes, Google Sans Flex, material-color-utilities and Jetpack Compose Material 3's tokens and algorithms are Google's and the Android Open Source Project's; see `NOTICE`. + +## License + +MIT. See `LICENSE`. diff --git a/config/livewire-material.php b/config/livewire-material.php index a1fb8244..fb9fb7a5 100644 --- a/config/livewire-material.php +++ b/config/livewire-material.php @@ -8,8 +8,10 @@ return [ |-------------------------------------------------------------------------- | | Every component is an anonymous Blade component. Without a prefix they are - | , and so on; set a prefix such as 'm-' when a name - | clashes with one of the application's own components (). + | , and so on; set a prefix such as 'm' when a name + | clashes with one of the application's own components, and they become + | , . They are always + | as well. | */ diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 8cdccc5c..ad91e0de 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -764,7 +764,7 @@ It fails on maryUI tags, daisyUI classes, colours the theme does not declare, un ## Conventions -- Components are anonymous Blade components: `` without a prefix, or `` when `config('livewire-material.prefix')` is set. +- Components are anonymous Blade components: `` without a prefix, or `` when `config('livewire-material.prefix')` is set; `` always works. - Write class names out whole. Tailwind cannot compile `'text-'.$tone` or `type-{{ $size }}`, and the design guard cannot read them. - The showcase at `/material` (local only, `MATERIAL_SHOWCASE=true` to force it) renders every token and component. diff --git a/resources/views/components/account-menu.blade.php b/resources/views/components/account-menu.blade.php index f8a31661..b5417baf 100644 --- a/resources/views/components/account-menu.blade.php +++ b/resources/views/components/account-menu.blade.php @@ -32,7 +32,7 @@ $initials = $image ? null : ($avatar ?? \Illuminate\Support\Str::of((string) $name)->explode(' ')->filter()->take(2)->map(fn (string $word): string => mb_strtoupper(mb_substr($word, 0, 1)))->join('')); @endphp - + @@ -60,20 +60,20 @@ @endif - + @endif {{ $slot }} @if ($theme) - + {{ __('Theme') }} - + @endif @isset($footer) - + {{ $footer }} @endisset - + diff --git a/resources/views/components/alert.blade.php b/resources/views/components/alert.blade.php index fd234346..dc177cb0 100644 --- a/resources/views/components/alert.blade.php +++ b/resources/views/components/alert.blade.php @@ -40,7 +40,7 @@ {{ $attributes->class(['flex items-start gap-3 rounded-corner-md p-4', $colours]) }} > @if ($icon) - + @endif
@@ -59,7 +59,7 @@ @if ($dismissible) @endif
diff --git a/resources/views/components/app-shell.blade.php b/resources/views/components/app-shell.blade.php index 336f16c6..5788a7c6 100644 --- a/resources/views/components/app-shell.blade.php +++ b/resources/views/components/app-shell.blade.php @@ -88,7 +88,7 @@ class="sr-only focus:not-sr-only focus:fixed focus:start-4 focus:top-[calc(env(safe-area-inset-top)+1rem)] focus:z-[60] focus:rounded-corner-full focus:bg-inverse-surface focus:px-4 focus:py-2 focus:type-label-lg focus:text-inverse-on-surface focus:shadow-elevation-3 focus:outline-none" >{{ __('Skip to content') }} - + @isset($brand) {{ $brand }} @endisset @@ -99,14 +99,14 @@ @foreach ($groups as $group) @if ($group->first()['section'] !== null) - + @foreach ($group as $item) - + @endforeach - + @else @foreach ($group as $item) - + @endforeach @endif @endforeach @@ -122,7 +122,7 @@ @endisset @endif - +
{{ $top ?? '' }} @@ -134,13 +134,13 @@ @if ($barItems->isNotEmpty())
- + @foreach ($barItems as $item) - + @endforeach - +
@endif - +
diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php index 264b04e5..e5e6463a 100644 --- a/resources/views/components/button.blade.php +++ b/resources/views/components/button.blade.php @@ -202,13 +202,13 @@ <{{ $tag }} {{ $attributes }}> @if ($spinnerTarget) - + @endif @if ($icon) - + @endif @@ -217,10 +217,10 @@ @endunless @if ($iconRight) - + @endif @if ($tip !== null) - + @endif diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php index 4c0bb75c..88261d44 100644 --- a/resources/views/components/card.blade.php +++ b/resources/views/components/card.blade.php @@ -60,7 +60,7 @@ @if ($separator) - + @endif @endif diff --git a/resources/views/components/carousel.blade.php b/resources/views/components/carousel.blade.php index 7fade74d..0fea12fb 100644 --- a/resources/views/components/carousel.blade.php +++ b/resources/views/components/carousel.blade.php @@ -136,7 +136,7 @@ 'hidden pointer-fine:flex' => $controls === null, 'flex' => $controls === true, ])> - - - - + + @if (filled($label) || filled($hint)) diff --git a/resources/views/components/chip.blade.php b/resources/views/components/chip.blade.php index 8228708a..c22a7823 100644 --- a/resources/views/components/chip.blade.php +++ b/resources/views/components/chip.blade.php @@ -230,7 +230,7 @@ $disabled]) /> @endif @elseif ($icon) - + @endif {{ $label ?? $slot }} @@ -251,7 +251,7 @@ 'cursor-not-allowed' => $disabled, ]) > - + @endif @@ -260,7 +260,7 @@ @endif @if ($tooltip !== null) - + @endif @else @@ -277,22 +277,22 @@ 'h-4.5 w-0 transition-[width] duration-(--md-sys-motion-effects-default-duration) ease-effects-default group-has-checked/chip:w-4.5 group-has-checked/chip:duration-(--md-sys-motion-spatial-fast-duration) group-has-checked/chip:ease-spatial-fast group-aria-pressed/chip:w-4.5 group-aria-pressed/chip:duration-(--md-sys-motion-spatial-fast-duration) group-aria-pressed/chip:ease-spatial-fast' => blank($icon), ])> @if ($icon) - + @endif - + @elseif ($icon) - + @endif {{ $label ?? $slot }} @if ($iconRight) - + @endif @if ($tooltip !== null) - + @endif @endif diff --git a/resources/views/components/choices.blade.php b/resources/views/components/choices.blade.php index 293f9086..9c695d16 100644 --- a/resources/views/components/choices.blade.php +++ b/resources/views/components/choices.blade.php @@ -115,7 +115,7 @@ @if ($model === null) x-modelable="value" @endif >
- + - + - +
    - +
  • {{ __('Nothing matches') }}
  • @@ -202,9 +202,9 @@ }" @if ($model === null) x-modelable="selection" @endif > - + @foreach ($choices as $index => $choice) - @endforeach - + @endif diff --git a/resources/views/components/collapse.blade.php b/resources/views/components/collapse.blade.php index 5e1a5fbd..e641cc25 100644 --- a/resources/views/components/collapse.blade.php +++ b/resources/views/components/collapse.blade.php @@ -28,12 +28,12 @@ 'px-4' => $variant === 'filled', ])> @if ($icon) - + @endif {{ $heading ?? $title }} - +
    $variant === 'filled', 'pt-1'])> diff --git a/resources/views/components/datepicker.blade.php b/resources/views/components/datepicker.blade.php index 9b5074b4..73528da2 100644 --- a/resources/views/components/datepicker.blade.php +++ b/resources/views/components/datepicker.blade.php @@ -133,7 +133,7 @@ data-datepicker >
    - get('disabled') || $attributes->get('readonly')) data-datepicker-toggle > - + - +
    @@ -241,10 +241,10 @@

    - + - +
    @@ -261,19 +261,19 @@ x-bind:aria-label="monthYear + ', ' + @js(__('Switch to selecting a year'))" > - + - - + +
    - + - + - + - +
    @@ -372,7 +372,7 @@ x-on:click="month.disabled || showMonthOf(+shown.slice(0, 4), month.value)" data-datepicker-option > - + @@ -388,7 +388,7 @@ x-on:click="showMonthOf(year.value, +shown.slice(5, 7))" data-datepicker-option > - + @@ -397,7 +397,7 @@
    - + - + @if ($range) - + - + @endif
    @@ -439,8 +439,8 @@
    - - + +
    diff --git a/resources/views/components/drawer.blade.php b/resources/views/components/drawer.blade.php index cfa17779..33242c77 100644 --- a/resources/views/components/drawer.blade.php +++ b/resources/views/components/drawer.blade.php @@ -113,13 +113,13 @@ @if ($withCloseButton) - + @endif @if ($separator) - + @endif @endif diff --git a/resources/views/components/empty-state.blade.php b/resources/views/components/empty-state.blade.php index 4ca67b91..af3641ec 100644 --- a/resources/views/components/empty-state.blade.php +++ b/resources/views/components/empty-state.blade.php @@ -19,8 +19,8 @@
    class('flex flex-col items-center gap-4 px-4 py-10 text-center') }}>
    - - + +
    @if ($title) diff --git a/resources/views/components/fab-menu-item.blade.php b/resources/views/components/fab-menu-item.blade.php index 8015f140..c013bae0 100644 --- a/resources/views/components/fab-menu-item.blade.php +++ b/resources/views/components/fab-menu-item.blade.php @@ -40,7 +40,7 @@ <{{ $tag }} {{ $attributes }}> @if ($icon) - + @endif {{ $label ?? $slot }} diff --git a/resources/views/components/fab-menu.blade.php b/resources/views/components/fab-menu.blade.php index 12b0821a..2461116e 100644 --- a/resources/views/components/fab-menu.blade.php +++ b/resources/views/components/fab-menu.blade.php @@ -49,8 +49,8 @@ $colours, ]) > - -