From c552ee9f9d701fa267aaf8144006ce1d385672db Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Wed, 16 Sep 2026 21:52:04 +0200 Subject: [PATCH] Update PHP and npm dependencies Everything stays within its existing constraint: Laravel 13.32, Livewire 4.4.5, Livewire Material 2.1.0, Pest 5.2, Boost 2.9, Pint 1.32.1, Vite 8.3 and autoprefixer 10.6. Boost's copy of the guidelines and skills follows Livewire Material 2.1, which is plain CSS without utilities. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/infer-conventions/SKILL.md | 3 +- .../livewire-material-development/SKILL.md | 174 +++-- .claude/skills/material-3-design/SKILL.md | 240 ++++--- CLAUDE.md | 59 +- composer.lock | 453 +++++++----- package-lock.json | 672 +++++++++--------- package.json | 4 +- 7 files changed, 880 insertions(+), 725 deletions(-) diff --git a/.claude/skills/infer-conventions/SKILL.md b/.claude/skills/infer-conventions/SKILL.md index 11a9327..c9b549f 100644 --- a/.claude/skills/infer-conventions/SKILL.md +++ b/.claude/skills/infer-conventions/SKILL.md @@ -1,6 +1,7 @@ --- name: infer-conventions -description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand." +description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Only run this skill when the user explicitly asks for it; never start a sweep as part of another task. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand." +disable-model-invocation: true license: MIT metadata: author: laravel diff --git a/.claude/skills/livewire-material-development/SKILL.md b/.claude/skills/livewire-material-development/SKILL.md index 13676b0..a7b52cb 100644 --- a/.claude/skills/livewire-material-development/SKILL.md +++ b/.claude/skills/livewire-material-development/SKILL.md @@ -48,7 +48,7 @@ php artisan material:scheme "#4f46e5" --variant=tonal-spot Variants: `tonal-spot` (M3's default), `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--spec` is the colour spec: `2025` (default, M3 Expressive) or `2021` (M3's original colour — keep it for a palette generated before Expressive; the library itself uses 2021 for variants 2025 does not define, and the header records the spec actually used). `--success`, `--warning` and `--info` set the source of the state colours; `--harmonize` pulls those three towards the seed (off by default: a state has to stay recognisable). The header of the stylesheet records the whole command, every option that differs from its default included. The command also writes `material-scheme.json` beside the stylesheet. -`success`, `warning` and `info` are built exactly as M3 builds `error` — dynamic colours on their own tonal palette — so they follow the scheme's spec, its dark tones and its contrast level like every other role. Use them as roles (`bg-success`, `text-on-warning-container`), never as a hex. +`success`, `warning` and `info` are built exactly as M3 builds `error` — dynamic colours on their own tonal palette — so they follow the scheme's spec, its dark tones and its contrast level like every other role. Use them as roles (`md-ink-success` on text, `var(--md-sys-color-on-warning-container)` in your own CSS, a component's `color="warning"`), never as a hex. ### Contrast levels @@ -90,19 +90,42 @@ Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile')); ## Tokens -2.0.0's vocabulary is plain CSS, without Tailwind. `resources/css/foundation.css` is the one required import, first in the entry, followed by the stylesheets of the components the views render (see Setup; 1.x's `material.css` shortcut is gone). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, the page's `surface` and `on-surface` in the brand typeface smoothed in grayscale (drop Tailwind's `antialiased`), every `--md-sys-*` token (spacing included: `--md-sys-measurement-space25` … `space900`, 2–72px), and `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link`. Text on plain elements takes the fixed text classes of `resources/css/text.css` and nothing else: `md-type-{display|headline|title|body|label}-{lg|md|sm}` and `md-type-emphasized-…`; `md-ink` (on-surface), `md-ink-variant`, `md-ink-quiet`, `md-ink-primary`, `md-ink-error`, `md-ink-success`, `md-ink-warning`, `md-ink-info`, `md-ink-inverse`; `md-text-start|center|end`, `md-truncate`, `md-line-clamp-2|3`, `md-nowrap`, `md-tabular`, `md-mono`, `md-visually-hidden`. Monospace is for a value read or typed character by character (a token, a recovery code, a file name, a key): put it in ``, `` or ``, which take `--md-ref-typeface-mono` already, or give another element `md-mono` after its `md-type-*` class; `` for a field. `--md-ref-typeface-mono` is the package's addition, the platform's monospace faces, not an M3 token. Every component is plain CSS now; the Tailwind names below stay for the showcase and an application's own views, until Tailwind itself leaves the package. +2.0.0's vocabulary is plain CSS. There are no utility classes: a Tailwind class, or one of 1.x's own colour, type, shape, elevation, motion or breakpoint utilities, compiles to nothing, and the design guard names the replacement for each (see Testing the design). `resources/css/foundation.css` is the one required import, first in the entry, followed by the stylesheets of the components the views render (see Setup; 1.x's `material.css` shortcut is gone). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, the page's `surface` and `on-surface` in the brand typeface smoothed in grayscale (drop Tailwind's `antialiased`), every `--md-sys-*` token, and the classes below. An application's views reach for four things, in this order: -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. The rules behind the names below — which role, surface container, corner, type style, elevation level, motion spring, state and window size class to use, and M3's don'ts — are the `material-3` guideline (always on) and the `material-3-design` skill (the tables and Google's source pages); activate that skill before designing a screen. +- **A component's own props** — `color`, `variant`, `size`, `dense` — before anything else. +- **Layout components** for arrangement and spacing: ``, ``, ``, ``, `` inside ``s (see Layout), with `gap` and `padding` as spacing tokens (`space25` … `space900`, 2–72px) and `hide-below`, `hide-from` and `stack-below` for the window size classes. Nothing arranges by class. +- **The fixed text and interaction classes** on plain elements, and nothing else: + - Type, from `resources/css/text.css`: `md-type-{display|headline|title|body|label}-{lg|md|sm}` and `md-type-emphasized-…` — M3's 15 styles and their emphasized twins, one weight step heavier, rounder, with their own tracking; each sets size, line height, weight and tracking together. Emphasis is deliberate and one element at a time (M3 asks for it on badges, primary buttons, selected rows, headlines), so a regular class inside an emphasized one goes back to plain. + - Ink, from the same file: `md-ink` (on-surface), `md-ink-variant` (on-surface-variant: body copy beside a title, metadata), `md-ink-quiet` (outline: decoration only), `md-ink-primary`, `md-ink-error`, `md-ink-success`, `md-ink-warning`, `md-ink-info`, `md-ink-inverse`. On a container the ink is the container's `on-` role, which the component sets. + - Text layout: `md-text-start|center|end`, `md-truncate`, `md-line-clamp-2|3`, `md-nowrap`, `md-tabular`, `md-mono`, `md-visually-hidden`. Monospace is for a value read or typed character by character (a token, a recovery code, a file name, a key): put it in ``, `` or ``, which take `--md-ref-typeface-mono` already, or give another element `md-mono` after its `md-type-*` class; `` for a field. `--md-ref-typeface-mono` is the package's addition, the platform's monospace faces, not an M3 token. + - Interaction, from `foundation/interaction.css`: `md-state-layer` (M3's hover, focus, press and dragged overlay in the content colour; makes the element `position: relative` and `isolation: isolate`; `data-md-dragged` draws the dragged layer), `md-focus-ring` (the keyboard focus indicator), `md-touch-target` (a 48px target around a smaller drawing), `md-link` (a link in running text, underlined; add `md-ink-primary` where it should read as primary). +- **The application's own stylesheet** for the rest, on the tokens, never on a literal value: + - Colour roles, `var(--md-sys-color-…)`: `primary`, `on-primary`, `primary-container`, `on-primary-container`, `primary-dim`, `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`, `error-dim`; `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` (a boundary that must be read), `outline-variant` (a divider, a card's edge), `scrim`, `shadow`. There is no white or black. + - Type: `font: var(--md-sys-typescale-body-md)` with `letter-spacing: var(--md-sys-typescale-body-md-tracking)`, never a size, weight or line height of your own. The typefaces are `--md-ref-typeface-brand` (Google Sans Flex), `--md-ref-typeface-plain` and `--md-ref-typeface-mono`; the weights `--md-ref-typeface-weight-regular|medium|bold`. + - Shape: `var(--md-sys-shape-corner-{none|xs|sm|md|lg|lg-increased|xl|xl-increased|xxl|full})`. + - Elevation: `box-shadow: var(--md-sys-elevation-{1…5})` — for what floats over content, not for panels (a panel separates by its container tone). + - Motion: `var(--md-sys-motion-spatial-{fast|default|slow})` (position, size, shape; springs that overshoot) and `var(--md-sys-motion-effects-{fast|default|slow})` (colour, opacity, which never overshoot), each always with its own `-duration`: `transition: transform var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast)` — M3's published web durations, spatial 350/500/650 ms and effects 150/200/300 ms. `motion.scheme` in the config picks `expressive` (the default, with the bounce) or `standard` (minimal bounce), which the head script writes to `` and which swaps the three spatial springs; a rule names a spring, never a scheme. Reduced motion zeroes every duration in both schemes. + - Spacing: `var(--md-sys-measurement-space25)` … `var(--md-sys-measurement-space900)`. + - States: `--md-sys-state-{hover|focus|pressed|dragged}-state-layer-opacity` and `--md-sys-state-disabled-{content|container}-opacity`, mixed into a role: `color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)`. A scrim is `color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent)`. -- 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-…` — M3's 15 styles and their emphasized twins, one weight step heavier, rounder, with their own tracking. Never assemble `text-*`, `leading-*` and `tracking-*` by hand; a utility carries size, line height and tracking together. Emphasis is deliberate and one element at a time (M3 asks for it on badges, primary buttons, selected rows, headlines), so a regular utility inside an emphasized one goes back to plain. 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, which never overshoot). Always pair an easing with its duration: `duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast` — M3's published web durations, spatial 350/500/650 ms and effects 150/200/300 ms. `motion.scheme` in the config picks `expressive` (the default, with the bounce) or `standard` (minimal bounce), which the head script writes to `` and which swaps the three spatial springs; a component names a spring, never a scheme. Reduced motion zeroes every duration in both schemes. -- 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). -- Breakpoints are M3's window size classes, and only those: `medium:` 600px, `expanded:` 840px, `large:` 1200px, `extra-large:` 1600px, with `max-medium:` … for "below" (compact is below `medium`). Tailwind's `sm:`…`2xl:` are cleared — a `sm:` compiles to nothing — because 640px means nothing in M3. Scripts ask `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`) so a stylesheet and a script never disagree at the boundary pixel; a component's *own* width is a container query (`@md:`), which is a different thing. -- `dark:` follows the page's theme (`data-theme`), not the operating system. +The rules behind these names — which role, surface container, corner, type style, elevation level, motion spring, state and window size class to use, and M3's don'ts — are the `material-3` guideline (always on) and the `material-3-design` skill (the tables and Google's source pages); activate that skill before designing a screen. + +```css +/* resources/css/app.css, after the imports */ +.quota-meter { + border-radius: var(--md-sys-shape-corner-md); + background-color: var(--md-sys-color-surface-container-high); + padding: var(--md-sys-measurement-space200); + transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); + + @media (width >= 840px) { + padding: var(--md-sys-measurement-space300); + } +} +``` + +- Breakpoints are M3's window size classes, and only those: compact below 600px, medium 600px, expanded 840px, large 1200px, extra-large 1600px. A layout component takes them as props; your own CSS writes them as range media queries (`@media (width >= 840px)`, `@media (width < 600px)`), and the design guard rejects any other width; a script asks `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`), so a stylesheet and a script never disagree at the boundary pixel. A component's *own* width is a container query (`@container (width >= 28rem)`), which is a different thing. +- The roles switch with the theme by themselves. A rule of your own that only applies in dark keys on `[data-theme='dark']` (the page's theme), never on `prefers-color-scheme` (the operating system's). - `x-figure` on an element holding one number counts it up on first appearance and on change. ## Theme @@ -169,7 +192,7 @@ Laravel's HTTP error pages — 403, 404, 419, 429, 500, 503, and the framework's ## Mail -Markdown mail (notifications and `markdown:` mailables) wears M3 once the application selects the theme: +Markdown mail (notifications, and mailables built with `->markdown()`) wears M3 once the application selects the theme: ```dotenv MAIL_MARKDOWN_THEME=livewire-material::mail.theme @@ -216,7 +239,7 @@ M3's optical size axis redraws a symbol so its strokes look equally heavy at eve ### `` -One of M3 Expressive's 35 shapes, filled in the text colour, `aria-hidden`, sized by its caller: ``. 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`. +One of M3 Expressive's 35 shapes, filled in the text colour, `aria-hidden`: ``. `size` is its size in px (8 to 1024); without it your own CSS sizes it. 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`. ### `` @@ -269,7 +292,7 @@ M3's plain tooltip, standalone around any trigger: ` ``` -``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`, `filter`, `sheet-at-compact`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="text-sport-run"`), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`, ticked at its end unless it has an `icon-right`), `current` (for a menu of places: marks the page you are on with `aria-current="page"` in secondary-container, never a checked choice), `badge` (`true` for a dot, or a count, at the end of the row), `disabled`, `keep-open`, `submenu`. Choosing an item closes the menu unless `keep-open`; a second press on the menu button closes it too. An open menu stays open while the Livewire component around it renders, a `keep-open` item's own `wire:click` included. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab; a `disabled` item keeps its place in that order, as M3 asks, but cannot be activated. A menu longer than the window scrolls. +``: `trigger` slot (its first button or link becomes the menu button, and the menu hangs on that button — a `position: fixed` trigger such as `` carries it along, and a menu with no room flips to the other side, end or both), `label`, `position` (`bottom-start` default, `bottom-end`, `top-start`, `top-end`), `vibrant`, `filter`, `sheet-at-compact`. ``: `label`, `icon`, `icon-class` (classes for the leading icon; a colour there paints it, a selected item's too, but not a disabled one's — `icon-class="sport-run"`, a class of your own), `icon-right`, `description`, `shortcut`, `link`, `external`, `selected` (makes it a `menuitemcheckbox`, ticked at its end unless it has an `icon-right`), `current` (for a menu of places: marks the page you are on with `aria-current="page"` in secondary-container, never a checked choice), `badge` (`true` for a dot, or a count, at the end of the row), `disabled`, `keep-open`, `submenu`. Choosing an item closes the menu unless `keep-open`; a second press on the menu button closes it too. An open menu stays open while the Livewire component around it renders, a `keep-open` item's own `wire:click` included. Keyboard: arrows, Home, End, a letter, Escape (focus returns to the trigger), Tab; a `disabled` item keeps its place in that order, as M3 asks, but cannot be activated. A menu longer than the window scrolls. `submenu` makes an item a menu of its own — the slot holds the nested ``s instead of a label, and they open beside it, on its end, flipping to its start where the window has no room: @@ -345,14 +368,14 @@ Attributes go to the leading button; the slot is the menu. `variant` (`filled` d ### `` -`` — `size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. The glyph is filled, as M3 requires of a FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself: put the page's FAB in ``'s `fab` slot, which places it at the bottom-end corner with M3's margin and clear of the navigation bar and a snackbar. `link`, `external`, `type`. There is no `disabled`: M3 says to remove a FAB whose action is unavailable, so hide it instead. `data-fab` on the root lets a place restyle a nested FAB (a rail flattens it to elevation 0). +`` — `size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. The glyph is filled, as M3 requires of a FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself: put the page's FAB in ``'s `fab` slot, which places it at the bottom-end corner with M3's margin and clear of the navigation bar and a snackbar. `link`, `external`, `type`. There is no `disabled`: M3 says to remove a FAB whose action is unavailable, so hide it instead. `data-md-fab` on the root lets a place restyle a nested FAB (a rail flattens it to elevation 0). `collapse-on-scroll` on an extended FAB with an `icon` (``) is M3's scroll behaviour: it shrinks to the FAB of its size while the window scrolls down and extends again on scroll-up or at the top of the page. The width morphs on the spatial spring and the label fades; under reduced motion it swaps outright. The label stays in the page, clipped, so the collapsed FAB keeps its accessible name. It watches the window, so it is for a FAB pinned over a scrolling page, not one inside a scrolling pane. ### ``, `` ```blade -
+
@@ -360,15 +383,28 @@ Attributes go to the leading button; the slot is the menu. `variant` (`filled` d
``` -Two to six items open above the FAB, which turns into a close button, rising into place as it opens and sinking back as it closes; a window too short for them scrolls the list while the FAB stays put. ``: `icon` (`add`), `label` (defaults to "Toggle menu" — the trigger has no other accessible name), `color`, `position` (`top-end` default). Give items the same `color`. Keyboard, and staying open through a Livewire render, as ``. The wrapper keeps M3's margin from the window edge: 16dp, 24dp from `large`. +```css +.new-share-menu { + position: fixed; + inset-inline-end: var(--md-sys-measurement-space200); + inset-block-end: var(--md-sys-measurement-space200); + + @media (width >= 1200px) { + inset-inline-end: var(--md-sys-measurement-space300); + inset-block-end: var(--md-sys-measurement-space300); + } +} +``` + +Two to six items open above the FAB, which turns into a close button, rising into place as it opens and sinking back as it closes; a window too short for them scrolls the list while the FAB stays put. ``: `icon` (`add`), `label` (defaults to "Toggle menu" — the trigger has no other accessible name), `color`, `position` (`top-end` default). Give items the same `color`. Keyboard, and staying open through a Livewire render, as ``. It does not place itself: the wrapper keeps M3's margin from the window edge, 16dp, 24dp from `large`. ### `` -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. +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: `size` is its size in px, a whole number from 24 to 240 — M3's responsive range, which it asks you never to leave; scale it up on large and extra-large windows — and the container and the shape keep their ratio at every size (outside the range it is ignored, and your own CSS may size it). Colour it with your own CSS. `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 - +
``` @@ -398,7 +434,7 @@ window.addEventListener('app:update', () => location.reload()) ### `` -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. +M3 Expressive's progress indicator: linear (as wide as its container) or `circular` (40px, 48px wavy, unless your own CSS sizes it), flat or `wavy`, determinate with a `value` or indeterminate without one. | Prop | Default | | |---|---|---| @@ -421,15 +457,15 @@ M3 Expressive's progress indicator: linear (as wide as its container) or `circul
``` -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. +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 width of your own on its `class` narrows a linear one (`inline-size: 12rem`); leave its `display` and `position` to it. ### `` -- `` — M3's small badge, a dot. `` — M3's large badge, a count. Both `error` by default. `floating` pins it to the top-end corner of a `relative` parent: ``. A dot or count is `aria-hidden` unless it has a `label`; name the control instead ("Messages, 4 unread"). +- `` — M3's small badge, a dot. `` — M3's large badge, a count. Both `error` by default. `floating` pins it to the top-end corner of a `position: relative` parent: ``. A dot or count is `aria-hidden` unless it has a `label`; name the control instead ("Messages, 4 unread"). - ``, ``, ``, `` — a status label (not an M3 badge) in the colour's container, in the colour itself (`solid`, for a label that has to stand out), or a neutral `outline` edge (the role that has to be seen, not the decorative `outline-variant` dividers use). `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`, `neutral`, `plain`; an unknown colour is `error`. - `color="neutral"` — neutral ink on every variant: a dot or count in on-surface-variant with surface text, `tonal` in surface-container-high with on-surface-variant text, `outline` in the outline edge with on-surface-variant text. -- `color="plain"` — no background, text or border colour in any variant (shape, size and type stay), so the classes you pass paint it: ``. Pass both a background and a text class; an `outline` badge's edge takes the text colour unless you pass a `border-*` colour. -- The value is `value` or the slot; the slot renders as HTML: ` Pro`. `value` is escaped. A slot that holds only whitespace or comments is still a dot. +- `color="plain"` — no background, text or border colour in any variant (shape, size and type stay), so your own CSS paints it: `` with `.sport-run-label { background-color: var(--md-sys-color-tertiary-container); color: var(--md-sys-color-on-tertiary-container); }`. Set both a background and a colour; an `outline` badge's edge takes the text colour unless you set a `border-color`. +- The value is `value` or the slot; the slot renders as HTML: ` Pro`. `value` is escaped. A slot that holds only whitespace or comments is still a dot. ### `` @@ -459,30 +495,30 @@ Shows on hover and keyboard focus and goes 1.5s after the pointer or the focus l ### `` -`` — 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 ``). Do not pass a `bg-*` class; wrap it. +`` — 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 ``). Its fill is surface-container: do not repaint it with a class of your own; wrap it. ### `` "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. -The `illustration` slot draws the application's own artwork in place of the shape and icon (`icon` and `shape` are then unused). Size the artwork yourself; the slot's attributes go on the element around it, so its `class` sets the colour `currentColor` takes. Mark decorative SVG `aria-hidden="true"`. A slot holding only whitespace or comments leaves the shape and icon. +The `illustration` slot draws the application's own artwork in place of the shape and icon (`icon` and `shape` are then unused). Size the artwork yourself; the slot's attributes go on the element around it, so its `style` (or a class of your own) sets the colour `currentColor` takes. Mark decorative SVG `aria-hidden="true"`. A slot holding only whitespace or comments leaves the shape and icon. ```blade - + ``` ### `` -`variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`, `heading` (the title's element, `h3` by default: pass `h2` for a card straight under the page's `h1`); slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). A card holds content and actions about one subject (M3); a table of many rows or a group of settings is a headed section, not a card. Do not pass `bg-*`; use `variant`. +`variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`, `heading` (the title's element, `h3` by default: pass `h2` for a card straight under the page's `h1`); slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). A card holds content and actions about one subject (M3); a table of many rows or a group of settings is a headed section, not a card. Choose its fill with `variant`, not a background of your own. A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-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 `` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. ```blade - {{ $share->name }} + {{ $share->name }} ``` @@ -545,7 +581,7 @@ Props: `title`, `subtitle`, `icon` (centred hero icon), `separator` (draw the di ### `` -An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`; mirrored in RTL), `width` (`400px`), `with-close-button` (**default true** — M3 requires a close affordance; `:with-close-button="false"` is ignored when Escape or the scrim is off, or on a `standard` sheet), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot (**left**-aligned in a 72dp row, which is what the side-sheet spec says; a dialog's are trailing-aligned). For the second pane of a list-detail layout use `` instead — `` no longer has a `pane` prop. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class. +An M3 side sheet, bound like ``; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`; mirrored in RTL), `width` (`400px`), `with-close-button` (**default true** — M3 requires a close affordance; `:with-close-button="false"` is ignored when Escape or the scrim is off, or on a `standard` sheet), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot (**left**-aligned in a 72dp row, which is what the side-sheet spec says; a dialog's are trailing-aligned). For the second pane of a list-detail layout use `` instead — `` no longer has a `pane` prop. Its body is a size container — lay out inside it with a container query in your own CSS (`@container (width >= 28rem)`), never a window size class. `standard` is M3's other side-sheet variant: supplementary content beside the primary content — filters, details, a list of actions — co-planar from `expanded`, flat on `surface` with 0dp elevation and no corner, the window's full height, an outline-variant rule down its inner edge instead of a scrim, nothing inert and no focus trap. Below `expanded` it is the modal sheet. Capped at M3's 400dp whatever `width` says, and it always draws the close button. Render it beside its content in a row that only lays out side by side from `expanded` (``, or a caller's own row). @@ -567,7 +603,7 @@ An M3 bottom sheet, bound like ``: modal by default (scrim, inert page, ``` -A row of items that change size between M3's keylines as it scrolls (native scroll snap; items are masked, content keeps its size). ``: `layout` (`multi-browse` default, `hero`, `uncontained`, `multi-aspect`, `full-screen` — one edge-to-edge item at a time scrolled **vertically**, which M3 gives to compact and medium windows in portrait only, and never to landscape), `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, **16** — M3's specs table; leading only for `uncontained`, none for `full-screen`), `centered` (hero), `label` (the region's name, "Carousel" by default), `controls` (previous/next buttons: default fine pointers only, `true` always, `false` never). ``: slot is an `` (fills and crops) or an element sized `size-full`; `label` overlays a line of text; `aspect` is its ratio in a `multi-aspect` carousel. A `region` of `slide` groups named "n of m", each item a tab stop and the row itself not one, as M3 asks; from a focused item the arrow keys move one item, Home/End go to the ends and Space/Enter opens one that is not fully in view. Works after a Livewire morph, in RTL and under reduced motion. Give items a `wire:key` in a loop. +A row of items that change size between M3's keylines as it scrolls (native scroll snap; items are masked, content keeps its size). ``: `layout` (`multi-browse` default, `hero`, `uncontained`, `multi-aspect`, `full-screen` — one edge-to-edge item at a time scrolled **vertically**, which M3 gives to compact and medium windows in portrait only, and never to landscape), `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, **16** — M3's specs table; leading only for `uncontained`, none for `full-screen`), `centered` (hero), `label` (the region's name, "Carousel" by default), `controls` (previous/next buttons: default fine pointers only, `true` always, `false` never). ``: slot is an `` (fills and crops) or an element your own CSS sizes to fill it (`inline-size: 100%; block-size: 100%`); `label` overlays a line of text; `aspect` is its ratio in a `multi-aspect` carousel. A `region` of `slide` groups named "n of m", each item a tab stop and the row itself not one, as M3 asks; from a focused item the arrow keys move one item, Home/End go to the ends and Space/Enter opens one that is not fully in view. Works after a Livewire morph, in RTL and under reduced motion. Give items a `wire:key` in a loop. `layout="multi-aspect"` is M3's uncontained multi-aspect-ratio layout (November 2025): each `` keeps its own ratio at the row's `height`, held inside M3's 9:16-to-16:9 range, so the widths come from the art. Only use it when the items really do have various widths. It is a plain flex row with uncontained scrolling — no keylines and no masks, since an arrangement of one item size cannot describe it — while the buttons, the arrow keys, Home/End and bring-into-view still work, from resting positions measured off the DOM. @@ -626,12 +662,12 @@ A one-column grid of fields with an `actions` slot at the foot, end-aligned (the ### ``, ``, ``, ``, ``, `` -M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant` (and all but `` a `hint-class`, classes added to the hint: `hint-class="text-warning"`), and read their errors from the bag under the `wire:model` name, or the `name` in a plain form (`photos[]` → `photos`, `address[city]` → `address.city`); the error replaces the hint, sets `aria-invalid`, and puts an `error` icon at the end of the row as M3's second indicator (not on `size="xs"`, and not when the field already trails something — `icon-right`, `clearable`, `copyable`). `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. +M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire-material.fields.variant')` (`outlined`). All take `label`, `hint`, `variant` (and all but `` a `hint-class`, classes added to the hint: `hint-class="md-ink-warning"`), and read their errors from the bag under the `wire:model` name, or the `name` in a plain form (`photos[]` → `photos`, `address[city]` → `address.city`); the error replaces the hint, sets `aria-invalid`, and puts an `error` icon at the end of the row as M3's second indicator (not on `size="xs"`, and not when the field already trails something — `icon-right`, `clearable`, `copyable`). `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. - ``: `icon`, `icon-right`, `prefix`, `suffix`, `clearable`, `copyable` (copies the value, confirms with a snackbar), `counter`, `size` (`sm` 40px, `xs` 32px — for unlabelled toolbar controls; give them `aria-label`), `mono`. - ``: a reveal button; `icon`, `size`. - ``: grows from `rows` (3) to `max-rows`, then scrolls; `:autogrow="false"` for a fixed, hand-resizable one; `counter`. -- Width: M3 asks that a text field never span the full width of a large screen, so from `medium` (600px) every field stops at **40rem**; below that it fills its pane. A `max-w-*` class on the component narrows or widens it, and `full` (on ``, ``, ``) takes the bound off for a field that really is the width of its pane — a search row, an editor. ``'s bar carries M3's own bound, 720px. +- Width: M3 asks that a text field never span the full width of a large screen, so from `medium` (600px) every field stops at **40rem**; below that it fills its pane. A width rule of your own on its `class` (`max-inline-size: 24rem`) narrows or widens it, because your unlayered CSS outranks the package's, and `full` (on ``, ``, ``) takes the bound off for a field that really is the width of its pane — a search row, an editor. ``'s bar carries M3's own bound, 720px. - `counter` (on `` and ``) puts M3's character counter at the end of the supporting-text row, beside the hint or the error: `n/max`, counted on every keystroke against the field's own `maxlength`, and in the error colour once the value is past it. It needs `maxlength` — without one there is nothing to count against and nothing is drawn. It is said as "Character count, 5/20" from a polite region a second after typing stops. - ``: native ``s (one per handle), so th | `orientation` | `horizontal` | `vertical` stands it up: the value grows upwards, the value label sits beside the handle, Up and Down move it. Ignored with `range` — M3 keeps range sliders horizontal | | `disabled` | `false` | | -Other attributes go to the input(s). A `wire:model.live` slider sends while it is dragged, and a server render never moves a handle under the pointer (the drawing is `wire:ignore`); a value the server sets moves the handle after the morph. The binding gets `.number`, so values arrive as numbers. Its width is the container's unless a `w-*` class is passed. +Other attributes go to the input(s). A `wire:model.live` slider sends while it is dragged, and a server render never moves a handle under the pointer (the drawing is `wire:ignore`); a value the server sets moves the handle after the morph. The binding gets `.number`, so values arrive as numbers. Its width is the container's unless your own CSS narrows it. -A vertical slider is as wide as a horizontal one is tall and as long as the wrapper it is in, so **give it a height** — `class="h-64"`, which the label and hint share. Without one it is 192px long. +A vertical slider is as wide as a horizontal one is tall and as long as the wrapper it is in, so **give it a height** — a `block-size` rule of your own on its `class`, which the label and hint share. Without one it is 192px long. ```blade - + ``` ### `` @@ -758,7 +794,7 @@ M3 search bar that opens into a search view: docked under the bar from `medium` ``` -The docked view overlaps what is under it; never place a search inside an element with `overflow-hidden` (a card), which clips it. The bar is never wider than M3's 720px and grows to that width while it is focused; for M3's 360px resting bar, wrap it in an element carrying `style="--search-width: 22.5rem"`. +The docked view overlaps what is under it; never place a search inside an element with `overflow: hidden` (a card), which clips it. The bar is never wider than M3's 720px and grows to that width while it is focused; for M3's 360px resting bar, wrap it in an element carrying `style="--search-width: 22.5rem"`. - `trigger="icon"` is M3's other entry point — search as a secondary action: one 48px search icon button that expands into the full-screen view at any width (so `docked` does not apply) and gives the button its focus back on close. Put it in a toolbar or an app bar row where a bar would not fit. - The `suggestions` slot is shown in the view until the first keystroke — recent or popular searches — and the results slot takes over once something is typed. The live region counts whichever list is on screen and names suggestions as such. @@ -808,13 +844,13 @@ A visitor who has pressed the menu button keeps that choice in both standard ban ['title' => 'Upload', 'icon' => 'upload', 'url' => route('upload')], ['title' => 'Users', 'icon' => 'group', 'url' => route('users'), 'section' => 'Admin', 'bar' => false], ]"> - SealShare + SealShare {{-- the page's app bar; its menu button opens the modal rail on a phone --}} - + @@ -829,9 +865,9 @@ A visitor who has pressed the menu button keeps that choice in both standard ban - `fab` places an `` as Compose's Scaffold does: fixed at the bottom-end corner, 16px from the window's edges on a compact window and 24px from `medium`, above the navigation bar and the bottom safe area, and lifted above a snackbar while one shows (M3: a snackbar appears above a FAB, never in front of or behind it). In focus order it comes after the page's bar and before the page. Use it or a FAB in `rail-header`, never both: M3 allows one FAB on a screen. - `banner` or `top`: M3's scaffold is bars, then rails, then panes. An application-wide bar — one search, one account menu, the same on every page — goes in `banner` and the rail starts under it; a bar that titles the page goes in `top`, beside the rail. Never both. A banner that pins itself to the top of the window says how tall it is (`style="--material-banner: 4rem"` on ``), so the rail sticks under it instead of behind it. - The rail is one element at every width: what is in it is also what a phone sees in the modal rail. On a compact window nothing opens it but `$store.rail.show()`, so a page whose destinations are not all in the bar needs a menu button in its app bar (hidden from `medium`). -- `--material-margin` is M3's window margin (16px compact, 24px from `medium`) and the content region already carries it, so a page inside the scaffold writes no gutters of its own, and a pane or canonical layout inside draws none; something that must reach the window's edges opts out with `-mx-(--material-margin)`. +- `--material-margin` is M3's window margin (16px compact, 24px from `medium`) and the content region already carries it, so a page inside the scaffold writes no gutters of its own, and a pane or canonical layout inside draws none; something that must reach the window's edges opts out with `margin-inline: calc(var(--material-margin) * -1)` in your own CSS. - On a compact window the scaffold sets `--material-bottom-bar` (the bar, the bottom safe area and `--material-bottom-extra`), so the snackbar, the FAB and the page's bottom padding clear the bar; pad anything else you pin to the bottom with it. See Safe areas. -- The content region is `max-expanded:overflow-x-clip`. Never make a page wrapper `overflow-x-hidden`: it turns the region into a scroll container and breaks every `sticky` inside. +- Below `expanded` the content region clips what overflows sideways (`overflow-x: clip`). Never give a page wrapper `overflow-x: hidden`: it turns the region into a scroll container and breaks every `position: sticky` inside. #### `` @@ -950,15 +986,24 @@ Children in columns inside a pane. ### ``, `` -M3 Expressive's flexible navigation bar, for three to five destinations. It does not position itself; wrap it (`` does): +M3 Expressive's flexible navigation bar, for three to five destinations. It does not position itself; pin it in a wrapper and hide it where a rail takes over (`` does both): ```blade -
+ -
+
+``` + +```css +.bottom-navigation { + position: fixed; + inset-inline: 0; + inset-block-end: 0; + z-index: 30; +} ``` 64px in surface-container with the bottom safe area under it. Narrower than 600px the icon sits in a 56×32 indicator over the label; from 600px (the bar's own width) icon and label share a 40px pill and the items gather in the middle. ``: `label` ("Main"), `tall` (M3's 80px container, which keeps the icon over the label at every width — `` picks it, and the bottom offset grows with it), `hide-on-scroll` (M3's scrolling behaviour: the bar slides out on a scroll down and springs back on a scroll up, never before the first screenful and never while a snackbar, bottom sheet or drawer is on screen; focus reaching it brings it back. `` picks it, and `--material-bottom-bar` goes down and comes back with the bar, so a `fab` button and the snackbar keep their distance from it). ``: `label` / slot, `icon`, `link` (with `wire:navigate` unless `external` or `no-wire-navigate`; without a link it is a button), `active` (`aria-current="page"`, filled icon, secondary-container indicator), `badge` (`true` for a dot, a number for a count, 999+ at most), `badge-label` (what a screen reader hears instead of ", 3"). @@ -968,9 +1013,9 @@ M3 Expressive's flexible navigation bar, for three to five destinations. It does M3 Expressive's navigation rail: collapsed (96px, icon over label) or expanded (a 56px full-width pill, icon beside label, count at the end). ```blade -
+ - SealShare + SealShare @@ -983,14 +1028,23 @@ M3 Expressive's navigation rail: collapsed (96px, icon over label) or expanded ( -
-
+
+
``` - `mode`: `collapsed`, `expanded`, `collapsible` (default: expanded until its menu button collapses it; the choice is `$store.rail`, remembered and applied before the first paint), `modal` (collapsed in the layout; the menu button or `$store.rail.show()` opens it expanded over a scrim, focus held until Escape, the scrim or leaving the page), `adaptive` (``'s, one rail per window size class: hidden and opened as a modal on a compact window, collapsed and opened as a modal at `medium`, a standard rail from `expanded` — collapsed there, expanded from `large`). - Props: `label` ("Main"), `width` (expanded width, `16rem`, held between 220 and 360px — or the word `narrow` for M3's other *collapsed* width, 80px against the default 96, where the items are their icons alone; the labels stay in the accessibility tree and a narrow rail still expands to 16rem), `align` (`top` default, or `center` for M3's centred destinations — preferred on a tablet; the menu button, brand and FAB stay at the top and the footer at the foot), `hide-when-collapsed` (M3's immersive expanded behaviour, `collapsible` and `adaptive` only: collapsing the rail takes it out of the layout instead of narrowing it, and `$store.rail.show()` brings it back expanded over a scrim — so put a menu button in the app bar; the rail's own button then docks it again. Not below `medium` for a collapsible rail nor at `medium` for an adaptive one, where the window rather than the visitor collapses it and M3's collapsed rail may never hide), `menu` (the menu button; on by default for `collapsible`, `modal`, `adaptive`), `divider` (M3's optional vertical divider on the page's side — use it when the page scrolls under a fixed rail), `fill` (`false` for a transparent container, which M3 allows while the items keep 3:1 contrast). Slots: `brand` (beside the menu button, expanded only), `header` (one ``, which the rail morphs into an extended FAB and back as it expands — it also rests at elevation 0, as M3 asks of a nested FAB), the destinations (the only part that scrolls), `footer`. In a flex row the rail sticks to the top of the viewport. -- Anything else inside a rail can take both shapes by matching the rail's own state directly — `data-md-navigation-rail`'s value, `:not([data-md-open])`, and the window band each mode collapses in, the same selectors `resources/css/components/navigation-rail.css` uses for every branch of "collapsed" (that file, not this one, is where the numbers live). The Tailwind `rail-collapsed:` variant this used to teach no longer exists: the package's own CSS is plain now. Nothing that shows while collapsed may be wider than 96px. -- A `collapsible` rail is held to the collapsed 96px below `medium` (600px), where M3 says to use a navigation bar rather than a standard rail. `collapsed` and `expanded` are fixed-width by design: wrap one in a `medium:` element if it must not show on a phone. +- Anything else inside a rail takes both shapes from the rail's value. The rail publishes M3's two values (Compose's `WideNavigationRailValue`) as `--md-navigation-rail-value`, `collapsed` or `expanded`, from the first paint and at the same moment as its own items change shape, whatever made it so (mode, window size class, the visitor's choice); a rail open over a scrim reads `expanded`. Every descendant inherits it, so your own CSS asks a style query rather than repeating the rail's conditions. Outside a rail it is unset and neither value matches. Style queries on a custom property need Chrome 111, Safari 18 or Firefox 151. Nothing that shows while collapsed may be wider than 96px. + +```css +@container style(--md-navigation-rail-value: collapsed) { + .account-summary { + display: none; + } +} +``` + +- A `collapsible` rail is held to the collapsed 96px below `medium` (600px), where M3 says to use a navigation bar rather than a standard rail. `collapsed` and `expanded` are fixed-width by design: wrap one in `` if it must not show on a phone. - ``: the same props as ``. ``: a group with a heading that shows only while the rail is expanded; it names the group for screen readers either way. - `$store.rail`: `collapsed`, `toggle()`, `collapse()`, `expand()` (the remembered choice; `auto` is true while nothing is stored, so an adaptive rail takes its window size class's default instead, and the first choice clears it), `open`, `show()`, `hide()` (the modal rail; closed on every `wire:navigate`). `config/livewire-material.php` → `rail.default` (`expanded` or `collapsed`) and `rail.storage_key` (`material-rail`). @@ -1015,7 +1069,7 @@ M3 Expressive top app bar, sticky by default (`:sticky="false"` to scroll away), ]" /> ``` -A collapsing bar needs the window to scroll: no ancestor with `overflow-hidden`/`overflow-auto` (`overflow-x-clip` is fine). +A collapsing bar needs the window to scroll: no ancestor with `overflow: hidden` or `overflow: auto` (`overflow-x: clip` is fine). ### `` @@ -1087,19 +1141,19 @@ A choice of colour profile (see Colour profiles): a swatch per generated profile ### ``, `` -A data table: write plain ``, ``, ``, `` inside ``; cell utilities (`text-end`, `whitespace-nowrap`) always win. Rows are 52px — a target a finger can hit. `dense` tightens them to 36px and `size="xs"` is for a table inside a panel inside a panel (32px rows); M3 says density is always an opt-in, so neither is a default and both are yours to justify. Scrolling is yours: wrap it in `
`. A row that opens something is `data-md-list-row` with one `data-md-list-open` control; a selected row is `aria-selected="true"`. +A data table: write plain ``, ``, ``, `` inside ``; the text classes on a cell (`md-text-end`, `md-nowrap`, `md-tabular`) always win over the table's own alignment. Rows are 52px — a target a finger can hit. `dense` tightens them to 36px and `size="xs"` is for a table inside a panel inside a panel (32px rows); M3 says density is always an opt-in, so neither is a default and both are yours to justify. Scrolling is yours: wrap it in an element that scrolls sideways (`overflow-x: auto` in your own CSS). A row that opens something is `data-md-list-row` with one `data-md-list-open` control; a selected row is `aria-selected="true"`. `Size` sorts through the Livewire property `sortBy` (`['column' => …, 'direction' => 'asc'|'desc']`; `model` names another), with `aria-sort`. ```blade -
+
- NameSize + NameSize @foreach ($shares as $share) {{ $share->name }} - {{ $share->size }} + {{ $share->size }} @endforeach @@ -1157,12 +1211,12 @@ In the `.css` files it is given (`material-scheme.css` skipped) it fails on a li ## Conventions - 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. +- Write class names out whole: the design guard reads a line at a time and cannot see `'md-ink-'.$tone` or `md-type-{{ $size }}`, so a class built at runtime goes unchecked. Where the choice is data, put it in a `data-*` attribute your own CSS keys on. - 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: `` reaches the browser as literal text. On a component tag use `{{ }}` and `:prop` bindings — `@js($value)` is `{{ \Illuminate\Support\Js::from($value) }}`, `@class([...])` is `:class="\Illuminate\Support\Arr::toCssClasses([...])"` — or put the Alpine on a plain element inside the slot. `DesignGuard` names the form for each directive. -- 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-medium:hidden`) is safe. +- Blade directives do not compile inside a component tag's attributes: `x-show="ok(@js($value))"` on a component tag reaches the browser as literal text. On a component tag use `{{ }}` and `:prop` bindings — `@js($value)` is `{{ \Illuminate\Support\Js::from($value) }}`, `@class([...])` is `:class="\Illuminate\Support\Arr::toCssClasses([...])"` — or put the Alpine on a plain element inside the slot. `DesignGuard` names the form for each directive. +- To hide a component, give it the `hidden` attribute (it wins over any `display`, foundation/hidden.css), `x-show`, or a layout component's `hide-below`/`hide-from` around it. A `display` or `position` of your own on a component's `class` wins too — your unlayered CSS outranks every package rule — but it replaces the one the component draws itself with, so put it on a wrapper instead. - `$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. diff --git a/.claude/skills/material-3-design/SKILL.md b/.claude/skills/material-3-design/SKILL.md index e0f7345..499539e 100644 --- a/.claude/skills/material-3-design/SKILL.md +++ b/.claude/skills/material-3-design/SKILL.md @@ -1,6 +1,6 @@ --- name: material-3-design -description: Material 3 Expressive's design system as Livewire Material implements it — colour roles and surface containers, elevation, shape, type, motion, states and targets, window size classes, spacing, icons, accessibility — each M3 name beside the utility that draws it and Google's source page, for deciding how a screen should look and behave before writing it. +description: Material 3 Expressive's design system as Livewire Material implements it — colour roles and surface containers, elevation, shape, type, motion, states and targets, window size classes, spacing, icons, accessibility — each M3 name beside the class, prop or token that draws it and Google's source page, for deciding how a screen should look and behave before writing it. --- # Material 3 design @@ -9,57 +9,59 @@ description: Material 3 Expressive's design system as Livewire Material implemen Use this skill when deciding how a screen, panel or control should look or behave — which colour, container, corner, type style, motion, breakpoint or spacing — in an application that requires `nonameweb/livewire-material`, and when reviewing a view against Material 3. The props and slots of each component are in the `livewire-material-development` skill; this one is the design language they implement. The rules an agent must always follow are in the `material-3` guideline; the tables here are what those rules compress. -Every table pairs the M3 name with the library's utility or attribute. The tokens are CSS variables (`--md-sys-color-*`, `--md-sys-typescale-*`, `--md-sys-shape-*`, `--md-sys-elevation-*`, `--md-sys-motion-*`, `--md-sys-state-*`, `--md-sys-measurement-*`, `--md-ref-typeface-*`) and the utilities read them, so a template names a utility and never a variable or a value. +Every table pairs the M3 name with what the library gives for it. The library is plain CSS with no utility classes, so that is one of three things: a component or layout component prop (`color="error"`, ``, `gap="space200"`), one of the fixed text and interaction classes (`md-type-*`, `md-ink-*`, `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link`), or a token the application's own CSS reads with `var()`. The tokens are CSS custom properties (`--md-sys-color-*`, `--md-sys-typescale-*`, `--md-sys-shape-*`, `--md-sys-elevation-*`, `--md-sys-motion-*`, `--md-sys-state-*`, `--md-sys-measurement-*`, `--md-ref-typeface-*`), so a stylesheet names a token and never a value. ## Colour -A colour scheme is generated from one seed by Google's colour science (`php artisan material:scheme`); every role below is a slot in that scheme, light and dark, at three contrast levels. A template names a role and nothing else — never a hex, a palette tone, an arbitrary value or an opacity — because only a role follows the theme, the contrast level and a colour profile. +A colour scheme is generated from one seed by Google's colour science (`php artisan material:scheme`); every role below is a slot in that scheme, light and dark, at three contrast levels. A view names a role and nothing else — never a hex, a palette tone or an opacity — because only a role follows the theme, the contrast level and a colour profile. ### Roles -| Role | Purpose | Its `on-` pair | Utility | +| Role | Purpose | Its `on-` pair | In this library | | --- | --- | --- | --- | -| primary | High-emphasis fills, text and icons: the key action on a screen | on-primary | `bg-primary text-on-primary`, `text-primary` | -| primary-container | A standout fill for key components (FAB, an emphasised panel) | on-primary-container | `bg-primary-container text-on-primary-container` | -| primary-dim | A darker primary for a pressed or contrasting fill (2025 spec) | on-primary | `bg-primary-dim` | -| secondary | Less prominent fills, text and icons | on-secondary | `bg-secondary text-on-secondary` | -| secondary-container | The recessive fill: tonal buttons, selected navigation, selected chips | on-secondary-container | `bg-secondary-container text-on-secondary-container` | -| tertiary | A complementary accent, used sparingly for contrast | on-tertiary | `bg-tertiary text-on-tertiary` | -| tertiary-container | The complementary fill | on-tertiary-container | `bg-tertiary-container text-on-tertiary-container` | -| error | Urgency and errors; static, does not follow dynamic colour | on-error | `text-error`, `bg-error text-on-error` | -| error-container | An error panel | on-error-container | `bg-error-container text-on-error-container` | -| success, warning, info | This library's custom state colours, built like error on the 2025 spec, with `-container` and `on-` pairs | on-success … | `text-success`, `bg-warning-container text-on-warning-container` | -| surface | The page background | on-surface | `bg-surface text-on-surface` | -| on-surface-variant | Lower-emphasis text and icons on any surface | — | `text-on-surface-variant`, `text-body`, `text-meta` | -| outline | A boundary that must be read: a text field, a target's edge (3:1 against surface) | — | `border-outline`, `text-outline`, `text-quiet` | -| outline-variant | Decorative lines: dividers, card edges | — | `border-outline-variant`, `border-divider`, `border-structure`, `border-chrome`, `divide-divider` | -| inverse-surface | A surface that contrasts with its surroundings (the snackbar) | inverse-on-surface | `bg-inverse-surface text-inverse-on-surface` | -| inverse-primary | An action on an inverse surface (the snackbar's action) | — | `text-inverse-primary` | -| scrim | Behind a modal, at 32% | — | `bg-scrim/32` | -| shadow | The shadow colour, inside `shadow-elevation-*` | — | — | -| surface-dim, surface-bright | Add-on surfaces that keep their relative brightness in both themes | on-surface | `bg-surface-dim`, `bg-surface-bright` | -| primary-fixed, primary-fixed-dim, on-primary-fixed, on-primary-fixed-variant (and secondary, tertiary) | Add-on roles with the same tone in light and dark; for a colour that must not change with the theme; never where contrast matters | — | `bg-primary-fixed text-on-primary-fixed` | +| primary | High-emphasis fills, text and icons: the key action on a screen | on-primary | `var(--md-sys-color-primary)`, `md-ink-primary`, `` | +| primary-container | A standout fill for key components (FAB, an emphasised panel) | on-primary-container | `var(--md-sys-color-primary-container)` | +| primary-dim | A darker primary for a pressed or contrasting fill (2025 spec) | on-primary | `var(--md-sys-color-primary-dim)` | +| secondary | Less prominent fills, text and icons | on-secondary | `var(--md-sys-color-secondary)` | +| secondary-container | The recessive fill: tonal buttons, selected navigation, selected chips | on-secondary-container | `var(--md-sys-color-secondary-container)`, `` | +| tertiary | A complementary accent, used sparingly for contrast | on-tertiary | `var(--md-sys-color-tertiary)`, `color="tertiary"` | +| tertiary-container | The complementary fill | on-tertiary-container | `var(--md-sys-color-tertiary-container)` | +| error | Urgency and errors; static, does not follow dynamic colour | on-error | `md-ink-error`, `var(--md-sys-color-error)`, `color="error"` | +| error-container | An error panel | on-error-container | `var(--md-sys-color-error-container)`, `` | +| success, warning, info | This library's custom state colours, built like error on the 2025 spec, with `-container` and `on-` pairs | on-success … | `md-ink-success`, `var(--md-sys-color-warning-container)`, `color="info"` | +| surface | The page background | on-surface | the page itself (the foundation paints it), `` | +| on-surface-variant | Lower-emphasis text and icons on any surface | — | `md-ink-variant` | +| outline | A boundary that must be read: a text field, a target's edge (3:1 against surface) | — | `md-ink-quiet`, `var(--md-sys-color-outline)` | +| outline-variant | Decorative lines: dividers, card edges | — | ``, ``, `var(--md-sys-color-outline-variant)` | +| inverse-surface | A surface that contrasts with its surroundings (the snackbar) | inverse-on-surface | `var(--md-sys-color-inverse-surface)` with `md-ink-inverse` | +| inverse-primary | An action on an inverse surface (the snackbar's action) | — | `var(--md-sys-color-inverse-primary)` | +| scrim | Behind a modal, at 32% | — | `color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent)` | +| shadow | The shadow colour, inside every `--md-sys-elevation-*` | — | — | +| surface-dim, surface-bright | Add-on surfaces that keep their relative brightness in both themes | on-surface | ``, `` | +| primary-fixed, primary-fixed-dim, on-primary-fixed, on-primary-fixed-variant (and secondary, tertiary) | Add-on roles with the same tone in light and dark; for a colour that must not change with the theme; never where contrast matters | — | `var(--md-sys-color-primary-fixed)` with `var(--md-sys-color-on-primary-fixed)` | -Pairing: a role's `on-` pair is the only combination whose contrast is guaranteed at every contrast level. `bg-primary text-on-primary` and `bg-secondary-container text-on-secondary-container` are right; `bg-primary-container text-on-surface` or `bg-secondary-container text-primary` are not, and break as the contrast level rises. Google: "Pair and layer color roles only as intended … Don't mix roles improperly." +Pairing: a role's `on-` pair is the only combination whose contrast is guaranteed at every contrast level. A `primary` fill under `on-primary` text and a `secondary-container` fill under `on-secondary-container` are right; `primary-container` under `on-surface`, or `secondary-container` under `primary`, are not, and break as the contrast level rises. A component sets its own pair; the application's CSS writes both halves (`background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary)`). Google: "Pair and layer color roles only as intended … Don't mix roles improperly." ### Surface containers A hierarchy of emphasis, not of height: the tone separates panels before any shadow does, and a region keeps its role at every breakpoint (body always `surface`, navigation always `surface-container`). -| Role | Use | Utility | +| Role | Use | In this library | | --- | --- | --- | -| surface | The page | `bg-surface` | -| surface-container-lowest | The most recessed panel; an elevated card's body in dark themes | `bg-surface-container-lowest` | -| surface-container-low | An elevated card, a modal bottom or side sheet, the full-screen search view | `bg-surface-container-low` | -| surface-container | Navigation bar and rail, docked and floating toolbars, menus, the segmented list | `bg-surface-container` | -| surface-container-high | Dialogs, the search bar, date and time pickers, a rich tooltip | `bg-surface-container-high` | -| surface-container-highest | A filled card, a filled text field, a filled chip's selected state | `bg-surface-container-highest` | +| surface | The page | the page itself, `` | +| surface-container-lowest | The most recessed panel; an elevated card's body in dark themes | `` | +| surface-container-low | An elevated card, a modal bottom or side sheet, the full-screen search view | `` | +| surface-container | Navigation bar and rail, docked and floating toolbars, menus, the segmented list | `` (the default level) | +| surface-container-high | Dialogs, the search bar, date and time pickers, a rich tooltip | `` | +| surface-container-highest | A filled card, a filled text field, a filled chip's selected state | `` | + +In the application's CSS each is `var(--md-sys-color-surface-container-low)` and so on; the ink on every one of them is `on-surface`. ### Emphasis and lines -- Default ink is `on-surface`; lower emphasis is `on-surface-variant`; decoration is `outline`. Emphasis is never an opacity: M3 reserves 38% (`/38`) for disabled content and 12% (`/12`) for a disabled container. +- Default ink is `on-surface` (`md-ink`); lower emphasis is `on-surface-variant` (`md-ink-variant`); decoration is `outline` (`md-ink-quiet`). Emphasis is never an opacity: M3 reserves 38% (`--md-sys-state-disabled-content-opacity`) for disabled content and 12% (`--md-sys-state-disabled-container-opacity`) for a disabled container. - `outline` for a boundary that has to be perceived (a text field's edge, a target's edge — 3:1 against the surface); `outline-variant` for dividers and the edge of a card or any component holding several elements. Google: "Don't use the outline color for dividers … use outline variant instead." `outline-variant` may edge a chip or a button only because the content inside already carries the contrast. -- A hyperlink in running text is `primary` (or `tertiary` for a quieter link) **and** underlined: the `link` utility. +- A hyperlink in running text is `primary` (or `tertiary` for a quieter link) **and** underlined: `md-link` with `md-ink-primary`. ### Contrast @@ -69,7 +71,7 @@ A hierarchy of emphasis, not of height: the tone separates panels before any sha | Medium | 3:1 minimum everywhere, without halation | `` | | High | 7:1 | ``, or the visitor's OS setting (`theme.contrast.default` = `system`) | -Every role changes with the level automatically; a component built from roles needs nothing else. Disabled states are exempt from contrast. A colour outside the roles (a hex, `white`, `black`) does not change and is the one thing that breaks a contrast level. +Every role changes with the level automatically; a component built from roles needs nothing else. Disabled states are exempt from contrast. A colour outside the roles (a hex, white, black) does not change and is the one thing that breaks a contrast level. Sources: https://m3.material.io/styles/color/roles · https://m3.material.io/styles/color/system/how-the-system-works · https://m3.material.io/styles/color/advanced/apply-colors · https://m3.material.io/foundations/designing/color-contrast @@ -77,18 +79,18 @@ Sources: https://m3.material.io/styles/color/roles · https://m3.material.io/sty M3 separates surfaces by tone first; a shadow says that something floats over the content or is being interacted with. "When it comes to applying shadows, less is more." -| Level | Shadow | Rests here | Utility | +| Level | Shadow | Rests here | In this library | | --- | --- | --- | --- | | 0 | none | The page, cards (filled, outlined), buttons (filled, tonal, outlined), button groups, icon buttons, lists, chips, tabs, sliders, the rail, a docked side sheet, a carousel, a full-screen dialog, a FAB inside the rail, an app bar at rest | — | -| 1 | 1dp | Elevated cards, elevated buttons and chips, modal bottom and side sheets, a banner | `shadow-elevation-1` | -| 2 | 3dp | Menus, the navigation bar, a scrolled app bar, toolbars, rich tooltips | `shadow-elevation-2` | -| 3 | 6dp | FAB and extended FAB, the FAB menu's close button, dialogs, date and time pickers, the search bar | `shadow-elevation-3` | -| 4 | 8dp | Interaction only: a level-3 element on hover or while dragged | `shadow-elevation-4` | -| 5 | 12dp | Interaction only | `shadow-elevation-5` | +| 1 | 1dp | Elevated cards, elevated buttons and chips, modal bottom and side sheets, a banner | `box-shadow: var(--md-sys-elevation-1)` | +| 2 | 3dp | Menus, the navigation bar, a scrolled app bar, toolbars, rich tooltips | `var(--md-sys-elevation-2)` | +| 3 | 6dp | FAB and extended FAB, the FAB menu's close button, dialogs, date and time pickers, the search bar | `var(--md-sys-elevation-3)` | +| 4 | 8dp | Interaction only: a level-3 element on hover or while dragged | `var(--md-sys-elevation-4)` | +| 5 | 12dp | Interaction only | `var(--md-sys-elevation-5)` | - Hover lifts an element one level (a FAB 3 → 4, an elevated card 1 → 2); focus and selection may too; a raised element lowers when something higher appears. - Overlapping panels take different surface-container roles to show separation; the roles are not tied to the levels. -- A scrim (`bg-scrim/32`) brings focus to a modal over a large surface; it is never a substitute for a shadow on a small floating element. +- A scrim (`scrim` at 32%) brings focus to a modal over a large surface; it is never a substitute for a shadow on a small floating element. - On a dark surface a shadow is nearly invisible, so the tone does the work there. Sources: https://m3.material.io/styles/elevation/overview · https://m3.material.io/styles/elevation/applying-elevation · https://m3.material.io/styles/elevation/tokens @@ -97,20 +99,20 @@ Sources: https://m3.material.io/styles/elevation/overview · https://m3.material ### The corner scale -| Style | Value | Utility | +| Style | Value | In this library | | --- | --- | --- | -| None | 0 | `rounded-corner-none` | -| Extra small | 4px | `rounded-corner-xs` | -| Small | 8px | `rounded-corner-sm` | -| Medium | 12px | `rounded-corner-md` | -| Large | 16px | `rounded-corner-lg` | -| Large increased | 20px | `rounded-corner-lg-increased` | -| Extra large | 28px | `rounded-corner-xl` | -| Extra large increased | 32px | `rounded-corner-xl-increased` | -| Extra extra large | 48px | `rounded-corner-xxl` | -| Full | a stadium or circle | `rounded-corner-full` | +| None | 0 | `var(--md-sys-shape-corner-none)`, `corner="none"` | +| Extra small | 4px | `var(--md-sys-shape-corner-xs)`, `corner="xs"` | +| Small | 8px | `var(--md-sys-shape-corner-sm)`, `corner="sm"` | +| Medium | 12px | `var(--md-sys-shape-corner-md)`, `corner="md"` | +| Large | 16px | `var(--md-sys-shape-corner-lg)`, `corner="lg"` | +| Large increased | 20px | `var(--md-sys-shape-corner-lg-increased)`, `corner="lg-increased"` | +| Extra large | 28px | `var(--md-sys-shape-corner-xl)`, `corner="xl"` | +| Extra large increased | 32px | `var(--md-sys-shape-corner-xl-increased)`, `corner="xl-increased"` | +| Extra extra large | 48px | `var(--md-sys-shape-corner-xxl)`, `corner="xxl"` | +| Full | a stadium or circle | `var(--md-sys-shape-corner-full)`, `corner="full"` | -Tailwind's `rounded-sm`, `rounded-lg`, `rounded-full`, … do not compile. The `-t`, `-s`, `-e` sides work (`rounded-t-corner-xl` for a bottom sheet). +`corner` is ``'s prop. In the application's CSS a corner is `border-radius` on a token, and one side at a time a logical longhand (`border-start-start-radius` and `border-start-end-radius` for a bottom sheet's top); a length of your own is off the scale. ### Corner by component @@ -139,26 +141,26 @@ Tailwind's `rounded-sm`, `rounded-lg`, `rounded-full`, … do not compile. The ` - Optical roundness: a shape nested in a rounded container takes inner radius = outer radius − padding (48 − 14 = 34), never the container's own radius. - Large and full corners do not belong on information-dense containers (cards, tables, text fields). - A press squares a round shape and rounds a square one (the components carry the morph on the fast spatial spring); nothing morphs on hover. -- The 35 Expressive shapes (``, also the loading indicator and the standard button group's press shape) are decoration for emphasis and delight — never a carrier of meaning, never behind text-heavy content, and used sparingly. +- The 35 Expressive shapes (``, also the loading indicator and the standard button group's press shape) are decoration for emphasis and delight — never a carrier of meaning, never behind text-heavy content, and used sparingly. Sources: https://m3.material.io/styles/shape/corner-radius-scale · https://m3.material.io/styles/shape/shape-morph · https://m3.material.io/styles/shape/overview-principles ## Type -The typeface is Google Sans Flex for brand and plain styles (`font-sans`, `--md-ref-typeface-brand`); an application may replace it after importing the stylesheet. Each style is one utility that sets size, line height, weight, family and tracking together; `text-sm`, `font-medium`, `leading-*` and `tracking-*` do not compile. +The typeface is Google Sans Flex for brand and plain styles (`--md-ref-typeface-brand`, `--md-ref-typeface-plain`); an application may replace it after importing the stylesheet. Each style is one class that sets size, line height, weight, family and tracking together — or, in the application's CSS, `font: var(--md-sys-typescale-body-md)` with `letter-spacing: var(--md-sys-typescale-body-md-tracking)`. A size, weight, line height or letter spacing of your own is off the scale. -| Role | Style | Size / line | Weight | Utility | Use for | +| Role | Style | Size / line | Weight | In this library | Use for | | --- | --- | --- | --- | --- | --- | -| Display | large / medium / small | 57/64 · 45/52 · 36/44 | 400 | `type-display-lg` … | hero figures, one short marketing line; never running text | -| Headline | large / medium / small | 32/40 · 28/36 · 24/32 | 400 | `type-headline-lg` … | page titles, section titles, a dialog's headline (`headline-sm`) | -| Title | large / medium / small | 22/28 · 16/24 · 14/20 | 400 / 500 / 500 | `type-title-lg` … | app bar title (`lg`), card and list-section titles (`md`), dense headers (`sm`) | -| Body | large / medium / small | 16/24 · 14/20 · 12/16 | 400 | `type-body-lg` … | paragraphs (`lg` for reading, `md` in components), supporting text (`sm`) | -| Label | large / medium / small | 14/20 · 12/16 · 11/16 | 500 | `type-label-lg` … | buttons and tabs (`lg`), chips and navigation (`md`), captions and badges (`sm`) | +| Display | large / medium / small | 57/64 · 45/52 · 36/44 | 400 | `md-type-display-lg` … | hero figures, one short marketing line; never running text | +| Headline | large / medium / small | 32/40 · 28/36 · 24/32 | 400 | `md-type-headline-lg` … | page titles, section titles, a dialog's headline (`headline-sm`) | +| Title | large / medium / small | 22/28 · 16/24 · 14/20 | 400 / 500 / 500 | `md-type-title-lg` … | app bar title (`lg`), card and list-section titles (`md`), dense headers (`sm`) | +| Body | large / medium / small | 16/24 · 14/20 · 12/16 | 400 | `md-type-body-lg` … | paragraphs (`lg` for reading, `md` in components), supporting text (`sm`) | +| Label | large / medium / small | 14/20 · 12/16 · 11/16 | 500 | `md-type-label-lg` … | buttons and tabs (`lg`), chips and navigation (`md`), captions and badges (`sm`) | -- `type-emphasized-*` is the same size and line height one weight step heavier (400 → 500, 500 → 700), fully rounded in Google Sans Flex, with its own tracking. M3 uses it deliberately, never by default: a selected list or menu item, a button's label on a primary action, an extended FAB, a badge, a headline given editorial weight. +- `md-type-emphasized-*` (`--md-sys-typescale-emphasized-*`) is the same size and line height one weight step heavier (400 → 500, 500 → 700), fully rounded in Google Sans Flex, with its own tracking. M3 uses it deliberately, never by default: a selected list or menu item, a button's label on a primary action, an extended FAB, a badge, a headline given editorial weight. - Tracking follows Compose's `TypeScaleTokens`: display-large −0.2, title-medium 0.2, title-small 0.1, body-large 0.5, body-medium 0.2, body-small 0.4, label-large 0.1, label-medium and small 0.5 (sp; rem = sp/16); the emphasized set tightens a few (display-large 0, title-medium 0.15, body-large 0.15, body-medium 0.25). -- Line length 40–60 characters (`max-w-prose` is close; `max-w-[60ch]` is exact). Figures that change take `tabular-nums`. -- Text must scale to 200%: containers grow, side-by-side controls stack, padding stays; components without text (progress, checkboxes) do not scale. Truncate to an ellipsis only when the full text is one tooltip or link away. +- Line length 40–60 characters (`max-inline-size: 60ch` in the application's CSS). Figures that change take `md-tabular`. +- Text must scale to 200%: containers grow, side-by-side controls stack, padding stays; components without text (progress, checkboxes) do not scale. Truncate to an ellipsis (`md-truncate`) only when the full text is one tooltip or link away. - When customising, change the typeface or tracking, never the sizes: component layout depends on them. Sources: https://m3.material.io/styles/typography/type-scale-tokens · https://m3.material.io/styles/typography/applying-type · https://m3.material.io/styles/typography/fonts · https://m3.material.io/foundations/writing/text-resizing · https://m3.material.io/foundations/writing/text-truncation @@ -167,82 +169,84 @@ Sources: https://m3.material.io/styles/typography/type-scale-tokens · https://m M3 Expressive moves on physics: every transition is a spring, and the library samples each spring into a CSS `linear()` easing paired with a duration. Use the pair together, or the curve is stretched over the wrong time. -| Spring | Damping / stiffness | Duration | Utility | For | +| Spring | Damping / stiffness | Duration | In this library | For | | --- | --- | --- | --- | --- | -| Spatial fast | 0.6 / 800 | 350ms | `ease-spatial-fast duration-(--md-sys-motion-spatial-fast-duration)` | small elements: a button's press morph, a switch, a chip | -| Spatial default | 0.8 / 380 | 500ms | `ease-spatial-default duration-(--md-sys-motion-spatial-default-duration)` | most position, size and shape changes | -| Spatial slow | 0.8 / 200 | 650ms | `ease-spatial-slow duration-(--md-sys-motion-spatial-slow-duration)` | large surfaces: a sheet, a pane, a full-screen transition | -| Effects fast | 1.0 / 3800 | 150ms | `ease-effects-fast duration-(--md-sys-motion-effects-fast-duration)` | state layers, small fades | -| Effects default | 1.0 / 1600 | 200ms | `ease-effects-default duration-(--md-sys-motion-effects-default-duration)` | most colour and opacity changes | -| Effects slow | 1.0 / 800 | 300ms | `ease-effects-slow duration-(--md-sys-motion-effects-slow-duration)` | large fades, a scrim | +| Spatial fast | 0.6 / 800 | 350ms | `var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast)` | small elements: a button's press morph, a switch, a chip | +| Spatial default | 0.8 / 380 | 500ms | `var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default)` | most position, size and shape changes | +| Spatial slow | 0.8 / 200 | 650ms | `var(--md-sys-motion-spatial-slow-duration) var(--md-sys-motion-spatial-slow)` | large surfaces: a sheet, a pane, a full-screen transition | +| Effects fast | 1.0 / 3800 | 150ms | `var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast)` | state layers, small fades | +| Effects default | 1.0 / 1600 | 200ms | `var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default)` | most colour and opacity changes | +| Effects slow | 1.0 / 800 | 300ms | `var(--md-sys-motion-effects-slow-duration) var(--md-sys-motion-effects-slow)` | large fades, a scrim | -- Spatial springs are underdamped and overshoot — that bounce is what reads as Expressive — so they carry only position, size and shape. Effects springs are critically damped and carry colour and opacity, which must never overshoot. A `transition-all` mixes the two and is wrong. +A transition names the property, then the pair: `transition: transform var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default), opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast)`. + +- Spatial springs are underdamped and overshoot — that bounce is what reads as Expressive — so they carry only position, size and shape. Effects springs are critically damped and carry colour and opacity, which must never overshoot. A transition on `all` mixes the two and is wrong. - The Standard motion scheme (``, config `motion.scheme`) swaps the spatial springs for stiffer ones with almost no bounce (0.9 / 1400, 700, 300; 350, 500, 750ms) for utilitarian products; effects are shared. -- Direction: something entering decelerates (`ease-emphasized-decelerate`, or a spatial spring from off-screen), a permanent exit accelerates (`ease-emphasized-accelerate`), a temporary exit that can be recalled (a drawer, a sheet) takes `ease-emphasized`; exits are shorter than entrances, and larger areas move longer. -- The cubic-bezier set (`ease-standard`, `ease-emphasized`, `ease-emphasized-decelerate`, `ease-emphasized-accelerate`, `--md-sys-motion-duration-short|medium|long`) is for the few transitions whose duration is fixed from outside: a view transition, an animated scroll. -- Reduced motion zeroes every duration token, so anything animated through them turns instant; a literal `duration-300` or a keyframe with its own timing ignores the visitor's setting and is a bug. Container transforms, parallax and expansions are removed, not slowed. +- Direction: something entering decelerates (`--md-sys-motion-easing-emphasized-decelerate`, or a spatial spring from off-screen), a permanent exit accelerates (`--md-sys-motion-easing-emphasized-accelerate`), a temporary exit that can be recalled (a drawer, a sheet) takes `--md-sys-motion-easing-emphasized`; exits are shorter than entrances, and larger areas move longer. +- The cubic-bezier set (`--md-sys-motion-easing-standard`, `-emphasized`, `-emphasized-decelerate`, `-emphasized-accelerate`, with `--md-sys-motion-duration-short|medium|long`) is for the few transitions whose duration is fixed from outside: a view transition, an animated scroll. +- Reduced motion zeroes every duration token, so anything animated through them turns instant; a literal `300ms`, or a keyframe animation with its own timing, ignores the visitor's setting and is a bug. Container transforms, parallax and expansions are removed, not slowed. Sources: https://m3.material.io/styles/motion/overview · https://m3.material.io/styles/motion/overview/specs · https://m3.material.io/styles/motion/easing-and-duration/tokens-specs · https://m3.material.io/styles/motion/transitions/transition-patterns ## States and targets -| State | Layer | Utility or hook | Also | +| State | Layer | Class or hook | Also | | --- | --- | --- | --- | | Enabled | none | — | | -| Hover | 8% of the content colour | `state-layer` (pointer devices only) | one level of elevation on floating elements | -| Focused | 10% | `state-layer focus-ring` (keyboard focus: a 3px `secondary` ring, 2px out) | only one focused element at a time | -| Pressed | 10% | `state-layer` (`:active`) | the shape morph on buttons | -| Dragged | 16% | `state-layer` with `data-dragged` | one level of elevation | -| Disabled | content 38%, container 12%, no state layer, not focusable | `disabled:text-on-surface/38 disabled:bg-on-surface/12` | exempt from contrast; a FAB is hidden rather than disabled | +| Hover | 8% of the content colour | `md-state-layer` (pointer devices only) | one level of elevation on floating elements | +| Focused | 10% | `md-state-layer md-focus-ring` (keyboard focus: a 3px `secondary` ring, 2px out) | only one focused element at a time | +| Pressed | 10% | `md-state-layer` (`:active`) | the shape morph on buttons | +| Dragged | 16% | `md-state-layer` with `data-md-dragged` | one level of elevation | +| Disabled | content 38%, container 12%, no state layer, not focusable | `color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent)`, and the container likewise with `--md-sys-state-disabled-container-opacity` | exempt from contrast; a FAB is hidden rather than disabled | | Selected | the `secondary-container` pair, a filled icon, the emphasized style | component props (`selected`, `aria-selected`, `aria-pressed`) | combines with hover, focus and press | -- The state layer takes the content's `on-` colour (on `secondary-container` it is `on-secondary-container`), is 40px on a 48px target, and only one shows at a time. +- The state layer takes the content's `on-` colour (on `secondary-container` it is `on-secondary-container`), is 40px on a 48px target, and only one shows at a time. `md-state-layer` draws it in `currentColor` as a `::before`, so the element becomes `position: relative`. - Every state shows two indicators, so a colour change alone is never a state: add a shape, an outline, an icon, a weight or a word (`aria-selected` plus the container, an error colour plus an icon and a message). -- Targets: 48×48px minimum, 8px between targets, on every device; `touch-target` extends a smaller drawing to 48px. Density is an opt-in prop (`dense`) that steps padding by 4px and never applies to menus, snackbars, dialogs or settings controls, and never takes a target below 48px. +- Targets: 48×48px minimum, 8px between targets, on every device; `md-touch-target` extends a smaller drawing to 48px. Density is an opt-in prop (`dense`) that steps padding by 4px and never applies to menus, snackbars, dialogs or settings controls, and never takes a target below 48px. - Keyboard: Tab and Shift+Tab between components in DOM order, arrows within a component (menu, tabs, grid, radio group), Enter and Space activate, Escape dismisses; a dialog moves focus in on open and back to its opener on close. Sources: https://m3.material.io/foundations/interaction/states/state-layers · https://m3.material.io/foundations/interaction/states/applying-states · https://m3.material.io/foundations/designing/structure · https://m3.material.io/foundations/layout/grids-spacing/density ## Layout and breakpoints -Layout keys on the width of the window, in M3's five window size classes; they are the only responsive variants that compile, and `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`) gives scripts the same numbers. +Layout keys on the width of the window, in M3's five window size classes and only those. A layout component names the class in a prop (`hide-below`, `hide-from`, `stack-below`, ``'s `columns` map); the application's CSS writes the width as a range media query; a script asks `resources/js/breakpoints.js` (`from('expanded')`, `upTo('medium')`) for the same numbers. -| Class | Width | Variant | Navigation | Panes | Dialogs and choices | Margins | +| Class | Width | Prop value · CSS | Navigation | Panes | Dialogs and choices | Margins | | --- | --- | --- | --- | --- | --- | --- | -| Compact | below 600px | the default; `max-medium:` for "only here" | navigation bar; the rail opens as a modal | 1 | full-screen or basic dialog; a bottom sheet for choices | 16px | -| Medium | 600–839px | `medium:` | collapsed rail (96px) | 1, or 2 for low-density content at 50% each | basic dialog; a menu for choices | 24px | -| Expanded | 840–1199px | `expanded:` | rail, collapsed or expanded, collapsible | 2 recommended; a fixed pane 360px | basic dialog; menu | 24px | -| Large | 1200–1599px | `large:` | rail expanded | 2; a fixed pane 412px | basic dialog; menu | 24px | -| Extra-large | 1600px and up | `extra-large:` | rail expanded | 2, or 3 with a standard side sheet (at most 400px) | basic dialog; menu | 24px | +| Compact | below 600px | the default; `hide-from="medium"` for "only here" · `@media (width < 600px)` | navigation bar; the rail opens as a modal | 1 | full-screen or basic dialog; a bottom sheet for choices | 16px | +| Medium | 600–839px | `medium` · `@media (width >= 600px)` | collapsed rail (96px) | 1, or 2 for low-density content at 50% each | basic dialog; a menu for choices | 24px | +| Expanded | 840–1199px | `expanded` · `@media (width >= 840px)` | rail, collapsed or expanded, collapsible | 2 recommended; a fixed pane 360px | basic dialog; menu | 24px | +| Large | 1200–1599px | `large` · `@media (width >= 1200px)` | rail expanded | 2; a fixed pane 412px | basic dialog; menu | 24px | +| Extra-large | 1600px and up | `extra-large` · `@media (width >= 1600px)` | rail expanded | 2, or 3 with a standard side sheet (at most 400px) | basic dialog; menu | 24px | -- `` implements the navigation column; `` is the second pane of a list-detail layout from `expanded:`, `` puts a supporting pane (360px, beside the focus pane) from `expanded:` and below it before that. Moving up a class, ask what to reveal, divide into panes, resize, reposition or swap — never swap a component for one that does not do the same job. +- `` implements the navigation column; `` is a content region with the margins above; `` is the second pane of a list-detail layout from expanded, `` puts a supporting pane (360px, beside the focus pane) from expanded and below it before that. Moving up a class, ask what to reveal, divide into panes, resize, reposition or swap — never swap a component for one that does not do the same job. - Scaffold: bars (app bar at the top, navigation bar at the bottom: 3–5 destinations), rails (the navigation rail, toolbars, the FAB, on the leading edge), panes (all content), around a safety region that stays clear of the device's own chrome (`--material-safe-top|bottom|left|right`). -- Canonical layouts: feed (a grid of cards that gains columns per class), list-detail (one pane on compact, two from expanded; a back button only in single-pane mode, a selected row only in two-pane mode), supporting pane (two thirds focus, one third support). -- Bidirectionality: write logical properties (`ps-4`, `me-2`, `start-0`, `text-start`, `border-s`); leading and trailing icons swap, directional icons (back, send) mirror, the rail moves to the right; charts, media controls, clocks and Hebrew progress bars stay left-to-right. +- Canonical layouts: feed (``, a grid of cards that gains columns as the room grows), list-detail (one pane on compact, two from expanded; a back button only in single-pane mode, a selected row only in two-pane mode), supporting pane (two thirds focus, one third support). +- Bidirectionality: write logical properties (`padding-inline-start`, `margin-inline-end`, `inset-inline-start`, `border-inline-start`, `md-text-start`); `` runs in the inline direction and mirrors by itself; leading and trailing icons swap, directional icons (back, send) mirror, the rail moves to the right; charts, media controls, clocks and Hebrew progress bars stay left-to-right. Sources: https://m3.material.io/foundations/layout/breakpoints/overview · https://m3.material.io/foundations/layout/breakpoints/compact (medium, expanded, large-extra-large) · https://m3.material.io/foundations/layout/scaffold/overview · https://m3.material.io/foundations/layout/canonical-examples/overview · https://m3.material.io/foundations/layout/bidirectionality-rtl ## Spacing -M3's spacing tokens are multiples of an 8px base on a 4px grid; Tailwind's spacing scale is that grid, so `p-4` is `space200`. +M3's spacing tokens are multiples of an 8px base on a 4px grid. A layout component takes the token's name (`gap="space200"`, ``); the application's CSS reads it (`var(--md-sys-measurement-space200)`). -| Token | Value | Utility | +| Token | Value | In this library | | --- | --- | --- | -| space25 | 2px | `p-0.5`, `gap-0.5` | -| space50 | 4px | `p-1` | -| space75 | 6px | `p-1.5` | -| space100 | 8px (the base) | `p-2`, `gap-2` | -| space125 | 10px | `p-2.5` | -| space200 | 16px | `p-4` — a component's padding, compact margins | -| space300 | 24px | `p-6` — a dialog's padding, margins from `medium:` | -| space400 | 32px | `p-8` | -| space500 | 40px | `p-10` | -| space600 | 48px | `p-12` — a target | -| space700 | 56px | `p-14` | -| space800 | 64px | `p-16` | -| space900 | 72px | `p-18` | +| space25 | 2px | `space25` | +| space50 | 4px | `space50` | +| space75 | 6px | `space75` | +| space100 | 8px (the base) | `space100` | +| space125 | 10px | `space125` | +| space200 | 16px | `space200` — a component's padding, compact margins | +| space300 | 24px | `space300` — a dialog's padding, margins from medium | +| space400 | 32px | `space400` | +| space500 | 40px | `space500` | +| space600 | 48px | `space600` — a target | +| space700 | 56px | `space700` | +| space800 | 64px | `space800` | +| space900 | 72px | `space900` | -- Padding and gaps live on the parent (`p-4 gap-2` on the container), never as margins on children; a margin is for space beyond a container's padding or between layout regions. +- Padding and gaps live on the parent (`` around ``), never as margins on children; a margin is for space beyond a container's padding or between layout regions. - Spacing does not scale with text: at 200% text size the same padding and gaps stay. - Name a gap by what it separates when a component has several (icon–label 8px, label–supporting text 4px). @@ -250,16 +254,16 @@ Sources: https://m3.material.io/styles/spacing/overview · https://m3.material.i ## Icons -`` draws a Material Symbol Rounded (weight 400, grade 0), outlined or `filled`, at optical size 24 or `optical="20"`. +`` draws a Material Symbol Rounded (weight 400, grade 0), outlined or `filled`, at optical size 24 or 20. | Axis | Values | In this library | | --- | --- | --- | | Fill | 0 outlined, 1 filled | `filled` — active, selected or on state (a selected navigation item, a FAB's icon, a checked filter chip) | | Weight | 100–700; never below 200 at 24px | 400 for every icon; one weight per group | | Grade | −25 on dark backgrounds, 0 otherwise, positive for emphasis | 0 | -| Optical size | 20 dense, 24 standard, 40–48 with display type | `optical="20"` when drawn at 20px or less (small buttons, chips, dense lists), 24 otherwise | +| Optical size | 20 dense, 24 standard, 40–48 with display type | `size="20"` and below pick the 20 cut (small buttons, chips, dense lists); `optical="20"` for an icon sized by the application's own CSS | -- An icon beside text takes the text's size and colour (`size-5` beside `type-label-lg`, `size-6` beside body) and the same optical weight; its baseline sits about 11.5% of the text size below the text's. +- An icon beside text takes the text's size and colour (`size="20"` beside `md-type-label-lg`, 24 beside body) and the same optical weight; its baseline sits about 11.5% of the text size below the text's. - Icons stay flat and forward-facing, on the pixel grid, inside their 20px live area of the 24px canvas. - An icon-only control has an accessible name (`aria-label`, or a tooltip that names it); a decorative icon is `aria-hidden`; a complex icon drawn below 20px needs a label beside it. @@ -269,8 +273,8 @@ Sources: https://m3.material.io/styles/icons/overview · https://m3.material.io/ - Native elements before ARIA: `