tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m4s
tests / lint (push) Successful in 59s
tests / browser (chrome, chromium) (push) Successful in 2m54s
tests / browser (firefox, firefox) (push) Successful in 3m3s
tests / browser (safari, webkit) (push) Successful in 4m13s
Outlined and filled text fields (input, password, auto-growing textarea, native select with the customizable select menu, file), checkbox with an indeterminate state, radio buttons, the M3 switch and form, ported from ReStride and extended. The first half of Phase 6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
429 lines
26 KiB
Markdown
429 lines
26 KiB
Markdown
---
|
|
name: livewire-material-development
|
|
description: 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/`:
|
|
|
|
```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'
|
|
```
|
|
|
|
Every layout puts the theme script in `<head>`, before `@vite`:
|
|
|
|
```blade
|
|
<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`:
|
|
|
|
```bash
|
|
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.php` → `theme.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
|
|
|
|
```php
|
|
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`.
|
|
|
|
### `<x-button>`
|
|
|
|
Label button, icon button, toggle and responsive FAB in one component.
|
|
|
|
| Prop | Default | |
|
|
|---|---|---|
|
|
| `label` / slot | | the words; without them and with an `icon` it is an icon button |
|
|
| `variant` | `text` | `filled`, `tonal`, `outlined`, `elevated`, `text` |
|
|
| `color` (alias `tone`) | `primary` | `primary`, `secondary`, `tertiary`, `error`, `success`, `warning`, `info` |
|
|
| `primary`, `danger`, `caution` | | shorthands: filled primary, filled error, filled warning |
|
|
| `size` | `sm` | `xs` 32px, `sm` 40px, `md` 56px, `lg` 96px, `xl` 136px |
|
|
| `shape` | `round` | or `square`; both square off further while pressed |
|
|
| `icon`, `icon-right` | | Material Symbol names |
|
|
| `width` | `default` | icon buttons only: `narrow`, `default`, `wide` |
|
|
| `selected` | `null` | `true`/`false` makes it a toggle (`aria-pressed`, selected colours and shape) |
|
|
| `link`, `external`, `no-wire-navigate` | | renders `<a>`, with `wire:navigate` unless external |
|
|
| `spinner` | | `true` shows the loading indicator while its `wire:click` runs; a string names the action |
|
|
| `tooltip`, `tooltip-left`, `tooltip-right`, `tooltip-bottom` | | plain tooltip; also the icon button's accessible name |
|
|
| `disabled`, `type`, `responsive`, `fab` | | `responsive` hides the label below `lg`; `fab` is an extended FAB below `sm`, a filled button above |
|
|
|
|
```blade
|
|
<x-button label="Create link" icon="link" variant="filled" size="md" wire:click="create" spinner />
|
|
<x-button icon="delete" tooltip="Delete share" wire:click="delete({{ $share->id }})" />
|
|
<x-button icon="favorite" aria-label="Keep" variant="tonal" :selected="$kept" wire:click="toggleKeep" />
|
|
```
|
|
|
|
### `<x-tooltip>`
|
|
|
|
M3's plain tooltip, standalone around any trigger: `<x-tooltip text="Copy link" side="bottom"><button>…</button></x-tooltip>`. `side`: `top` (default), `bottom`, `left`, `right`. Shows on hover (fine pointers) and keyboard focus; `aria-hidden`, so the trigger still needs its own accessible name. Buttons and FABs take a `tooltip` prop instead.
|
|
|
|
### `<x-menu>`, `<x-menu-item>`, `<x-menu-group>`, `<x-menu-separator>`
|
|
|
|
```blade
|
|
<x-menu label="Share actions" position="bottom-end">
|
|
<x-slot:trigger>
|
|
<x-button icon="more_vert" tooltip="More" />
|
|
</x-slot:trigger>
|
|
|
|
<x-menu-group label="Sort by">
|
|
<x-menu-item label="Newest" :selected="$sort === 'newest'" wire:click="$set('sort', 'newest')" keep-open />
|
|
</x-menu-group>
|
|
<x-menu-separator />
|
|
<x-menu-item label="Settings" icon="settings" link="{{ route('settings') }}" />
|
|
<x-menu-item label="Delete" icon="delete" wire:click="delete" description="Recipients lose access" shortcut="⌘⌫" />
|
|
</x-menu>
|
|
```
|
|
|
|
`<x-menu>`: `trigger` slot (its first button or link becomes the menu button), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`. `<x-menu-item>`: `label`, `icon`, `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`), `disabled`, `keep-open`. Choosing an item closes the menu unless `keep-open`. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab.
|
|
|
|
### `<x-button-group>`
|
|
|
|
A row of `<x-button>`s: `<x-button-group label="View" size="md">…</x-button-group>`. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside.
|
|
|
|
### `<x-group>`
|
|
|
|
A choice between a few options as a connected button group of native radios (checkboxes with `multiple`):
|
|
|
|
```blade
|
|
<x-group label="Expires after" wire:model.live="expiry" :options="[
|
|
['id' => '1h', 'name' => '1 hour'],
|
|
['id' => '1d', 'name' => '1 day', 'icon' => 'today'],
|
|
['id' => '7d', 'name' => '7 days', 'disabled' => true],
|
|
]" hint="Recipients lose access after that" />
|
|
```
|
|
|
|
Props: `label`, `hint`, `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint.
|
|
|
|
### `<x-split-button>`
|
|
|
|
```blade
|
|
<x-split-button label="Download all" icon="download" wire:click="downloadZip" menu-label="Download options">
|
|
<x-menu-item label="Download files one by one" wire:click="downloadEach" />
|
|
</x-split-button>
|
|
```
|
|
|
|
Attributes go to the leading button; the slot is the menu. `variant` (`filled` default, `tonal`, `outlined`, `elevated`), `color`, `size`, `disabled`, `spinner`, `menu-label`, `position`.
|
|
|
|
### `<x-fab>`
|
|
|
|
`<x-fab icon="add" tooltip="New share" />` — `size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself; wrap it (`<div class="fixed end-4 bottom-4">`). `link`, `external`, `disabled`, `type`.
|
|
|
|
### `<x-fab-menu>`, `<x-fab-menu-item>`
|
|
|
|
```blade
|
|
<div class="fixed end-4 bottom-4">
|
|
<x-fab-menu label="New">
|
|
<x-fab-menu-item label="Upload files" icon="upload_file" wire:click="uploadFiles" />
|
|
<x-fab-menu-item label="Paste text" icon="content_paste" link="{{ route('paste') }}" />
|
|
</x-fab-menu>
|
|
</div>
|
|
```
|
|
|
|
Two to six items open above the FAB, which turns into a close button. `<x-fab-menu>`: `icon` (`add`), `label`, `color`, `position` (`top-end` default). Give items the same `color`. Keyboard as `<x-menu>`.
|
|
|
|
### `<x-loading>`
|
|
|
|
M3 Expressive's loading indicator — a shape morphing through seven Expressive shapes as it turns — for a wait of unknown length. 48px and `primary` unless sized or coloured by class; `contained` sets it on a primary-container circle. A `progressbar` named by `label` ("Loading"); `:label="false"` makes it decorative. It rests under reduced motion.
|
|
|
|
```blade
|
|
<x-loading />
|
|
<x-loading contained class="size-8" label="Uploading" />
|
|
<div wire:loading.flex wire:target="upload"><x-loading /></div>
|
|
```
|
|
|
|
`<x-button spinner>` shows a decorative one in place of its icon.
|
|
|
|
### `<x-toast>`
|
|
|
|
The snackbar host. Once per layout, near the end of `<body>`: `<x-toast />` (`position="bottom-start"` to leave the centre free). It is `@persist`ed across `wire:navigate` and shows, one at a time, every toast from the `Toasts` concern (see Toasts above) or from JavaScript:
|
|
|
|
```js
|
|
materialToast('Share deleted', { type: 'success', description: null, timeout: 4000, action: { label: 'Undo', handler: () => $wire.restore() } })
|
|
```
|
|
|
|
`type` (`success`, `error`, `warning`, `info`) adds the state icon; `timeout: 0` keeps it until dismissed; a toast with an action or no timeout gets a close button. Hover or focus pauses the timer.
|
|
|
|
### `<x-progress>`
|
|
|
|
M3 Expressive's progress indicator: linear (as wide as its container) or `circular` (40px, 48px wavy, unless a `size-*` class is passed), flat or `wavy`, determinate with a `value` or indeterminate without one.
|
|
|
|
| Prop | Default | |
|
|
|---|---|---|
|
|
| `value` | `null` | 0 to `max`, clamped; `null` is indeterminate |
|
|
| `max` | `100` | |
|
|
| `bind` | `null` | an Alpine expression it follows in the browser; `null`/`undefined` is indeterminate |
|
|
| `circular` | `false` | circular instead of linear |
|
|
| `wavy` | `false` | Expressive's wave (flat below 10% and from 95%) |
|
|
| `thick` | `false` | 8px track and indicator instead of 4px |
|
|
| `color` | `primary` | `primary`, `secondary`, `tertiary`, `error`, `success`, `warning`, `info`; the track is the colour's container (secondary-container for primary) |
|
|
| `label` | `"Progress"` | names the `progressbar`; `:label="false"` makes it decorative |
|
|
|
|
```blade
|
|
<x-progress :value="$share->uploaded" :max="$share->size" label="Uploading" />
|
|
<x-progress circular wavy label="Preparing the download" />
|
|
|
|
<div x-data="{ progress: null }" x-on:livewire-upload-progress="progress = $event.detail.progress" x-on:livewire-upload-finish="progress = null">
|
|
<input type="file" wire:model="file">
|
|
<div x-show="progress !== null"><x-progress bind="progress" wavy label="Uploading" /></div>
|
|
</div>
|
|
```
|
|
|
|
A value the server changes animates after a morph (the SVG is `wire:ignore`; only the root's attributes change). Under reduced motion values jump, the wave stands still and an indeterminate indicator holds one frame. A `w-*` class narrows a linear one; never pass a display or position class.
|
|
|
|
### `<x-badge>`
|
|
|
|
- `<x-badge />` — M3's small badge, a dot. `<x-badge value="4" max="99" />` — M3's large badge, a count. Both `error` by default. `floating` pins it to the top-end corner of a `relative` parent: `<span class="relative inline-flex"><x-icon name="mail" /><x-badge value="4" floating /></span>`. A dot or count is `aria-hidden` unless it has a `label`; name the control instead ("Messages, 4 unread").
|
|
- `<x-badge value="Expired" tonal />`, `<x-badge value="Active" color="success" tonal />`, `<x-badge value="Pro" outline />` — a status label (not an M3 badge) in the colour's container or a neutral edge. `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`.
|
|
|
|
### `<x-alert>`
|
|
|
|
A notice in the page, in the state's container colour with its icon:
|
|
|
|
```blade
|
|
<x-alert title="Storage almost full" description="3.8 GB of 4 GB used." color="warning" />
|
|
<x-alert color="error" dismissible>
|
|
The upload failed.
|
|
<x-slot:actions><x-button label="Try again" wire:click="retry" /></x-slot:actions>
|
|
</x-alert>
|
|
```
|
|
|
|
`color` (alias `tone`): `info` (default), `success`, `warning`, `error`, `primary`, `secondary`, `tertiary`, `neutral`. `icon` overrides the state icon; `:icon="false"` removes it. Errors and warnings are `role="alert"`, the rest `role="status"`.
|
|
|
|
### `<x-rich-tooltip>`
|
|
|
|
A few lines of context around a trigger, with an optional `title` and `actions` slot:
|
|
|
|
```blade
|
|
<x-rich-tooltip title="Expiry" text="Recipients lose access after this time.">
|
|
<x-button icon="help" aria-label="About expiry" />
|
|
</x-rich-tooltip>
|
|
```
|
|
|
|
Shows on hover and keyboard focus; `persistent` opens it on press and keeps it until a press elsewhere or Escape (use it when there are actions). `side`: `bottom` (default), `top`, `left`, `right`.
|
|
|
|
### `<x-stat>`
|
|
|
|
`<x-stat title="Shares" value="1,204" icon="link" description="12 this week" />` — a figure on a surface-container panel; the value counts up on first appearance and when it changes. The slot goes under the description (e.g. a quota's `<x-progress>`). Do not pass a `bg-*` class; wrap it.
|
|
|
|
### `<x-empty-state>`
|
|
|
|
"Nothing here yet": `icon` on an Expressive `shape` (`cookie-9` by default), `title`, `description` or slot, and an `actions` slot. Use it for an empty collection, not for a filter that matched nothing.
|
|
|
|
### `<x-card>`
|
|
|
|
`variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`; slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). Do not pass `bg-*`; use `variant`.
|
|
|
|
A card or list item that opens something is a **row**: `data-list-row` on it and `data-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `<a>` or use a stretched link.
|
|
|
|
```blade
|
|
<x-card variant="outlined" data-list-row wire:key="share-{{ $share->id }}">
|
|
<a href="{{ route('shares.show', $share) }}" data-list-open wire:navigate class="type-title-md">{{ $share->name }}</a>
|
|
<x-slot:actions><x-button label="Copy link" wire:click="copy({{ $share->id }})" /></x-slot:actions>
|
|
</x-card>
|
|
```
|
|
|
|
### `<x-list>`, `<x-list-item>`
|
|
|
|
`<x-list>`: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). `<x-list-item>`: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content.
|
|
|
|
```blade
|
|
<x-list segmented label="Files">
|
|
@foreach ($files as $file)
|
|
<x-list-item :title="$file->name" :description="$file->size" icon="description" wire:key="file-{{ $file->id }}">
|
|
<x-slot:end><x-button icon="download" tooltip="Download" wire:click="download({{ $file->id }})" /></x-slot:end>
|
|
</x-list-item>
|
|
@endforeach
|
|
</x-list>
|
|
```
|
|
|
|
### `<x-divider>`
|
|
|
|
`<x-divider />` — outline-variant line; `vertical`, `inset` (16px start), `middle`, `decorative` (hidden from assistive tech).
|
|
|
|
### `<x-collapse>`
|
|
|
|
A disclosure on native `<details>`: `<x-collapse title="Advanced" icon="tune" open variant="filled">…</x-collapse>` (`variant` `plain` or `filled`; `heading` slot for rich titles). Keeps its state through a morph.
|
|
|
|
### `<x-modal>`
|
|
|
|
An M3 dialog on native `<dialog>`. Bind with `wire:model` to a flag or an id; closing (Escape, scrim, `close()`) writes back `false` or `null`. Without `wire:model` it uses `open` from the surrounding Alpine scope.
|
|
|
|
```blade
|
|
<x-modal wire:model="deletingId" title="Delete this share?" subtitle="Recipients lose access at once." icon="delete">
|
|
<x-slot:actions>
|
|
<x-button label="Cancel" x-on:click="close()" />
|
|
<x-button label="Delete" danger wire:click="delete" />
|
|
</x-slot:actions>
|
|
</x-modal>
|
|
```
|
|
|
|
Props: `title`, `subtitle`, `icon` (centred hero icon), `separator`, `persistent` (no Escape or scrim), `fullscreen` (whole screen below `sm`, for forms), `box-class`. Never remove its `wire:ignore.self` behaviour by re-rendering it conditionally with `@if`; toggle the bound property instead.
|
|
|
|
### `<x-drawer>`
|
|
|
|
An M3 side sheet, bound like `<x-modal>`; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button`, `close-on-escape` (default true), `without-backdrop-close`, `actions` slot. `pane` (with `pane-width`) turns it into a list-detail pane from `xl`: render it after the list inside `<div class="xl:flex xl:items-start xl:gap-6">`. Its body is a size container — lay out inside with `@md:` etc., not `sm:`.
|
|
|
|
### `<x-bottom-sheet>`
|
|
|
|
An M3 bottom sheet, bound like `<x-modal>`: modal by default (scrim, inert page, drag the handle down or press Escape to close), `standard` for one that is part of the page. Props: `title`, `height` (`90dvh`), `actions` slot.
|
|
|
|
### `<x-carousel>`, `<x-carousel-item>`
|
|
|
|
```blade
|
|
<x-carousel label="Recent uploads" item-width="220">
|
|
@foreach ($photos as $photo)
|
|
<x-carousel-item :label="$photo->title" wire:key="photo-{{ $photo->id }}">
|
|
<img src="{{ $photo->url }}" alt="{{ $photo->alt }}" />
|
|
</x-carousel-item>
|
|
@endforeach
|
|
</x-carousel>
|
|
```
|
|
|
|
A row of items that change size between M3's keylines as it scrolls (native scroll snap; items are masked, content keeps its size). `<x-carousel>`: `layout` (`multi-browse` default, `hero`, `uncontained`, `full-screen`), `item-width` (px or any CSS length; the large size multi-browse aims for, the fixed size uncontained keeps, the cap for hero; 186 by default), `height` (205px), `padding` (px at the ends, 0), `centered` (hero), `label` (the region's name, "Carousel" by default), `controls` (previous/next buttons: default fine pointers only, `true` always, `false` never). `<x-carousel-item>`: slot is an `<img>` (fills and crops) or an element sized `size-full`; `label` overlays a line of text. A focusable `region` of `slide` groups named "n of m"; arrow keys move one item while the row has focus, Home/End to the ends. Works after a Livewire morph, in RTL and under reduced motion. Give items a `wire:key` in a loop.
|
|
|
|
### `<x-form>`
|
|
|
|
A one-column grid of fields with an `actions` slot at the foot (the slot takes its own `class`); `separator` draws a divider above the actions.
|
|
|
|
```blade
|
|
<x-form wire:submit="save">
|
|
<x-input label="Share name" wire:model="name" required />
|
|
<x-select label="Expires after" wire:model="hours" :options="$expiryOptions" />
|
|
<x-slot:actions>
|
|
<x-button label="Cancel" wire:click="cancel" />
|
|
<x-button label="Create share" variant="filled" type="submit" spinner="save" />
|
|
</x-slot:actions>
|
|
</x-form>
|
|
```
|
|
|
|
### `<x-field>`, `<x-input>`, `<x-password>`, `<x-textarea>`, `<x-select>`, `<x-file>`
|
|
|
|
M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant`, and read their errors from the bag under the `wire:model` name (the error replaces the hint, sets `aria-invalid`). `class` lands on the field's outer element (margins, widths); every other attribute (`wire:model`, `type`, `required`, `readonly`, `autocomplete`) reaches the control. Never pass `placeholder` expecting it to show while a label rests in the field: it shows once the field has focus.
|
|
|
|
- `<x-input>`: `icon`, `icon-right`, `prefix`, `suffix`, `clearable`, `copyable` (copies the value, confirms with a snackbar), `size` (`sm` 40px, `xs` 32px — for unlabelled toolbar controls; give them `aria-label`), `mono`.
|
|
- `<x-password>`: a reveal button; `icon`, `size`.
|
|
- `<x-textarea>`: grows from `rows` (3) to `max-rows`, then scrolls; `:autogrow="false"` for a fixed, hand-resizable one.
|
|
- `<x-select>`: native `<select>` (M3 menu where the browser supports customizable selects). `options` as `['id' => …, 'name' => …, 'disabled' => bool]`, `option-value`, `option-label`, `placeholder` + `placeholder-value`, or `<option>`s in the slot; `icon`, `size`.
|
|
- `<x-file>`: native file input; errors from `photos` and `photos.*`. Show previews of what was chosen yourself.
|
|
- `<x-field id="…" label="…" :messages="$messages">` wraps a custom control given `class="field-control"`; only for controls the package does not have.
|
|
|
|
### `<x-checkbox>`, `<x-radio>`, `<x-toggle>`
|
|
|
|
M3 selection controls on native inputs; the whole row is the label.
|
|
|
|
- `<x-checkbox label hint right indeterminate />` — `indeterminate` for a "select all" whose items are partly ticked (bind it to a server expression; it follows every render).
|
|
- `<x-radio label wire:model :options inline />` — options `['id', 'name', 'hint', 'disabled']` (`option-value`, `option-label`, `option-hint`); `value` checks an option without `wire:model`; `name` names an unbound group.
|
|
- `<x-toggle label hint right icons />` — M3 switch (`role="switch"`); `icons` puts a check and a cross on the handle, `icons="selected"` only the check. Without `label`, pass `aria-label`.
|
|
|
|
```blade
|
|
<x-checkbox label="All files" :checked="count($selected) === $files->count()" :indeterminate="$selected && count($selected) < $files->count()" wire:click="toggleAll" />
|
|
<x-toggle label="Notify me on download" wire:model.live="notify" right />
|
|
```
|
|
|
|
## 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']) // roles this application's rules leave out
|
|
->violations())->toBe([]);
|
|
});
|
|
```
|
|
|
|
It fails on maryUI tags, daisyUI classes, colours the theme does not declare, unknown symbol names and Blade directives written inside a component tag (where they do not compile), 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.
|