diff --git a/README.md b/README.md index df5724f9..afbbcbda 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ import '../../vendor/nonameweb/livewire-material/resources/js/material.js' Do not install Alpine separately; Livewire provides it. +Importing the package's CSS replaces parts of Tailwind's theme with M3's, so some default utilities no longer compile. Breakpoints are the most visible: they are M3's window size classes and only those — `medium:` 600px, `expanded:` 840px, `large:` 1200px and `extra-large:` 1600px, with `max-medium:` and friends for "below", compact being everything under `medium`. Tailwind's `sm:`…`2xl:` are cleared, so an `sm:grid-cols-2` left over from another project compiles to nothing; rewrite it as `medium:grid-cols-2`. The same goes for the default radius, shadow, text-size, weight, leading, tracking and easing scales, which the M3 sets (`rounded-corner-*`, `shadow-elevation-*`, `type-*`, `ease-spatial-*`) replace. Scripts that need a window size class import `from()` / `upTo()` from the package's `resources/js/breakpoints.js` rather than writing their own media query. + ### Layout The theme script goes in ``, before `@vite`, so the page paints in the visitor's theme: diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 013d31c0..d6c7dea2 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -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 `` 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 `` 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 `` (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 `` (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 ``, 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 @@ -279,12 +280,12 @@ 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. `color` `primary`/`secondary`/`tertiary`, drawn in the container, or `variant="filled"`. It does not position itself; wrap it (`
`). `link`, `external`, `disabled`, `type`. +`` — `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 (`
` — M3's 16dp margin, 24dp from `large`). `link`, `external`, `disabled`, `type`. ### ``, `` ```blade -
+
@@ -461,11 +462,11 @@ An M3 dialog on native ``. Bind with `wire:model` to a flag or an id; cl ``` -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. ### `` -An M3 side sheet, bound like ``; `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 `
`. 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 ``; `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 `
`. 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. ### `` @@ -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 ### `` -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 @@ -665,7 +666,16 @@ The docked view overlaps what is under it; never place a search inside an elemen ### `` -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 `
` behind a skip link, and the snackbar host (do not add another ``). It needs `` in ``. +The adaptive app shell, a whole layout's body: one navigation per M3 window size class, the page as `
` behind a skip link, and the snackbar host (do not add another ``). It needs `` in ``. + +| 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` 600–839 | collapsed rail (96px) in the layout, no bar; its menu button opens it expanded over a scrim | 24px | +| Expanded | `expanded` 840–1199 | 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 + {{ $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`: `` 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. ### ``, `` M3 Expressive's flexible navigation bar, for three to five destinations. It does not position itself; wrap it (`` does): ```blade -
+
@@ -735,11 +747,11 @@ 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: 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` (``'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: `…expanded only…`, ``. Put the variant on a wrapper, never on a component. Nothing that shows while collapsed may be wider than 96px. - ``: 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), `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`). ### `` @@ -779,7 +791,7 @@ M3 tabs with a server-rendered tablist (arrow keys, Home/End, disabled tabs skip ### `` -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`. ### `` @@ -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: `` 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. diff --git a/resources/css/components/navigation.css b/resources/css/components/navigation.css index 1ca4dc5a..a2f1db1b 100644 --- a/resources/css/components/navigation.css +++ b/resources/css/components/navigation.css @@ -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 (set before the first paint by - * ), or a window under `lg` for the adaptive rail. A rail item is written once + * ), 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 * (``). + * + * 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 (840–1199): 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 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; diff --git a/resources/css/components/timepicker.css b/resources/css/components/timepicker.css index f08c012b..414a4b48 100644 --- a/resources/css/components/timepicker.css +++ b/resources/css/components/timepicker.css @@ -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; } diff --git a/resources/js/datepicker.js b/resources/js/datepicker.js index 232c7abb..84c57f86 100644 --- a/resources/js/datepicker.js +++ b/resources/js/datepicker.js @@ -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)) diff --git a/resources/js/navigation.js b/resources/js/navigation.js index ffced60b..51000f42 100644 --- a/resources/js/navigation.js +++ b/resources/js/navigation.js @@ -5,6 +5,9 @@ * localStorage. has already applied it before the first paint as * , 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 (840–1199), 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) } }, })) diff --git a/resources/js/search.js b/resources/js/search.js index 4d8aec73..56fe0603 100644 --- a/resources/js/search.js +++ b/resources/js/search.js @@ -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)) diff --git a/resources/views/components/app-shell.blade.php b/resources/views/components/app-shell.blade.php index be0c2900..a95c2624 100644 --- a/resources/views/components/app-shell.blade.php +++ b/resources/views/components/app-shell.blade.php @@ -11,13 +11,31 @@ …the page… - - 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`: - ``. - - `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`, ). + 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`: + ``. + - **Medium** (600–839): 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** (840–1199): 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`, ), 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`: `` 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 `
` with `wire:transition.navigate`, behind a skip link that is the first thing a keyboard reaches. The snackbar host (``) 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 @@
$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(), ]) > {{ $top ?? '' }} -
+
{{ $slot }}
@if ($barItems->isNotEmpty()) -
+
@foreach ($barItems as $item) diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php index d8eb90b3..dd8d75c5 100644 --- a/resources/views/components/button.blade.php +++ b/resources/views/components/button.blade.php @@ -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) - + @endif @unless ($iconOnly) - $responsive])>{{ $label ?? $slot }} + $responsive])>{{ $label ?? $slot }} @endunless @if ($iconRight) diff --git a/resources/views/components/drawer.blade.php b/resources/views/components/drawer.blade.php index e83ad5a0..e7847ba4 100644 --- a/resources/views/components/drawer.blade.php +++ b/resources/views/components/drawer.blade.php @@ -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" >
@@ -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'), ]) }} > diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php index 1295cc08..fd9d0d66 100644 --- a/resources/views/components/modal.blade.php +++ b/resources/views/components/modal.blade.php @@ -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'), ]) >
$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) -
+
@if (filled($title)) @@ -77,20 +78,20 @@
@endif -
$fullscreen])> +
$fullscreen])> @if (filled($title) || filled($subtitle) || $icon) {{-- A full-screen dialog's bar names it on a phone, so only the subtitle stays there. --}} -
$fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])> +
$fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])> @if ($icon) @endif @if (filled($title)) -

$fullscreen && ! $icon])>{{ $title }}

+

$fullscreen && ! $icon])>{{ $title }}

@endif @if (filled($subtitle)) -

filled($title) || $icon, 'max-sm:mt-0' => $fullscreen && ! $icon])>{{ $subtitle }}

+

filled($title) || $icon, 'max-medium:mt-0' => $fullscreen && ! $icon])>{{ $subtitle }}

@endif @if ($separator) @@ -105,7 +106,7 @@ @isset($actions)
$fullscreen, + 'max-medium:border-t max-medium:border-outline-variant max-medium:px-6 max-medium:py-4' => $fullscreen, ])> {{ $actions }}
diff --git a/resources/views/components/navigation-bar.blade.php b/resources/views/components/navigation-bar.blade.php index 874c64af..2e763bca 100644 --- a/resources/views/components/navigation-bar.blade.php +++ b/resources/views/components/navigation-bar.blade.php @@ -1,7 +1,7 @@ {{-- M3 Expressive's flexible navigation bar: three to five destinations at the bottom of a compact window. -
+
diff --git a/resources/views/components/navigation-rail.blade.php b/resources/views/components/navigation-rail.blade.php index 8eece1c3..469d530c 100644 --- a/resources/views/components/navigation-rail.blade.php +++ b/resources/views/components/navigation-rail.blade.php @@ -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).
@@ -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 `` 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 `` 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` (600–839) collapsed in the layout, opening as a modal; at `expanded` (840–1199) + 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 diff --git a/resources/views/components/radio.blade.php b/resources/views/components/radio.blade.php index 0b7a6652..2162c963 100644 --- a/resources/views/components/radio.blade.php +++ b/resources/views/components/radio.blade.php @@ -3,7 +3,8 @@ A `
` 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 @@ {{ $label }} @endif -
$inline])> +
$inline])> @foreach ($options as $option) @php($optionHintText = data_get($option, $optionHint)) diff --git a/resources/views/components/scheme-picker.blade.php b/resources/views/components/scheme-picker.blade.php index 87094a69..2102b45b 100644 --- a/resources/views/components/scheme-picker.blade.php +++ b/resources/views/components/scheme-picker.blade.php @@ -34,7 +34,7 @@ {{ $label }} @endif -
+
@foreach ($profiles as $profile => $scheme)