Merge branch 'worktree-agent-aadcbb716875e9282'

This commit is contained in:
Andreas Reinhold / reini
2026-09-14 05:37:13 +02:00
45 changed files with 463 additions and 207 deletions
@@ -86,6 +86,7 @@ Tailwind's default palette is cleared: every colour class names an M3 role. `tex
- 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 `<html data-motion>` 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.
- `x-figure` on an element holding one number counts it up on first appearance and on change.
@@ -99,7 +100,7 @@ Tailwind's default palette is cleared: every colour class names an M3 role. `tex
Every component that meets the edge of the screen (app bar, navigation bar and rail, docked and placed toolbars, full-screen search, dialog and side sheet, bottom sheet, the skip link) keeps clear of a notch or home indicator through `var(--material-safe-top|bottom|left|right, env(safe-area-inset-…))`. The layout needs `viewport-fit=cover` in its viewport meta for the insets to be non-zero. Set a variable to replace the device's inset, on `<html>` or any ancestor: a browser test fakes a notch with `document.documentElement.style.setProperty('--material-safe-top', '47px')`, and an app that draws its own status strip adds its height.
`--material-bottom-extra` (default `0px`) is the height of anything the application docks on top of the phone's navigation bar in `<x-app-shell>` (an offline banner): the shell adds it to `--material-bottom-bar` (64px + the bottom inset), so the snackbar, a `fab` button and the page's bottom padding clear it too. Set it while the docked element shows, and remove it when it goes; place the docked element itself directly above the bar, at `bottom: calc(4rem + var(--material-safe-bottom, env(safe-area-inset-bottom)))`, below `sm` only.
`--material-bottom-extra` (default `0px`) is the height of anything the application docks on top of the phone's navigation bar in `<x-app-shell>` (an offline banner): the shell adds it to `--material-bottom-bar` (64px + the bottom inset), so the snackbar, a `fab` button and the page's bottom padding clear it too. Set it while the docked element shows, and remove it when it goes; place the docked element itself directly above the bar, at `bottom: calc(4rem + var(--material-safe-bottom, env(safe-area-inset-bottom)))`, on a compact window (below `medium`) only.
## Toasts
@@ -218,7 +219,7 @@ Label button, icon button, toggle and responsive FAB in one component.
| `link`, `external`, `no-wire-navigate` | | renders `<a>`, with `wire:navigate` unless external |
| `spinner` | | `true` shows the loading indicator while its `wire:click` runs; a string names the action |
| `tooltip`, `tooltip-left`, `tooltip-right`, `tooltip-bottom` | | plain tooltip; also the icon button's accessible name |
| `disabled`, `type`, `responsive`, `fab` | | `responsive` hides the label below `lg`; `fab` is an extended FAB below `sm`, a filled button above |
| `disabled`, `type`, `responsive`, `fab` | | `responsive` hides the label below `expanded`; `fab` is an extended FAB on a compact window (below `medium`), a filled button from there |
```blade
<x-button label="Create link" icon="link" variant="filled" size="md" wire:click="create" spinner />
@@ -279,12 +280,12 @@ Attributes go to the leading button; the slot is the menu. `variant` (`filled` d
### `<x-fab>`
`<x-fab icon="add" tooltip="New share" />``size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself; wrap it (`<div class="fixed end-4 bottom-4">`). `link`, `external`, `disabled`, `type`.
`<x-fab icon="add" tooltip="New share" />``size` `sm` 56px (default), `md` 80px, `lg` 96px; with `label` it is an extended FAB. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself; wrap it (`<div class="fixed end-4 bottom-4 large:end-6 large:bottom-6">` — M3's 16dp margin, 24dp from `large`). `link`, `external`, `disabled`, `type`.
### `<x-fab-menu>`, `<x-fab-menu-item>`
```blade
<div class="fixed end-4 bottom-4">
<div class="fixed end-4 bottom-4 large:end-6 large:bottom-6">
<x-fab-menu label="New">
<x-fab-menu-item label="Upload files" icon="upload_file" wire:click="uploadFiles" />
<x-fab-menu-item label="Paste text" icon="content_paste" link="{{ route('paste') }}" />
@@ -461,11 +462,11 @@ An M3 dialog on native `<dialog>`. Bind with `wire:model` to a flag or an id; cl
</x-modal>
```
Props: `title`, `subtitle`, `icon` (centred hero icon), `separator`, `persistent` (no Escape or scrim), `fullscreen` (whole screen below `sm`, for forms), `box-class`. Never remove its `wire:ignore.self` behaviour by re-rendering it conditionally with `@if`; toggle the bound property instead.
Props: `title`, `subtitle`, `icon` (centred hero icon), `separator`, `persistent` (no Escape or scrim), `fullscreen` (whole screen on a compact window, below `medium`, for forms — M3 allows a full-screen dialog only there), `box-class`. Never remove its `wire:ignore.self` behaviour by re-rendering it conditionally with `@if`; toggle the bound property instead.
### `<x-drawer>`
An M3 side sheet, bound like `<x-modal>`; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button`, `close-on-escape` (default true), `without-backdrop-close`, `actions` slot. `pane` (with `pane-width`) turns it into a list-detail pane from `xl`: render it after the list inside `<div class="xl:flex xl:items-start xl:gap-6">`. Escape leaves a pane open unless `pane-close-on-escape`. Its body is a size container — lay out inside with `@md:` etc., not `sm:`.
An M3 side sheet, bound like `<x-modal>`; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button`, `close-on-escape` (default true), `without-backdrop-close`, `actions` slot. `pane` (with `pane-width`, `22.5rem` — M3's 360dp fixed pane) turns it into a second pane from `expanded`, where M3 shows two panes: render it after the list inside `<div class="expanded:flex expanded:items-start expanded:gap-6">`. Escape leaves a pane open unless `pane-close-on-escape`. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class.
### `<x-bottom-sheet>`
@@ -600,7 +601,7 @@ M3 date pickers on a text field. `wire:model` stores `Y-m-d` strings (`x-model`
| Prop | Default | |
|---|---|---|
| `mode` | `docked` | `docked`: type a date (in the locale's numeric format) or pick one from a calendar under the field, which opens as a dialog below `sm`; `modal`: the field opens a calendar dialog; `input`: the dialog opens on a text field. Both dialogs switch between calendar and typing |
| `mode` | `docked` | `docked`: type a date (in the locale's numeric format) or pick one from a calendar under the field, which opens as a dialog on a compact window (below `medium`), as M3 asks; `modal`: the field opens a calendar dialog; `input`: the dialog opens on a text field. Both dialogs switch between calendar and typing |
| `range` | `false` | binds one array property, `['start' => 'Y-m-d', 'end' => 'Y-m-d']` (either may be null); errors for `trip`, `trip.start` and `trip.end` show on the field |
| `min`, `max` | `null` | `Y-m-d` or a date; days outside are disabled and the keyboard stays inside |
| `label`, `hint`, `icon`, `variant`, `size` | | the field's |
@@ -650,7 +651,7 @@ Bind with `wire:model` (entangled) or, without Livewire, `x-model`. The options
### `<x-search>`
M3 search bar that opens into a search view: docked under the bar from `sm`, full screen with a back arrow below (`docked` keeps it docked). Bind the input like any other and render the results in the slot; `empty` is shown when the slot renders nothing. Choosing a result (a link or button) closes the view; ArrowDown walks the results, Escape closes. Props: `placeholder` ("Search"), `label`, `icon`; `trailing` slot (avatar, icon buttons).
M3 search bar that opens into a search view: docked under the bar from `medium` (600px), full screen with a back arrow on a compact window (`docked` keeps it docked). Bind the input like any other and render the results in the slot; `empty` is shown when the slot renders nothing. Choosing a result (a link or button) closes the view; ArrowDown walks the results, Escape closes. Props: `placeholder` ("Search"), `label`, `icon`; `trailing` slot (avatar, icon buttons).
```blade
<x-search wire:model.live.debounce.300ms="query" placeholder="Search shares">
@@ -665,7 +666,16 @@ The docked view overlaps what is under it; never place a search inside an elemen
### `<x-app-shell>`
The adaptive app shell, a whole layout's body: a navigation bar below `sm`, a collapsed rail that opens as a modal to `lg`, an expanded rail the visitor can collapse from `lg`, the page as `<main id="content" wire:transition.navigate>` behind a skip link, and the snackbar host (do not add another `<x-toast />`). It needs `<x-theme-script />` in `<head>`.
The adaptive app shell, a whole layout's body: one navigation per M3 window size class, the page as `<main id="content" wire:transition.navigate>` behind a skip link, and the snackbar host (do not add another `<x-toast />`). It needs `<x-theme-script />` in `<head>`.
| Window size class | Width | Navigation | Margin |
| --- | --- | --- | --- |
| Compact | below `medium` (600px) | navigation bar, pinned to the bottom; the rest in the modal rail, opened by `$store.rail.show()` | 16px |
| Medium | `medium` 600839 | collapsed rail (96px) in the layout, no bar; its menu button opens it expanded over a scrim | 24px |
| Expanded | `expanded` 8401199 | standard rail in the layout, collapsed; the menu button expands it in place, no scrim | 24px |
| Large, extra-large | `large` from 1200 | the same standard rail, expanded to begin with | 24px |
A visitor who has pressed the menu button keeps that choice in both standard bands (`$store.rail`, remembered and applied before the first paint).
```blade
<x-app-shell :destinations="[
@@ -683,7 +693,7 @@ The adaptive app shell, a whole layout's body: a navigation bar below `sm`, a co
</x-slot:rail-footer>
<x-slot:top>
{{-- the page's app bar; its menu button opens the modal rail on a phone --}}
<span class="sm:hidden"><x-button icon="menu" tooltip="Open navigation" x-on:click="$store.rail.show()" /></span>
<span class="medium:hidden"><x-button icon="menu" tooltip="Open navigation" x-on:click="$store.rail.show()" /></span>
</x-slot:top>
{{ $slot }}
@@ -692,16 +702,18 @@ The adaptive app shell, a whole layout's body: a navigation bar below `sm`, a co
- `destinations`: `title`, `icon`, `url`; optional `active` (default: the URL is the page's, also during a Livewire update request), `badge` (`true` for a dot, or a count), `badgeLabel` (what a screen reader hears for the badge: "3 unread"), `section` (a heading in the rail, shown only while it is expanded; consecutive destinations with the same section are grouped), `bar` (default `true`; `false` keeps it out of the bottom bar — M3 wants three to five there), `navigate` (`false` for a full page load instead of `wire:navigate`).
- Slots, each rendered once: `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the app bar, above the page at every width), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`).
- The rail is one element at every width: what is in it is also what a phone sees in the modal rail. Below `sm` 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 `sm`).
- Below `sm` the shell sets `--material-bottom-bar` (the bar, the bottom safe area and `--material-bottom-extra`), so the snackbar, a `fab` button 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-lg:overflow-x-clip`. Never make a page wrapper `overflow-x-hidden`: it turns the region into a scroll container and breaks every `sticky` inside.
- 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 shell writes no gutters of its own; something that must reach the window's edges opts out with `-mx-(--material-margin)`.
- Two panes side by side are M3's from `expanded`: `<x-drawer pane>` is the second one, in an `expanded:flex expanded:items-start expanded:gap-6` row inside the page.
- On a compact window the shell sets `--material-bottom-bar` (the bar, the bottom safe area and `--material-bottom-extra`), so the snackbar, a `fab` button 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.
### `<x-navigation-bar>`, `<x-navigation-bar-item>`
M3 Expressive's flexible navigation bar, for three to five destinations. It does not position itself; wrap it (`<x-app-shell>` does):
```blade
<div class="fixed inset-x-0 bottom-0 z-30 sm:hidden">
<div class="fixed inset-x-0 bottom-0 z-30 medium:hidden">
<x-navigation-bar>
<x-navigation-bar-item label="Shares" icon="folder_shared" link="{{ route('shares.index') }}" :active="request()->routeIs('shares.*')" badge="3" />
<x-navigation-bar-item label="Upload" icon="upload" link="{{ route('upload') }}" />
@@ -735,11 +747,11 @@ M3 Expressive's navigation rail: collapsed (96px, icon over label) or expanded (
</div>
```
- `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` (`<x-app-shell>`'s: hidden and opened as a modal below `sm`, collapsed and opened as a modal to `lg`, collapsible from `lg`).
- `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` (`<x-app-shell>`'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), `menu` (the menu button; on by default for `collapsible`, `modal`, `adaptive`). Slots: `brand` (beside the menu button, expanded only), `header` (a FAB), the destinations (the only part that scrolls), `footer`. In a flex row the rail sticks to the top of the viewport.
- Anything inside a rail takes both shapes with the `rail-collapsed:` variant, true while that rail is drawn collapsed for whatever reason: `<span class="rail-collapsed:hidden">…expanded only…</span>`, `<span class="hidden rail-collapsed:inline-flex">…collapsed only…</span>`. Put the variant on a wrapper, never on a component. Nothing that shows while collapsed may be wider than 96px.
- `<x-navigation-rail-item>`: the same props as `<x-navigation-bar-item>`. `<x-navigation-rail-section label="…">`: 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), `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`).
- `$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`).
### `<x-app-bar>`
@@ -779,7 +791,7 @@ M3 tabs with a server-rendered tablist (arrow keys, Home/End, disabled tabs skip
### `<x-section-nav>`
Navigation between the sections of one area (settings, admin): secondary tabs as links from `sm` (wrapping onto a grid rather than scrolling), a menu picker below, whose items mark the current section as the page (`current`) and carry each section's badge. `items`: `['title', 'url', 'icon', 'active', 'badge']` — current when `active` or its `url` is the page's (during a Livewire update request, the page the component was rendered on, so the section stays lit when a component re-renders). `label`, `no-wire-navigate`.
Navigation between the sections of one area (settings, admin): secondary tabs as links from `medium` (600px; wrapping onto a grid rather than a single row below `large`, rather than scrolling), a menu picker on a compact window, whose items mark the current section as the page (`current`) and carry each section's badge. `items`: `['title', 'url', 'icon', 'active', 'badge']` — current when `active` or its `url` is the page's (during a Livewire update request, the page the component was rendered on, so the section stays lit when a component re-renders). `label`, `no-wire-navigate`.
### `<x-account-menu>`
@@ -872,6 +884,6 @@ It also fails on every value the theme cleared, and names the replacement on the
## Livewire traps
- Blade directives do not compile inside a component tag's attributes: `<x-foo x-show="ok(@js($value))">` reaches the browser as literal text. On a component tag use `{{ }}` and `:prop` bindings, or put the Alpine on a plain element inside the slot.
- Never pass `hidden`, a display utility or a position (`absolute`, `relative`) to a component: it is merged beside the component's own and whichever Tailwind emits last wins. Wrap the component in an element that carries it. A variant that only hides (`max-sm:hidden`) is safe.
- 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.
- `$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.
+27 -8
View File
@@ -30,9 +30,17 @@
*
* `rail-collapsed:` matches a rail, and everything in it, while it is drawn collapsed — whatever
* made it so: its mode, the visitor's choice on <html data-rail> (set before the first paint by
* <x-theme-script>), or a window under `lg` for the adaptive rail. A rail item is written once
* <x-theme-script>), or a window below `expanded` (840px) for the adaptive rail. A rail item is written once
* and takes both shapes from it; so can anything an application puts in a rail
* (`<span class="rail-collapsed:hidden">`).
*
* The two widths the adaptive rail turns on are M3's window size classes, not Tailwind's screens
* (docs/reference/m3/foundations.md § Layout, foundations-supplement.md § Breakpoints): 37.5rem
* (600px) is the compact/medium boundary — below it the navigation bar carries the destinations and
* the rail takes no room in the layout; 52.5rem (840px) is `expanded`, where M3 asks for a standard
* rail in the layout rather than one that opens over a scrim; 75rem (1200px) is `large`, where the
* rail starts expanded instead of collapsed. The bar's own item layout is a *container* query at
* the same 37.5rem, so a bar in a narrow column lays out by its own width.
*/
@custom-variant rail-collapsed {
@@ -48,13 +56,24 @@
@slot;
}
@media (width < 64rem) {
@media (width < 52.5rem) {
&:where([data-navigation-rail='adaptive']:not([data-open]), [data-navigation-rail='adaptive']:not([data-open]) *) {
@slot;
}
}
@media (width >= 64rem) {
/* Expanded (8401199): a standard rail, collapsed unless the visitor expanded it. `data-rail`
alone cannot say that — it carries `rail.default` for a visitor who never chose — so
`data-rail-auto`, which <x-theme-script> sets while nothing is stored, stands for "no
choice yet" and the class's own default applies. */
@media (52.5rem <= width < 75rem) {
&:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive'], :is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive'] *) {
@slot;
}
}
/* Large and extra-large (from 1200): expanded to begin with, which is what M3 prefers there. */
@media (width >= 75rem) {
&:where([data-rail='collapsed'] [data-navigation-rail='adaptive'], [data-rail='collapsed'] [data-navigation-rail='adaptive'] *) {
@slot;
}
@@ -176,18 +195,18 @@
}
/* A modal rail keeps its collapsed width in the layout while it is open over it, as Compose's
ModalWideNavigationRail does; the adaptive rail does below lg, and takes no room below sm. */
ModalWideNavigationRail does; the adaptive rail does below `expanded`, and takes no room on a compact window. */
[data-navigation-rail='modal'] {
width: 6rem;
}
@media (width < 64rem) {
@media (width < 52.5rem) {
[data-navigation-rail='adaptive'] {
width: 6rem;
}
}
@media (width < 40rem) {
@media (width < 37.5rem) {
[data-navigation-rail='adaptive'] {
width: 0;
}
@@ -230,9 +249,9 @@
box-shadow: var(--md-sys-elevation-2);
}
/* Below sm there is no collapsed rail to grow out of: the open rail slides in from the edge,
/* On a compact window there is no collapsed rail to grow out of: the open rail slides in from the edge,
on emphasized decelerate rather than a spring, which would overshoot and open a gap. */
@media (width < 40rem) {
@media (width < 37.5rem) {
[data-navigation-rail='adaptive'] > [data-navigation-rail-panel] {
position: fixed;
inset-block: 0;
+15 -7
View File
@@ -24,10 +24,18 @@
* on by default): two toggle buttons 4px apart, round and surface-container-lowest when off, a 12px
* corner and primary-container with a bold label when on, not the outlined pair of the tokens.
*
* In a landscape window the dial variant lies on its side, as Compose's HorizontalTimePicker does
* whenever the screen is wider than it is tall: the display with the period selector (216×38, 16px
* under it) on the start side, the dial 36px after it, the actions under both; and the dial shrinks
* to 238 or 200px when the window is short (ClockFaceSizeModifier). The input variant always stands.
* In a landscape window with no room for the upright dial the dial variant lies on its side, as
* Compose's HorizontalTimePicker does: the display with the period selector (216×38, 16px under it)
* on the start side, the dial 36px after it, the actions under both; and the dial shrinks to 238 or
* 200px when the window is shorter still (ClockFaceSizeModifier). The input variant always stands.
*
* The switch is orientation and viewport *height*, never a width breakpoint: M3 says the picker
* "swaps orientation/variant based on device orientation and viewport height"
* (docs/reference/m3/components-navigation-selection-inputs.md § Time pickers/Behaviour), and no
* window size class applies. 35rem is what the upright dial needs — 33rem of surface (3 padding,
* 2.25 title, 7.25 display, 17.5 dial, 3 actions) inside the dialog's `100dvh - 2rem` — so a
* landscape phone lies the dial down and a landscape tablet or desktop, which has the height,
* leaves it standing.
*
* The selector's angle is a registered property, so a single transition turns the line, carries the
* handle round and moves the clip that inks the number under it, on the default spatial spring. The
@@ -400,7 +408,7 @@
/* ---- Landscape ---------------------------------------------------------------------------- */
@media (orientation: landscape) and (min-width: 37rem) {
@media (orientation: landscape) and (height < 35rem) {
[data-timepicker-surface][data-mode="dial"] {
grid-template-columns: 13.5rem auto;
grid-template-areas:
@@ -446,13 +454,13 @@
}
}
@media (orientation: landscape) and (min-width: 37rem) and (max-height: 22.75rem) {
@media (orientation: landscape) and (max-height: 22.75rem) {
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
--dial: 14.875rem;
}
}
@media (orientation: landscape) and (min-width: 37rem) and (max-height: 21.625rem) {
@media (orientation: landscape) and (max-height: 21.625rem) {
[data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] {
--dial: 12.5rem;
}
+4 -3
View File
@@ -4,7 +4,8 @@
*
* The dialog is shown two ways. Docked, it is a `popover="manual"` placed under the field by CSS
* anchor positioning; modal (the `modal` and `input` modes, and a docked picker on a compact
* window) it is opened with `showModal()`. Everything a person sees in it is drawn by Alpine from
* window — below `medium`, 600px, where M3 puts the picker in a dialog rather than under the
* field) it is opened with `showModal()`. Everything a person sees in it is drawn by Alpine from
* the state below, and the dialog is `wire:ignore`, so a Livewire render never touches it while
* it is open.
*
@@ -20,7 +21,7 @@
* Enter selects and confirms, Escape closes and hands focus back to the field. Focus never leaves
* the `min``max` span, so a disabled day cannot be chosen from the keyboard either.
*/
const COMPACT = '(max-width: 39.99rem)'
import { upTo } from './breakpoints.js'
// CLDR's first day of the week by region, for an engine without `Intl.Locale#getWeekInfo`.
const WEEK_STARTS_SUNDAY = 'AG AS BD BR BS BT BW BZ CA CN CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW'.split(' ')
@@ -222,7 +223,7 @@ document.addEventListener('alpine:init', () => {
}
})
const query = window.matchMedia(COMPACT)
const query = upTo('medium')
this.compact = query.matches
query.addEventListener('change', (event) => (this.compact = event.matches))
+49 -15
View File
@@ -5,6 +5,9 @@
* localStorage. <x-theme-script> has already applied it before the first paint as
* <html data-rail="expanded|collapsed">, which is what the stylesheet keys on (the
* `rail-collapsed:` variant); the store starts from that attribute and writes it back.
* `$store.rail.auto` is true while nothing is stored the value is `rail.default`, not a choice
* and the adaptive rail then takes its window size class's default instead: collapsed in the
* expanded class (8401199), expanded from `large` (1200), as M3 asks. The first `set()` drops it.
*
* `$store.rail.open` is the modal rail: on a window too narrow for an expanded rail, a menu
* button opens it over a scrim (`show()`), and Escape, the scrim or leaving the page closes it
@@ -13,7 +16,7 @@
* `materialNavigationRail` is one rail's view of the store for its `mode` see
* resources/views/components/navigation-rail.blade.php.
*/
const WIDE = '(min-width: 64rem)'
import { from } from './breakpoints.js'
/*
* The active indicator grows out of its centre when a page arrives through wire:navigate. The
@@ -48,6 +51,10 @@ document.addEventListener('alpine:init', () => {
window.Alpine.store('rail', {
collapsed: root.dataset.rail === 'collapsed',
// Nothing stored yet: `collapsed` is only `rail.default`, so an adaptive rail may still
// take its window size class's own default. The first choice made here clears it.
auto: root.hasAttribute('data-rail-auto'),
open: false,
toggle() {
@@ -64,7 +71,9 @@ document.addEventListener('alpine:init', () => {
set(collapsed) {
this.collapsed = collapsed
this.auto = false
root.dataset.rail = collapsed ? 'collapsed' : 'expanded'
root.removeAttribute('data-rail-auto')
try {
localStorage.setItem(root.dataset.railKey || 'material-rail', root.dataset.rail)
@@ -86,30 +95,43 @@ document.addEventListener('alpine:init', () => {
document.addEventListener('livewire:navigating', () => window.Alpine.store('rail').hide())
window.Alpine.data('materialNavigationRail', (mode) => ({
wide: mode === 'adaptive' ? window.matchMedia(WIDE).matches : false,
query: null,
onWidth: null,
wide: mode === 'adaptive' ? from('expanded').matches : false,
roomy: mode === 'adaptive' ? from('large').matches : false,
queries: [],
listeners: [],
init() {
if (mode !== 'adaptive') {
return
}
// From lg the adaptive rail is a standard, collapsible rail: a modal left open while
// the window widens is shut, or its focus trap would hold a page that has no scrim.
this.query = window.matchMedia(WIDE)
this.onWidth = (event) => {
this.wide = event.matches
// From `expanded` (840px) the adaptive rail is a standard, collapsible rail — what M3
// asks for at expanded and above. A modal left open while the window widens is shut,
// or its focus trap would hold a page that has no scrim.
this.watch(from('expanded'), (matches) => {
this.wide = matches
if (event.matches) {
if (matches) {
this.$store.rail.hide()
}
}
this.query.addEventListener('change', this.onWidth)
})
// From `large` (1200px) it starts expanded rather than collapsed, until someone
// chooses otherwise; below that the expanded class starts it collapsed. Both mirror
// the `rail-collapsed:` variant in resources/css/components/navigation.css.
this.watch(from('large'), (matches) => (this.roomy = matches))
},
watch(query, onChange) {
const listener = (event) => onChange(event.matches)
query.addEventListener('change', listener)
this.queries.push(query)
this.listeners.push(listener)
},
destroy() {
this.query?.removeEventListener('change', this.onWidth)
this.queries.forEach((query, index) => query.removeEventListener('change', this.listeners[index]))
},
/** Whether this rail expands over a scrim rather than in the layout. */
@@ -126,7 +148,17 @@ document.addEventListener('alpine:init', () => {
return true
}
return (mode === 'collapsible' || (mode === 'adaptive' && this.wide)) && !this.$store.rail.collapsed
if (this.$store.rail.collapsed) {
return false
}
if (mode === 'adaptive') {
// A standard rail from `expanded`; with no choice stored it is the window size
// class that decides, and only `large` and above start it expanded.
return this.wide && (this.roomy || !this.$store.rail.auto)
}
return mode === 'collapsible'
},
/** The rail's own menu button: open or close the modal, or collapse and expand in place. */
@@ -134,7 +166,9 @@ document.addEventListener('alpine:init', () => {
if (this.modal) {
this.$store.rail.open ? this.$store.rail.hide() : this.$store.rail.show()
} else {
this.$store.rail.toggle()
// `set`, not `toggle`: with nothing stored the store's `collapsed` is only
// `rail.default`, so the button has to flip what is actually drawn.
this.$store.rail.set(this.expanded)
}
},
}))
+5 -5
View File
@@ -4,11 +4,11 @@
* Focus or a press on the bar opens the view; Escape, the back arrow, a press outside, focus
* leaving the search, or choosing a result closes it. ArrowDown from the input moves into the
* results and the arrow keys walk them; ArrowUp past the first result returns to the input. On a
* compact window (below `sm`) the view is full screen and modal: focus stays in it and the page
* behind does not scroll. The results themselves are the caller's, rendered by Livewire into the
* view as the query changes.
* compact window (below `medium`, 600px) the view is full screen and modal: focus stays in it and
* the page behind does not scroll M3 docks the view from medium upwards. The results themselves
* are the caller's, rendered by Livewire into the view as the query changes.
*/
const COMPACT = '(max-width: 39.99rem)'
import { upTo } from './breakpoints.js'
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
@@ -26,7 +26,7 @@ document.addEventListener('alpine:init', () => {
closedAt: -Infinity,
init() {
const query = window.matchMedia(COMPACT)
const query = upTo('medium')
this.compact = query.matches
query.addEventListener('change', (event) => (this.compact = event.matches))
+32 -14
View File
@@ -11,13 +11,31 @@
…the page…
</x-app-shell>
- Below `sm`: a navigation bar with the destinations marked `bar`, pinned to the bottom.
Everything else is in the modal rail, which slides in when something calls
`$store.rail.show()` put a menu button in the app bar for it, hidden from `sm`:
`<span class="sm:hidden"><x-button icon="menu" tooltip="Open navigation" x-on:click="$store.rail.show()" /></span>`.
- `sm` to `lg`: the collapsed rail, whose menu button opens it expanded, as a modal.
- From `lg`: the expanded rail, collapsed and expanded again by its menu button; the choice is
remembered and applied before the first paint (`$store.rail`, <x-theme-script>).
The navigation is M3's per window size class (docs/reference/m3/foundations.md § Layout and
foundations-supplement.md § Breakpoints), and only those four numbers:
- **Compact**, below `medium` (600px): a navigation bar with the destinations marked `bar`,
pinned to the bottom. Everything else is in the modal rail, which slides in when something
calls `$store.rail.show()` put a menu button in the app bar for it, hidden from `medium`:
`<span class="medium:hidden"><x-button icon="menu" tooltip="Open navigation" x-on:click="$store.rail.show()" /></span>`.
- **Medium** (600839): the collapsed rail in the layout, 96px, and no bar; its menu button
opens it expanded over a scrim, since 256px beside the page would leave the page too little.
- **Expanded** (8401199): a standard rail in the layout, nothing covered collapsed until
the menu button expands it in place.
- **Large and extra-large** (from 1200): the same standard rail, expanded to begin with
(`rail.default`), which is what M3 prefers once there is room.
From `expanded` the choice the menu button makes is remembered and applied before the first
paint (`$store.rail`, <x-theme-script>), so the rail never paints one width and snaps to the
other. A visitor who has chosen keeps that choice in both bands; one who never has gets the
class's own default.
`--material-margin` is M3's window margin 16px on a compact window, 24px from `medium` and
the content region is padded with it, so a page inside the shell writes no gutters of its own.
Something meant to reach the window's edges opts out with `-mx-(--material-margin)`.
Two panes side by side are M3's from `expanded`: `<x-drawer pane>` is the second one, 360dp
wide, in an `expanded:flex expanded:items-start expanded:gap-6` row inside the page.
`destinations` is a list of arrays: `title`, `icon` (a Material Symbol), `url`, and optionally
`active` (by default: the URL is the page's; during a Livewire update request, the page the
@@ -37,15 +55,15 @@
The page is `<main id="content">` with `wire:transition.navigate`, behind a skip link that is
the first thing a keyboard reaches. The snackbar host (`<x-toast />`) is part of the shell;
below `sm` it, and a `fab` button, sit above the bottom bar through `--material-bottom-bar`:
on a compact window it, and a `fab` button, sit above the bottom bar through `--material-bottom-bar`:
the bar's 64px, the bottom safe area (`--material-safe-bottom`, else the device's inset) and
`--material-bottom-extra` (0px unless the application docks something, an offline banner, on
top of the bar).
`max-lg:overflow-x-clip` on the content region is the backstop under every page, and it stays
`max-expanded:overflow-x-clip` on the content region is the backstop under every page, and it stays
`clip`: `overflow-x: hidden` would force `overflow-y` to `auto`, turn the region into a scroll
container and break every `position: sticky` inside it (an app bar, a list-detail pane). Below
`lg` only, so a wide window never clips what overhangs on purpose.
`expanded` only, so a wide window never clips what overhangs on purpose.
Nothing application-specific belongs in here: an app's destinations and chrome come in through
the props and slots. --}}
@@ -84,8 +102,8 @@
<div
data-app-shell
@class([
'min-h-dvh bg-surface text-on-surface sm:flex',
'max-sm:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty(),
'min-h-dvh bg-surface text-on-surface [--material-margin:1rem] medium:flex medium:[--material-margin:1.5rem]',
'max-medium:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty(),
])
>
<a
@@ -133,13 +151,13 @@
<div class="flex min-w-0 flex-1 flex-col">
{{ $top ?? '' }}
<main id="content" tabindex="-1" wire:transition.navigate class="min-w-0 flex-1 outline-none max-lg:overflow-x-clip max-sm:pb-(--material-bottom-bar)">
<main id="content" tabindex="-1" wire:transition.navigate class="min-w-0 flex-1 px-(--material-margin) outline-none max-expanded:overflow-x-clip max-medium:pb-(--material-bottom-bar)">
{{ $slot }}
</main>
</div>
@if ($barItems->isNotEmpty())
<div data-app-shell-bar class="fixed inset-x-0 bottom-0 z-30 sm:hidden">
<div data-app-shell-bar class="fixed inset-x-0 bottom-0 z-30 medium:hidden">
<x-livewire-material::navigation-bar :label="$label">
@foreach ($barItems as $item)
<x-livewire-material::navigation-bar-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
+8 -6
View File
@@ -24,9 +24,11 @@
Behaviour kept from maryUI: `link` renders an anchor with `wire:navigate` (unless `external`
or `no-wire-navigate`); `disabled` works on a link too, as `aria-disabled`; `spinner` shows
the loading indicator while the button's own `wire:click` runs (or the action named by a
string); `responsive` hides the label below `lg`; `tooltip`, `tooltip-left`, `tooltip-right`
string); `responsive` hides the label below `expanded` (840px, where M3 lets buttons reposition);
`tooltip`, `tooltip-left`, `tooltip-right`
and `tooltip-bottom` attach a plain tooltip. `fab` is a page's create action: an extended FAB
pinned in the thumb zone below `sm`, a filled button above it one element either way.
pinned in the thumb zone on a compact window (below `medium`, 600px), a filled button from
there one element either way.
Never pass `hidden`, a display or a position class: the button is `inline-flex` and
`relative`, and whichever Tailwind emits last wins. Wrap it instead. --}}
@@ -173,8 +175,8 @@
'disabled:cursor-not-allowed disabled:shadow-none aria-disabled:pointer-events-none aria-disabled:shadow-none',
'disabled:bg-on-surface/10 disabled:text-on-surface/38 aria-disabled:bg-on-surface/10 aria-disabled:text-on-surface/38' => $contained,
'disabled:text-on-surface/38 aria-disabled:text-on-surface/38' => ! $contained,
// The FAB, below sm only: an extended FAB in the thumb zone, clear of a bottom bar the layout declares.
'max-sm:fixed max-sm:end-4 max-sm:bottom-[calc(var(--material-bottom-bar,0px)+1rem)] max-sm:z-30 max-sm:h-14 max-sm:gap-2 max-sm:px-4 max-sm:rounded-corner-lg max-sm:type-title-md max-sm:bg-primary-container max-sm:text-on-primary-container max-sm:shadow-elevation-3' => $fab,
// The FAB, on a compact window only: an extended FAB in the thumb zone, clear of a bottom bar the layout declares.
'max-medium:fixed max-medium:end-4 max-medium:bottom-[calc(var(--material-bottom-bar,0px)+1rem)] max-medium:z-30 max-medium:h-14 max-medium:gap-2 max-medium:px-4 max-medium:rounded-corner-lg max-medium:type-title-md max-medium:bg-primary-container max-medium:text-on-primary-container max-medium:shadow-elevation-3' => $fab,
];
$tag = $isLink ? 'a' : 'button';
@@ -210,12 +212,12 @@
@if ($icon)
<span class="contents" @if ($spinnerTarget) wire:loading.remove wire:target="{{ $spinnerTarget }}" @endif>
<x-livewire-material::icon :name="$icon" :filled="$selected === true" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'max-sm:size-6' => $fab])" />
<x-livewire-material::icon :name="$icon" :filled="$selected === true" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'max-medium:size-6' => $fab])" />
</span>
@endif
@unless ($iconOnly)
<span @class(['max-lg:hidden' => $responsive])>{{ $label ?? $slot }}</span>
<span @class(['max-expanded:hidden' => $responsive])>{{ $label ?? $slot }}</span>
@endunless
@if ($iconRight)
+22 -18
View File
@@ -1,5 +1,8 @@
{{-- An M3 side sheet: detail or controls that slide in from the edge over a scrim; on a phone it is
the whole screen. With `pane`, from `xl` it is a list-detail pane beside the list instead.
the whole screen. With `pane`, from `expanded` (840px) it is a second pane beside the list
instead M3 shows two panes for list-detail from expanded upwards, and puts a supporting pane
on the side of the focus pane at expanded, 360dp wide
(docs/reference/m3/foundations-supplement.md § Canonical layout examples).
The open state is the Livewire property in `wire:model` (entangled live, so a detail held in the
URL follows a close) a flag or an id and closing writes back `false` or `null`. Without
@@ -9,13 +12,14 @@
As a sheet it is modal: the page inert and still (`x-trap.inert.noscroll`), and it enters on
emphasized decelerate rather than a spring a sheet anchored to the edge that overshot would
open a gap. M3's modal side sheet: surface-container-low, a large corner on its inner edge,
elevation 1; `side` `end` (the default) or `start`; `width` from `sm` (a caller's `w-*` would
race the sheet's own).
elevation 1; `side` `end` (the default) or `start`; `width` from `medium` (a caller's `w-*`
would race the sheet's own a compact window gets the full-bleed sheet).
As a pane (`pane`, from `xl`) nothing is covered: the page renders the drawer after its list in
an `xl:flex xl:items-start xl:gap-6` row, the drawer sticks under the top of the viewport, the
As a pane (`pane`, from `expanded`) nothing is covered: the page renders the drawer after its
list in an `expanded:flex expanded:items-start expanded:gap-6` row, the drawer sticks under the
top of the viewport, the
list stays usable and another row swaps what it shows no scrim, no trap, no inert page. While
closed it takes no room. `pane-width` sizes the pane (the sheet's width by default). Escape closes
closed it takes no room. `pane-width` sizes the pane (`22.5rem`, M3's 360dp fixed pane). Escape closes
the sheet but leaves a pane open, since the page beside it is still in use; `pane-close-on-escape`
closes the pane on Escape too, for a pane that is a transient detail. The body is
a size container, so its contents lay out by the room the sheet or pane actually has (`@md:`),
@@ -35,7 +39,7 @@
'withoutBackdropClose' => false,
'width' => '25rem',
'pane' => false,
'paneWidth' => null,
'paneWidth' => '22.5rem',
'paneCloseOnEscape' => false,
])
@@ -52,7 +56,7 @@
close() { this.open = typeof this.open === 'boolean' ? false : null; },
@if ($pane)
init() {
const query = window.matchMedia('(min-width: 80rem)');
const query = window.matchMedia('(width >= 52.5rem)');
this.wide = query.matches;
query.addEventListener('change', (event) => this.wide = event.matches);
},
@@ -61,8 +65,8 @@
@if ($closeOnEscape) x-on:keydown.window.escape="{{ $paneCloseOnEscape ? 'if (open) close()' : 'if (open && ! wide) close()' }}" @endif
data-sheet="{{ $id }}"
@if ($pane)
x-bind:class="! open && 'xl:hidden'"
class="xl:sticky xl:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1.25rem)] xl:shrink-0 xl:self-start"
x-bind:class="! open && 'expanded:hidden'"
class="expanded:sticky expanded:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1.25rem)] expanded:shrink-0 expanded:self-start"
data-pane
@endif
>
@@ -71,7 +75,7 @@
x-show="open"
x-transition.opacity.duration.200ms
@if (! $withoutBackdropClose) x-on:click="close()" @endif
@class(['fixed inset-0 z-40 bg-scrim/32', 'xl:hidden' => $pane])
@class(['fixed inset-0 z-40 bg-scrim/32', 'expanded:hidden' => $pane])
aria-hidden="true"
></div>
@@ -80,24 +84,24 @@
x-show="open"
x-trap.inert.noscroll="open && ! wide"
x-transition:enter="transition-[translate,opacity] duration-(--md-sys-motion-spatial-default-duration) ease-emphasized-decelerate"
x-transition:enter-start="{{ $pane ? ($start ? 'max-xl:-translate-x-full xl:opacity-0' : 'max-xl:translate-x-full xl:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
x-transition:enter-start="{{ $pane ? ($start ? 'max-expanded:-translate-x-full expanded:opacity-0' : 'max-expanded:translate-x-full expanded:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
x-transition:enter-end="translate-x-0 opacity-100"
x-transition:leave="transition-[translate,opacity] duration-(--md-sys-motion-effects-default-duration) ease-emphasized-accelerate"
x-transition:leave-start="translate-x-0 opacity-100"
x-transition:leave-end="{{ $pane ? ($start ? 'max-xl:-translate-x-full xl:opacity-0' : 'max-xl:translate-x-full xl:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
x-transition:leave-end="{{ $pane ? ($start ? 'max-expanded:-translate-x-full expanded:opacity-0' : 'max-expanded:translate-x-full expanded:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
id="{{ $id }}"
x-bind:role="wide ? 'region' : 'dialog'"
x-bind:aria-modal="wide ? null : 'true'"
role="dialog"
aria-modal="true"
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
style="--sheet-width: {{ $width }}; --pane-width: {{ $paneWidth ?? $width }}"
style="--sheet-width: {{ $width }}; --pane-width: {{ $paneWidth }}"
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->class([
'fixed top-[var(--material-safe-top,env(safe-area-inset-top))] bottom-0 z-50 flex w-full flex-col overflow-y-auto bg-surface-container-low p-6 text-on-surface shadow-elevation-1',
'end-0 sm:rounded-s-corner-lg' => ! $start,
'start-0 sm:rounded-e-corner-lg' => $start,
'sm:w-(--sheet-width) sm:max-w-[calc(100vw-4rem)]',
'xl:relative xl:top-0 xl:z-auto xl:max-h-[calc(100dvh-2.5rem-var(--material-safe-top,env(safe-area-inset-top)))] xl:w-(--pane-width) xl:max-w-none xl:rounded-corner-lg xl:bg-surface-container xl:shadow-none' => $pane,
'end-0 medium:rounded-s-corner-lg' => ! $start,
'start-0 medium:rounded-e-corner-lg' => $start,
'medium:w-(--sheet-width) medium:max-w-[calc(100vw-4rem)]',
'expanded:relative expanded:top-0 expanded:z-auto expanded:max-h-[calc(100dvh-2.5rem-var(--material-safe-top,env(safe-area-inset-top)))] expanded:w-(--pane-width) expanded:max-w-none expanded:rounded-corner-lg expanded:bg-surface-container expanded:shadow-none' => $pane,
$attributes->get('class'),
]) }}
>
+10 -9
View File
@@ -19,7 +19,8 @@
high, extra-large corner, elevation 3, a headline-small `title`, body-medium `subtitle` in
on-surface-variant, and the `actions` slot at the end. `icon` puts a secondary-coloured icon
above a centred title, as M3 draws a dialog with a hero icon. `fullscreen` makes a dialog that
holds a form take the whole screen below `sm`, with a close button and the title in a top bar
holds a form take the whole screen on a compact window (below `medium`, 600px M3 uses
full-screen dialogs "only in compact breakpoints"), with a close button and the title in a top bar
clear of the notch. `persistent` ignores Escape and the scrim, for a dialog that must be
answered. It opens on the fast spatial spring and closes at once, as M3's do. --}}
@@ -58,17 +59,17 @@
'm-auto max-h-[calc(100dvh-3rem)] w-[calc(100vw-3rem)] max-w-[35rem] min-w-70 overflow-visible bg-transparent p-0 text-on-surface',
'backdrop:bg-scrim/32',
'opacity-100 scale-100 starting:opacity-0 starting:scale-95 transition-[opacity,scale] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
'max-sm:m-0 max-sm:h-dvh max-sm:max-h-none max-sm:w-full max-sm:max-w-none max-sm:min-w-0' => $fullscreen,
'max-medium:m-0 max-medium:h-dvh max-medium:max-h-none max-medium:w-full max-medium:max-w-none max-medium:min-w-0' => $fullscreen,
$attributes->get('class'),
])
>
<div @class([
'flex max-h-[inherit] flex-col overflow-y-auto rounded-corner-xl bg-surface-container-high p-6 shadow-elevation-3',
'max-sm:h-full max-sm:rounded-none max-sm:p-0 max-sm:pt-[var(--material-safe-top,env(safe-area-inset-top))]' => $fullscreen,
'max-medium:h-full max-medium:rounded-none max-medium:p-0 max-medium:pt-[var(--material-safe-top,env(safe-area-inset-top))]' => $fullscreen,
$boxClass,
])>
@if ($fullscreen)
<div class="flex h-16 shrink-0 items-center gap-1 px-1 sm:hidden">
<div class="flex h-16 shrink-0 items-center gap-1 px-1 medium:hidden">
<x-livewire-material::button icon="close" :tooltip="__('Close')" x-on:click="close()" />
@if (filled($title))
@@ -77,20 +78,20 @@
</div>
@endif
<div @class(['min-h-0 flex-1', 'max-sm:overflow-y-auto max-sm:px-6 max-sm:pb-6' => $fullscreen])>
<div @class(['min-h-0 flex-1', 'max-medium:overflow-y-auto max-medium:px-6 max-medium:pb-6' => $fullscreen])>
@if (filled($title) || filled($subtitle) || $icon)
{{-- A full-screen dialog's bar names it on a phone, so only the subtitle stays there. --}}
<div @class(['mb-4', 'max-sm:hidden' => $fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])>
<div @class(['mb-4', 'max-medium:hidden' => $fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])>
@if ($icon)
<x-livewire-material::icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" />
@endif
@if (filled($title))
<h2 id="{{ $id }}-title" @class(['type-headline-sm', 'max-sm:hidden' => $fullscreen && ! $icon])>{{ $title }}</h2>
<h2 id="{{ $id }}-title" @class(['type-headline-sm', 'max-medium:hidden' => $fullscreen && ! $icon])>{{ $title }}</h2>
@endif
@if (filled($subtitle))
<p @class(['type-body-md text-on-surface-variant', 'mt-4' => filled($title) || $icon, 'max-sm:mt-0' => $fullscreen && ! $icon])>{{ $subtitle }}</p>
<p @class(['type-body-md text-on-surface-variant', 'mt-4' => filled($title) || $icon, 'max-medium:mt-0' => $fullscreen && ! $icon])>{{ $subtitle }}</p>
@endif
@if ($separator)
@@ -105,7 +106,7 @@
@isset($actions)
<div @class([
'flex shrink-0 flex-wrap items-center justify-end gap-2 pt-6',
'max-sm:border-t max-sm:border-outline-variant max-sm:px-6 max-sm:py-4' => $fullscreen,
'max-medium:border-t max-medium:border-outline-variant max-medium:px-6 max-medium:py-4' => $fullscreen,
])>
{{ $actions }}
</div>
@@ -1,7 +1,7 @@
{{-- M3 Expressive's flexible navigation bar: three to five destinations at the bottom of a
compact window.
<div class="fixed inset-x-0 bottom-0 z-30 sm:hidden">
<div class="fixed inset-x-0 bottom-0 z-30 medium:hidden">
<x-navigation-bar>
<x-navigation-bar-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />
<x-navigation-bar-item label="Starred" icon="star" link="/starred" />
@@ -1,4 +1,4 @@
{{-- M3 Expressive's navigation rail: destinations down the start edge of a medium or wider
{{-- M3 Expressive's navigation rail: destinations down the start edge of a `medium` or wider
window, collapsed (96px, icon over label) or expanded (icon beside label in a full-width pill).
<div class="flex min-h-dvh">
@@ -29,8 +29,12 @@
- `modal` collapsed in the layout; the menu button (or `$store.rail.show()` from anywhere)
opens it expanded over a scrim, holding focus until Escape, the scrim, the menu button or
leaving the page closes it (Compose's ModalWideNavigationRail).
- `adaptive` what `<x-app-shell>` uses: below `sm` nothing until `$store.rail.show()` slides
it in as a modal; from `sm` collapsed, opening as a modal; from `lg` collapsible.
- `adaptive` what `<x-app-shell>` uses, one rail per M3 window size class: on a compact
window (below `medium`, 600px) nothing until `$store.rail.show()` slides it in as a modal;
at `medium` (600839) collapsed in the layout, opening as a modal; at `expanded` (8401199)
a standard rail, collapsed until its menu button expands it in place; from `large` (1200)
the same standard rail, expanded to begin with. A visitor who has used the menu button keeps
that choice in both standard bands.
Slots: `brand` beside the menu button, only while expanded; `header` under it a FAB, drawn
as an extended FAB when expanded (`rail-collapsed:` below); the destinations in the default
+3 -2
View File
@@ -3,7 +3,8 @@
A `<fieldset>` whose legend is the question, so a screen reader reads the group's name with each
option, and native radios sharing a name, so the arrow keys move between them. `options` as
`['id' => …, 'name' => …, 'hint' => …]` (`'disabled' => true` greys one out), `option-value`,
`option-label` and `option-hint` to read other keys, `inline` to lay them in a row from `sm`, and
`option-label` and `option-hint` to read other keys, `inline` to lay them in a row from `medium`
(600px, M3's compact/medium boundary compact always stacks), and
`value` for the option chosen when the page loads (with `wire:model`, Livewire sets it).
Errors are read from the bag under the `wire:model` name
(resources/css/components/selection.css). --}}
@@ -36,7 +37,7 @@
<legend class="mb-3 type-label-lg text-on-surface-variant">{{ $label }}</legend>
@endif
<div @class(['grid gap-4', 'sm:flex sm:flex-wrap sm:gap-x-6' => $inline])>
<div @class(['grid gap-4', 'medium:flex medium:flex-wrap medium:gap-x-6' => $inline])>
@foreach ($options as $option)
@php($optionHintText = data_get($option, $optionHint))
@@ -34,7 +34,7 @@
<legend class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</legend>
@endif
<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
<div class="grid grid-cols-2 gap-2 medium:grid-cols-4">
@foreach ($profiles as $profile => $scheme)
<label
data-scheme-option="{{ $profile }}"
@@ -2,8 +2,10 @@
single destination in the app's own navigation.
`items`, a list of `['title' => …, 'url' => …]` with an optional `icon`, `active` and `badge`
(an item is current when `active` is true, or when its `url` is the page's). From `sm` they
are M3's secondary tabs as links, the current one underlined; below `sm`, where a row of them
(an item is current when `active` is true, or when its `url` is the page's). From `medium`
(600px) they
are M3's secondary tabs as links, the current one underlined; on a compact window, where a row
of them
never fits, a button naming the current section opens a menu of all of them, the current one
marked `aria-current="page"` and each with its badge. The same list is
rendered for both, and CSS shows one.
@@ -11,7 +13,8 @@
The page's URL is `Livewire::originalUrl()`: while a Livewire component on the page updates, the
request is Livewire's update endpoint, and comparing with it left no section lit.
A row too long for its column wraps onto a grid rather than scrolling: below `xl` five or six
A row too long for its column wraps onto a grid rather than scrolling: below `large` (1200px)
five or six
sections go 3 + 3 and seven or more go four to a row tabs that scroll hid the last sections on
a tablet. `label` names the navigation ("Sections"). Links use `wire:navigate` unless
`no-wire-navigate`. --}}
@@ -29,15 +32,15 @@
$current = collect($items)->first($isCurrent) ?? ($items[0] ?? null);
$layout = match (true) {
count($items) < 5 => 'sm:flex',
count($items) < 7 => 'sm:grid sm:grid-cols-3 xl:flex',
default => 'sm:grid sm:grid-cols-4 xl:flex',
count($items) < 5 => 'medium:flex',
count($items) < 7 => 'medium:grid medium:grid-cols-3 large:flex',
default => 'medium:grid medium:grid-cols-4 large:flex',
};
@endphp
<div {{ $attributes->class(['min-w-0']) }} data-section-nav>
@if ($current)
<div class="sm:hidden" data-section-picker>
<div class="medium:hidden" data-section-picker>
<x-livewire-material::menu :$label position="bottom-start">
<x-slot:trigger>
<button type="button" class="focus-ring flex h-12 w-[min(20rem,calc(100vw-2rem))] cursor-pointer items-center gap-3 rounded-corner-xs border border-outline px-4 text-start type-body-lg text-on-surface">
@@ -56,7 +59,7 @@
</div>
@endif
<nav aria-label="{{ $label }}" class="max-sm:hidden">
<nav aria-label="{{ $label }}" class="max-medium:hidden">
<ul data-tabs-bar data-variant="secondary" class="{{ $layout }}">
@foreach ($items as $item)
@php($on = $isCurrent($item))
@@ -19,7 +19,10 @@
(`livewire-material.rail.storage_key`, falling back to `rail.default`), and a collapsible
rail's width is CSS keyed on it (the `rail-collapsed:` variant). Set any later, a collapsed
rail would paint wide and snap shut on every load. `$store.rail` (resources/js/navigation.js)
changes it.
changes it. <html data-rail-auto> rides with it and says nothing was stored the value is only
`rail.default`, not a choice so `<x-app-shell>`'s adaptive rail can start collapsed in the
expanded class (8401199) and expanded from large, as M3 asks, while still obeying a visitor
who has chosen. `$store.rail` drops it the first time they do.
With `theme.meta` on, the browser's own chrome follows too: the `content` of every
<meta name="theme-color"> without a `media` attribute one is added to <head> when there is
@@ -72,12 +75,14 @@
var valid = function (value) { return value === 'light' || value === 'dark' || value === 'system'; };
var choice = settings.default;
var rail = settings.rail.default;
var railChosen = false;
try {
var storedRail = localStorage.getItem(settings.rail.key);
if (storedRail === 'collapsed' || storedRail === 'expanded') {
rail = storedRail;
railChosen = true;
}
var stored = localStorage.getItem(settings.key);
@@ -114,6 +119,12 @@
root.setAttribute('data-theme-choice', choice);
root.setAttribute('data-rail-key', settings.rail.key);
root.setAttribute('data-rail', rail);
if (railChosen) {
root.removeAttribute('data-rail-auto');
} else {
root.setAttribute('data-rail-auto', '');
}
apply();
media.addEventListener('change', apply);
@@ -152,7 +163,7 @@
@endif
document.addEventListener('livewire:navigating', function (event) {
var kept = ['data-scheme', 'data-theme', 'data-theme-choice', 'data-theme-key', 'data-rail', 'data-rail-key'].map(function (name) {
var kept = ['data-scheme', 'data-theme', 'data-theme-choice', 'data-theme-key', 'data-rail', 'data-rail-auto', 'data-rail-key'].map(function (name) {
return [name, root.getAttribute(name)];
});
+4 -3
View File
@@ -25,7 +25,8 @@
body-medium text, a label-large action in inverse-primary, extra-small corners, elevation 3,
48px for one line. A type draws its state icon in the inverse state colour. `position`:
`bottom` (centred, the default) or `bottom-start`. It lifts above a bottom bar through
`--material-bottom-bar`. --}}
`--material-bottom-bar`. A compact window (below `medium`, 600px) gets the full-width snackbar;
from `medium` it hugs its line length instead, as M3 asks. --}}
@props(['position' => 'bottom'])
@@ -35,7 +36,7 @@
{{ $attributes->class([
'pointer-events-none fixed inset-x-4 z-50 flex bottom-[calc(var(--material-bottom-bar,0px)+1rem)]',
'justify-center' => $position !== 'bottom-start',
'justify-start sm:start-6' => $position === 'bottom-start',
'justify-start medium:start-6' => $position === 'bottom-start',
]) }}
>
<template x-if="current">
@@ -48,7 +49,7 @@
x-on:mouseleave="resume()"
x-on:focusin="pause()"
x-on:focusout="resume()"
class="pointer-events-auto flex min-h-12 w-full max-w-[min(100%,36rem)] items-center gap-3 rounded-corner-xs bg-inverse-surface py-1.5 ps-4 pe-2 text-inverse-on-surface shadow-elevation-3 transition-[translate,opacity] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast starting:translate-y-4 starting:opacity-0 sm:w-auto sm:min-w-86"
class="pointer-events-auto flex min-h-12 w-full max-w-[min(100%,36rem)] items-center gap-3 rounded-corner-xs bg-inverse-surface py-1.5 ps-4 pe-2 text-inverse-on-surface shadow-elevation-3 transition-[translate,opacity] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast starting:translate-y-4 starting:opacity-0 medium:w-auto medium:min-w-86"
>
<template x-if="icon(current.type)">
<span class="flex shrink-0" x-bind:class="{
@@ -47,7 +47,7 @@
</head>
<body class="min-h-dvh bg-surface font-sans text-on-surface antialiased">
<main data-error-page class="mx-auto flex min-h-dvh max-w-xl flex-col items-center justify-center px-6 py-12 text-center">
<div data-error-art class="relative grid size-48 shrink-0 place-items-center sm:size-60">
<div data-error-art class="relative grid size-48 shrink-0 place-items-center medium:size-60">
<div data-error-shape class="absolute inset-0">
<x-livewire-material::shape :name="trim($__env->yieldContent('shape', 'cookie-7'))" class="size-full text-primary-container" />
</div>
@@ -55,7 +55,7 @@
<p data-error-code class="relative type-emphasized-display-lg text-on-primary-container tabular-nums">@yield('code')</p>
</div>
<h1 data-error-headline class="mt-10 type-headline-md text-balance sm:type-headline-lg">
<h1 data-error-headline class="mt-10 type-headline-md text-balance medium:type-headline-lg">
@hasSection('headline')
@yield('headline')
@else
@@ -12,8 +12,8 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-pagination class="flex items-center justify-between gap-4">
<p class="type-body-sm text-on-surface-variant">
<span class="sm:hidden">{{ __('Page :page of :pages', ['page' => $paginator->currentPage(), 'pages' => $paginator->lastPage()]) }}</span>
<span class="max-sm:hidden">{{ __(':first:last of :total', ['first' => $paginator->firstItem(), 'last' => $paginator->lastItem(), 'total' => $paginator->total()]) }}</span>
<span class="medium:hidden">{{ __('Page :page of :pages', ['page' => $paginator->currentPage(), 'pages' => $paginator->lastPage()]) }}</span>
<span class="max-medium:hidden">{{ __(':first:last of :total', ['first' => $paginator->firstItem(), 'last' => $paginator->lastItem(), 'total' => $paginator->total()]) }}</span>
</p>
<div class="flex items-center gap-1">
@@ -29,15 +29,15 @@
@foreach ($elements as $element)
@if (is_string($element))
<span class="{{ $dead }} max-sm:hidden" aria-disabled="true">{{ $element }}</span>
<span class="{{ $dead }} max-medium:hidden" aria-disabled="true">{{ $element }}</span>
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<span aria-current="page" class="{{ $step }} bg-secondary-container text-on-secondary-container max-sm:hidden">{{ $page }}</span>
<span aria-current="page" class="{{ $step }} bg-secondary-container text-on-secondary-container max-medium:hidden">{{ $page }}</span>
@else
<a href="{{ $url }}" class="{{ $live }} max-sm:hidden" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">{{ $page }}</a>
<a href="{{ $url }}" class="{{ $live }} max-medium:hidden" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">{{ $page }}</a>
@endif
@endforeach
@endif
@@ -28,8 +28,8 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" data-pagination class="flex items-center justify-between gap-4">
<p class="type-body-sm text-on-surface-variant">
<span class="sm:hidden">{{ __('Page :page of :pages', ['page' => $paginator->currentPage(), 'pages' => $paginator->lastPage()]) }}</span>
<span class="max-sm:hidden">{{ __(':first:last of :total', ['first' => $paginator->firstItem(), 'last' => $paginator->lastItem(), 'total' => $paginator->total()]) }}</span>
<span class="medium:hidden">{{ __('Page :page of :pages', ['page' => $paginator->currentPage(), 'pages' => $paginator->lastPage()]) }}</span>
<span class="max-medium:hidden">{{ __(':first:last of :total', ['first' => $paginator->firstItem(), 'last' => $paginator->lastItem(), 'total' => $paginator->total()]) }}</span>
</p>
<div class="flex items-center gap-1">
@@ -45,12 +45,12 @@
@foreach ($elements as $element)
@if (is_string($element))
<span class="{{ $dead }} max-sm:hidden" aria-disabled="true">{{ $element }}</span>
<span class="{{ $dead }} max-medium:hidden" aria-disabled="true">{{ $element }}</span>
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
<span wire:key="paginator-{{ $paginator->getPageName() }}-page{{ $page }}" class="max-sm:hidden">
<span wire:key="paginator-{{ $paginator->getPageName() }}-page{{ $page }}" class="max-medium:hidden">
@if ($page == $paginator->currentPage())
<span aria-current="page" class="{{ $step }} bg-secondary-container text-on-secondary-container">{{ $page }}</span>
@else
+2 -2
View File
@@ -1,7 +1,7 @@
@extends('livewire-material::showcase.layout')
@section('content')
<div class="mx-auto w-full max-w-6xl space-y-12 px-4 pt-4 pb-16 sm:px-6">
<div class="mx-auto w-full max-w-6xl space-y-12 pt-4 pb-16">
<div class="max-w-3xl space-y-3">
<h1 class="type-headline-lg">Livewire Material</h1>
<p class="type-title-lg">Material 3 Expressive for Laravel and Livewire.</p>
@@ -15,7 +15,7 @@
<section class="space-y-4" aria-labelledby="group-{{ \Illuminate\Support\Str::slug($group) }}">
<h2 id="group-{{ \Illuminate\Support\Str::slug($group) }}" class="type-title-lg">{{ $group }}</h2>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
<div class="grid grid-cols-1 gap-3 medium:grid-cols-2 expanded:grid-cols-3">
@foreach ($entries as $key => $entry)
<x-livewire-material::card variant="outlined" data-list-row wire:key="section-{{ $key }}">
<div class="flex items-start gap-4">
+3 -3
View File
@@ -50,7 +50,7 @@
<x-slot:top>
<x-livewire-material::app-bar variant="search">
<x-slot:navigation>
<span class="sm:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="showcase-menu" /></span>
<span class="medium:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="showcase-menu" /></span>
</x-slot:navigation>
{{-- The search looks through every section, example and component: the index is
@@ -145,8 +145,8 @@
</x-livewire-material::menu>
@endif
<span class="ms-2 me-3 max-md:hidden"><x-livewire-material::theme-toggle mode="picker" /></span>
<span class="md:hidden"><x-livewire-material::theme-toggle mode="cycle" /></span>
<span class="ms-2 me-3 max-expanded:hidden"><x-livewire-material::theme-toggle mode="picker" /></span>
<span class="expanded:hidden"><x-livewire-material::theme-toggle mode="cycle" /></span>
</x-slot:actions>
</x-livewire-material::app-bar>
</x-slot:top>
+1 -1
View File
@@ -11,7 +11,7 @@
@endphp
@section('content')
<div class="mx-auto w-full max-w-6xl px-4 pt-4 pb-16 sm:px-6">
<div class="mx-auto w-full max-w-6xl pt-4 pb-16">
<header class="mb-6 space-y-1">
<p class="type-label-lg text-on-surface-variant">{{ $sections[$section]['group'] }}</p>
<h1 class="type-headline-lg">{{ $sections[$section]['title'] }}</h1>
@@ -77,7 +77,7 @@
</x-button-group>
BLADE,
'A choice as a connected group' => <<<'BLADE'
<div x-data="{ theme: 'system', days: ['mon'] }" class="grid w-full gap-6 md:grid-cols-2">
<div x-data="{ theme: 'system', days: ['mon'] }" class="grid w-full gap-6 medium:grid-cols-2">
<x-group label="Theme" name="showcase-theme" x-model="theme" :options="[
['id' => 'light', 'name' => 'Light', 'icon' => 'light_mode'],
['id' => 'dark', 'name' => 'Dark', 'icon' => 'dark_mode'],
@@ -38,7 +38,7 @@
<x-showcase::example :$title :$code />
@endforeach
<div class="grid gap-4 lg:grid-cols-2">
<div class="grid gap-4 expanded:grid-cols-2">
@foreach (['light', 'dark'] as $theme)
<div data-theme="{{ $theme }}" class="space-y-6 rounded-corner-lg bg-surface p-4 text-on-surface">
<h3 class="type-title-md capitalize">{{ $theme }}</h3>
@@ -47,7 +47,7 @@
<div class="space-y-2">
<h4 class="type-label-lg text-on-surface-variant">{{ $group }}</h4>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
<div class="grid grid-cols-2 gap-2 medium:grid-cols-3">
@foreach ($roles as $role)
<div class="flex items-center gap-2">
<span @class(['size-8 shrink-0 rounded-corner-sm border border-outline-variant', $role])></span>
@@ -53,7 +53,7 @@
<x-alert description="Settings saved." color="success" class="w-full" />
BLADE,
'Stats' => <<<'BLADE'
<div class="grid w-full gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="grid w-full gap-4 medium:grid-cols-2 expanded:grid-cols-4">
<x-stat title="Shares" value="1,204" icon="link" description="12 this week" />
<x-stat title="Files" value="8,317" icon="description" />
<x-stat title="Stored" value="3.2 GB" icon="hard_drive" description="of 4 GB" />
@@ -1,7 +1,7 @@
@php
$examples = [
'Cards' => <<<'BLADE'
<div class="grid w-full gap-4 md:grid-cols-3">
<div class="grid w-full gap-4 expanded:grid-cols-3">
<x-card title="Filled" subtitle="surface-container-highest">
The default card.
<x-slot:actions><x-button label="Open" /></x-slot:actions>
@@ -27,7 +27,7 @@
</x-card>
BLADE,
'Lists' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<x-list dividers label="Files">
<x-list-item title="holiday-photos.zip" description="248 MB" icon="folder_zip" trailing="3 days" />
<x-list-item title="contract.pdf" overline="Password protected" description="1.2 MB · downloaded twice" icon="picture_as_pdf" trailing="1 hour" />
@@ -5,7 +5,7 @@
Five shadow levels as <code>shadow-elevation-*</code>, for what floats over content.
</p>
<div class="grid grid-cols-2 gap-6 rounded-corner-lg bg-surface-container-low p-6 sm:grid-cols-5">
<div class="grid grid-cols-2 gap-6 rounded-corner-lg bg-surface-container-low p-6 medium:grid-cols-5">
@foreach (['shadow-elevation-1', 'shadow-elevation-2', 'shadow-elevation-3', 'shadow-elevation-4', 'shadow-elevation-5'] as $shadow)
<div @class(['flex h-24 items-center justify-center rounded-corner-md bg-surface-container-high', $shadow])>
<code class="type-label-md">{{ $shadow }}</code>
@@ -1,7 +1,7 @@
@php
$examples = [
'Outlined and filled text fields' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-input label="Share name" hint="Recipients see this name" />
<x-input label="Recipient" icon="mail" type="email" placeholder="name@example.com" clearable value="anna@example.com" />
@@ -22,7 +22,7 @@
</div>
BLADE,
'Errors, required and sizes' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-input label="Email" required />
<x-field id="showcase-error" label="Expiry" :messages="['Choose a date in the future.']">
@@ -42,7 +42,7 @@
</div>
BLADE,
'Textarea and select' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-textarea label="Message" hint="Grows as you type, up to six lines" rows="2" max-rows="6" />
<x-select label="Expires after" :options="[['id' => 1, 'name' => '1 hour'], ['id' => 24, 'name' => '1 day'], ['id' => 168, 'name' => '7 days'], ['id' => 720, 'name' => '30 days', 'disabled' => true]]" />
@@ -57,7 +57,7 @@
</div>
BLADE,
'Checkboxes' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-3">
<div class="grid w-full gap-6 expanded:grid-cols-3">
<div class="grid content-start gap-4">
<x-checkbox label="Notify me on download" checked />
<x-checkbox label="Select all files" indeterminate />
@@ -75,13 +75,13 @@
</div>
BLADE,
'Radio buttons' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<x-radio label="Who can open the link" name="showcase-audience" value="password" :options="[['id' => 'anyone', 'name' => 'Anyone with the link'], ['id' => 'password', 'name' => 'Anyone with the password', 'hint' => 'Share the password separately'], ['id' => 'team', 'name' => 'My team only', 'disabled' => true]]" />
<x-radio label="Theme" name="showcase-radio-theme" value="system" inline :options="[['id' => 'light', 'name' => 'Light'], ['id' => 'dark', 'name' => 'Dark'], ['id' => 'system', 'name' => 'System']]" hint="Inline from sm" />
</div>
BLADE,
'Switches' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-3">
<div class="grid w-full gap-6 expanded:grid-cols-3">
<div class="grid content-start gap-4">
<x-toggle label="Email notifications" checked />
<x-toggle label="Weekly summary" />
@@ -99,7 +99,7 @@
</div>
BLADE,
'Choices' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-6">
<x-choices label="Days you are free" hint="Filter chips: any number" :value="[1, 3]" :options="[['id' => 1, 'name' => 'Mon'], ['id' => 2, 'name' => 'Tue'], ['id' => 3, 'name' => 'Wed'], ['id' => 4, 'name' => 'Thu'], ['id' => 5, 'name' => 'Fri'], ['id' => 6, 'name' => 'Sat', 'disabled' => true]]" />
<x-choices label="Expires after" single :value="24" :options="[['id' => 1, 'name' => '1 hour'], ['id' => 24, 'name' => '1 day'], ['id' => 168, 'name' => '7 days']]" />
@@ -112,7 +112,7 @@
</div>
BLADE,
'Search' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div x-data="{ query: '' }">
<x-search x-model="query" placeholder="Search shares">
<x-list>
@@ -1,7 +1,7 @@
@php
$examples = [
'Docked' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ expires: '{{ now()->addWeek()->format('Y-m-d') }}', starts: null }">
<div class="grid w-full gap-6 medium:grid-cols-2" x-data="{ expires: '{{ now()->addWeek()->format('Y-m-d') }}', starts: null }">
<div class="grid content-start gap-4">
<x-datepicker label="Expires on" clearable x-model="expires" hint="Type a date or pick one" />
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(expires)"></code></p>
@@ -14,7 +14,7 @@
</div>
BLADE,
'Modal and modal input' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ birthday: '1990-05-17', delivery: null }">
<div class="grid w-full gap-6 medium:grid-cols-2" x-data="{ birthday: '1990-05-17', delivery: null }">
<div class="grid content-start gap-4">
<x-datepicker label="Birthday" mode="modal" x-model="birthday" :max="now()" />
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(birthday)"></code></p>
@@ -27,7 +27,7 @@
</div>
BLADE,
'Range' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ trip: { start: '{{ now()->addDays(3)->format('Y-m-d') }}', end: '{{ now()->addDays(9)->format('Y-m-d') }}' }, leave: { start: null, end: null } }">
<div class="grid w-full gap-6 medium:grid-cols-2" x-data="{ trip: { start: '{{ now()->addDays(3)->format('Y-m-d') }}', end: '{{ now()->addDays(9)->format('Y-m-d') }}' }, leave: { start: null, end: null } }">
<div class="grid content-start gap-4">
<x-datepicker label="Trip" range x-model="trip" />
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(trip)"></code></p>
@@ -40,7 +40,7 @@
</div>
BLADE,
'First day of the week and format' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2" x-data="{ race: '{{ now()->addMonth()->format('Y-m-d') }}', block: { start: null, end: null } }">
<div class="grid w-full gap-6 medium:grid-cols-2" x-data="{ race: '{{ now()->addMonth()->format('Y-m-d') }}', block: { start: null, end: null } }">
<div class="grid content-start gap-4">
<x-datepicker label="Race day" week-start="0" format="yyyy-MM-dd" x-model="race" hint="Weeks start on Sunday; typed year first" />
<p class="type-body-sm text-on-surface-variant">Bound value: <code x-text="JSON.stringify(race)"></code></p>
@@ -53,7 +53,7 @@
</div>
BLADE,
'Limits, errors and states' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-datepicker label="Within the next 30 days" :min="now()" :max="now()->addDays(30)" hint="Days outside are disabled" />
<x-datepicker label="Required" required />
@@ -5,7 +5,7 @@
The corner scale as <code>rounded-corner-*</code>, and M3 Expressive's shapes as <code>&lt;x-shape name="" /&gt;</code>.
</p>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-5">
<div class="grid grid-cols-2 gap-4 medium:grid-cols-5">
@foreach (['rounded-corner-none', 'rounded-corner-xs', 'rounded-corner-sm', 'rounded-corner-md', 'rounded-corner-lg', 'rounded-corner-lg-increased', 'rounded-corner-xl', 'rounded-corner-xl-increased', 'rounded-corner-xxl', 'rounded-corner-full'] as $corner)
<div class="space-y-2">
<div @class(['h-20 bg-primary-container', $corner])></div>
@@ -14,7 +14,7 @@
@endforeach
</div>
<div class="grid grid-cols-3 gap-4 sm:grid-cols-5 lg:grid-cols-7">
<div class="grid grid-cols-3 gap-4 medium:grid-cols-5 expanded:grid-cols-7">
@foreach (\NoNameWeb\LivewireMaterial\Support\SvgFile::shapeNames() as $shape)
<div class="space-y-2 text-center">
<x-livewire-material::shape :name="$shape" class="mx-auto size-20 text-secondary-container" />
@@ -1,7 +1,7 @@
@php
$examples = [
'Standard, discrete and disabled' => <<<'BLADE'
<div class="grid w-full gap-8 md:grid-cols-3">
<div class="grid w-full gap-8 expanded:grid-cols-3">
<x-slider label="Volume" value="40" hint="Drag the handle or use the arrow keys" />
<x-slider label="Quality" value="6" :max="10" ticks />
<x-slider label="Locked" value="70" disabled />
@@ -17,20 +17,20 @@
</div>
BLADE,
'Range and centred' => <<<'BLADE'
<div class="grid w-full gap-8 md:grid-cols-2">
<div class="grid w-full gap-8 medium:grid-cols-2">
<x-slider label="Price" :value="[20, 60]" range ticks :step="10" hint="Handles never cross" />
<x-slider label="Balance" value="15" :min="-50" :max="50" centered />
</div>
BLADE,
'Colours and value labels' => <<<'BLADE'
<div class="grid w-full gap-8 md:grid-cols-3">
<div class="grid w-full gap-8 expanded:grid-cols-3">
<x-slider label="Always labelled" value="55" color="secondary" value-label="always" size="sm" />
<x-slider label="Tertiary, no label" value="35" color="tertiary" value-label="never" size="sm" />
<x-slider label="Error" :value="[30, 70]" color="error" range size="sm" />
</div>
BLADE,
'Bound with x-model' => <<<'BLADE'
<div x-data="{ volume: 25, price: [100, 300] }" class="grid w-full gap-8 md:grid-cols-2">
<div x-data="{ volume: 25, price: [100, 300] }" class="grid w-full gap-8 medium:grid-cols-2">
<div class="space-y-2">
<x-slider label="Bound volume" x-model="volume" />
<p class="type-body-md text-on-surface-variant">volume: <span data-bound="volume" x-text="volume"></span> <x-button label="Set 80" x-on:click="volume = 80" /></p>
@@ -1,7 +1,7 @@
@php
$examples = [
'12 and 24 hours, outlined and filled' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-timepicker label="Meeting starts" value="09:30" hint="The locale's clock" />
<x-timepicker label="Pick-up" value="17:15" format="12" icon="local_shipping" clearable />
@@ -16,7 +16,7 @@
</div>
BLADE,
'Steps and limits' => <<<'BLADE'
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-timepicker label="Appointment" value="10:00" format="24" step="15" min="08:00" max="17:30" hint="Quarter hours from 08:00 to 17:30" />
<x-timepicker label="Night shift starts" value="23:00" format="24" min="22:00" max="06:00" hint="From 22:00, across midnight, to 06:00" />
@@ -27,7 +27,7 @@
<div class="divide-y divide-divider rounded-corner-lg bg-surface-container">
@foreach ($styles as [$regular, $emphasized])
<div class="grid gap-2 p-4 md:grid-cols-[12rem_1fr_1fr] md:items-baseline">
<div class="grid gap-2 p-4 expanded:grid-cols-[12rem_1fr_1fr] expanded:items-baseline">
<code class="type-label-md text-on-surface-variant">{{ $regular }}</code>
<p @class([$regular, 'truncate'])>Share files, safely</p>
<p @class([$emphasized, 'truncate'])>Share files, safely</p>
+4 -4
View File
@@ -59,14 +59,14 @@
</x-slot:actions>
<x-slot:top>
<header class="sticky top-0 z-20 flex h-16 items-center gap-1 bg-surface px-1 pt-[var(--material-safe-top,env(safe-area-inset-top))] sm:px-4">
<span class="sm:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="shell-menu" /></span>
<h1 class="min-w-0 flex-1 truncate px-3 type-title-lg sm:px-0">{{ $current['title'] }}</h1>
<header class="sticky top-0 z-20 flex h-16 items-center gap-1 bg-surface px-1 pt-[var(--material-safe-top,env(safe-area-inset-top))] medium:px-4">
<span class="medium:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="shell-menu" /></span>
<h1 class="min-w-0 flex-1 truncate px-3 type-title-lg medium:px-0">{{ $current['title'] }}</h1>
<x-livewire-material::button icon="search" tooltip="Search" />
</header>
</x-slot:top>
<div class="mx-auto w-full max-w-5xl space-y-4 px-4 pb-6 sm:px-6">
<div class="mx-auto w-full max-w-5xl space-y-4 pb-6">
<p class="type-body-md text-on-surface-variant" data-test="shell-page">This is the {{ strtolower($current['title']) }} page.</p>
<x-livewire-material::list segmented>