Files
livewire-material/resources/boost/skills/livewire-material-development/SKILL.md
T
Andreas Reinhold / reiniandClaude Opus 5 b48e879254
tests / lint (push) Successful in 1m0s
tests / feature (8.4) (push) Successful in 1m0s
tests / feature (8.5) (push) Successful in 1m1s
tests / browser (safari, webkit) (push) Successful in 1m59s
tests / browser (chrome, chromium) (push) Successful in 1m49s
tests / browser (firefox, firefox) (push) Successful in 1m54s
Add the Material 3 Expressive foundation
Colour, shape, type, elevation and motion as tokens and Tailwind
utilities; `php artisan material:scheme`, which generates an app's colour
roles with Google's material-color-utilities (spec 2025); the theme head
script with light, dark and system and its Alpine store; Google Sans Flex;
every Material Symbol (4,135, outlined and filled) drawn by <x-icon>
without blade-icons; all 35 M3 Expressive shapes, ported from androidx, as
<x-shape>; the x-figure directive; the Toasts concern; DesignGuard for
applications' tests; and a showcase with every token, both themes side by
side and an icon search.

Colour utilities are `@theme inline`, so a section with its own
data-theme repaints; without it they resolve once on :root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 05:24:11 +02:00

7.8 KiB

name, description
name description
livewire-material-development Build Laravel and Livewire views with Livewire Material's Material 3 Expressive Blade components — props, slots, colour roles, type, shape, motion, theming, toasts, the design guard, 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

Composer packages must be installed before the Vite build (in Dockerfiles and CI alike), because the application's build imports from vendor/:

/* 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';
// resources/js/app.js
import '../../vendor/nonameweb/livewire-material/resources/js/material.js'

Every layout puts the theme script in <head>, before @vite:

<head>
    <x-theme-script />
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

Colour scheme

The scheme is generated, never hand-edited. Regenerate it with the seed and variant recorded at the top of resources/css/material-scheme.css:

php artisan material:scheme "#4f46e5" --variant=tonal-spot

Variants: tonal-spot (M3's default), vibrant, expressive, neutral, fidelity, content, monochrome, rainbow, fruit-salad. --success, --warning and --info set the source of the state colours; --contrast goes from -1 to 1. The command also writes material-scheme.json beside the stylesheet.

Tokens

Tailwind's default palette is cleared: every colour class names an M3 role. text-red-600, bg-base-200 or text-gray-500 compile to nothing.

  • Colour roles (bg-*, text-*, border-*, …): primary, on-primary, primary-container, on-primary-container, inverse-primary, primary-fixed, primary-fixed-dim, on-primary-fixed, on-primary-fixed-variant; the same for secondary and tertiary; error, on-error, error-container, on-error-container; success, warning and info with their on-, -container and on-…-container; inverse-error|success|warning|info; surface, surface-dim, surface-bright, surface-container-lowest|low||high|highest, on-surface, on-surface-variant, inverse-surface, inverse-on-surface, outline, outline-variant, scrim, shadow; plus white and black.
  • Ink and lines by meaning: text-body (body copy), text-meta (metadata), text-quiet (decoration only), border-structure, border-chrome, border-divider / divide-divider.
  • Type: type-{display|headline|title|body|label}-{lg|md|sm} and type-emphasized-…. Never assemble text-*, leading-* and tracking-* by hand. The font is Google Sans Flex (font-sans).
  • Shape: rounded-corner-{none|xs|sm|md|lg|lg-increased|xl|xl-increased|xxl|full}.
  • Elevation: shadow-elevation-{1…5} — for what floats over content, not for panels (a panel separates by its container tone).
  • Motion: ease-spatial-{fast|default|slow} (position, size, shape; springs that overshoot) and ease-effects-{fast|default|slow} (colour, opacity). Always pair an easing with its duration: duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast. Reduced motion zeroes the durations.
  • States: state-layer (M3's hover/focus/press overlay; makes the element relative and isolate), focus-ring (keyboard focus indicator), link (a link in running text).
  • dark: follows the page's theme (data-theme), not the operating system.
  • x-figure on an element holding one number counts it up on first appearance and on change.

Theme

config/livewire-material.phptheme.default (light, dark or system), theme.storage_key, theme.legacy_keys. In Alpine, $store.theme holds choice (what the visitor picked), resolved (light or dark, what shows), set('light'|'dark'|'system') and toggle(); x-model="$store.theme.value" binds a control.

Toasts

use NoNameWeb\LivewireMaterial\Concerns\Toasts;

class Settings extends Component
{
    use Toasts;

    public function save(): void
    {
        // …
        $this->success('Settings saved');                         // also warning(), error(), info()
        $this->info('Link copied', timeout: 6000);
        $this->success('Share created', redirectTo: route('shares.show', $share));
    }
}

The methods are protected. They dispatch a toast browser event (assertDispatched('toast', type: 'success', title: 'Settings saved') in tests).

Components

<x-icon>

A Material Symbol (Rounded, weight 400, grade 0, 24px), inline. Every symbol on fonts.google.com/icons exists, by Google's name with underscores. An unknown name throws.

Prop Default
name required calendar_month, cloud_upload, content_copy
filled false the filled symbol — M3 uses it for active or selected
label null names the icon for screen readers when it carries the meaning alone; otherwise it is aria-hidden

24px (size-6) unless a size-*, w-* or h-* class is passed. Colour follows the text: <x-icon name="lock" class="size-5 text-on-surface-variant" />.

<x-shape>

One of M3 Expressive's 35 shapes, filled in the text colour, aria-hidden, sized by its caller: <x-shape name="cookie-9" class="size-40 text-secondary-container" />. Names: circle, square, slanted, arch, fan, arrow, semi-circle, oval, pill, triangle, diamond, clam-shell, pentagon, gem, very-sunny, sunny, cookie-4, cookie-6, cookie-7, cookie-9, cookie-12, ghostish, clover-4, clover-8, burst, soft-burst, boom, soft-boom, flower, puffy, puffy-diamond, pixel-circle, pixel-triangle, bun, heart.

<x-theme-script>

The theme decided before the first paint. Exactly once per layout, in <head>, before @vite. No props; configured in config/livewire-material.php.

Testing the design

use NoNameWeb\LivewireMaterial\Testing\DesignGuard;

it('uses only what compiles', function () {
    expect(DesignGuard::scan([resource_path('views'), resource_path('js'), app_path()])
        ->forbidColours(['tertiary'])          // roles this application's rules leave out
        ->violations())->toBe([]);
});

It fails on maryUI tags, daisyUI classes, colours the theme does not declare and unknown symbol names, with path:line for each.

Conventions

  • Components are anonymous Blade components: <x-name> without a prefix, or <x-{prefix}name> when config('livewire-material.prefix') is set.
  • 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.

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.