From b1642aed1cb8fb400ef3cf97115a916abb568f8f Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 04:03:54 +0200 Subject: [PATCH] Key the views on M3's window size classes, not Tailwind's Plan step 15. Every `sm:`/`md:`/`lg:`/`xl:` in the components, the showcase (sections, layout, shell) and the pagination and error views becomes the M3 window size class the audits' breakpoint maps prescribe: the full-screen dialog, the FAB, the snackbar's width, `radio inline`, the section picker and the page numbers move from 640px to `medium` (600px, M3's compact boundary, N-07 / C-07 / IN-28); `button responsive` moves from 1024px to `expanded` (840px); `section-nav`'s grid-to-row switch from 1280px to `large` (1200px, N-07); and `drawer pane` from 1280px to `expanded`, where M3 shows two panes for list-detail (C-07). The pane's default width is now 360dp, M3's fixed supporting pane at expanded. Showcase example grids take two columns from `medium` and three or more from `expanded`; the showcase's own theme picker swaps at `expanded`, the nearest class boundary to the 768px it used (no M3 rule applies to it). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/views/components/button.blade.php | 14 ++++--- resources/views/components/drawer.blade.php | 40 ++++++++++--------- resources/views/components/modal.blade.php | 19 ++++----- .../views/components/navigation-bar.blade.php | 2 +- resources/views/components/radio.blade.php | 5 ++- .../views/components/scheme-picker.blade.php | 2 +- .../views/components/section-nav.blade.php | 19 +++++---- resources/views/components/toast.blade.php | 7 ++-- .../error-pages/errors/minimal.blade.php | 4 +- .../pagination/laravel/tailwind.blade.php | 10 ++--- .../pagination/livewire/tailwind.blade.php | 8 ++-- resources/views/showcase/index.blade.php | 2 +- resources/views/showcase/layout.blade.php | 6 +-- .../views/showcase/sections/buttons.blade.php | 2 +- .../views/showcase/sections/colour.blade.php | 4 +- .../showcase/sections/communication.blade.php | 2 +- .../showcase/sections/containment.blade.php | 4 +- .../showcase/sections/elevation.blade.php | 2 +- .../views/showcase/sections/fields.blade.php | 16 ++++---- .../views/showcase/sections/pickers.blade.php | 10 ++--- .../views/showcase/sections/shape.blade.php | 4 +- .../views/showcase/sections/sliders.blade.php | 8 ++-- .../showcase/sections/timepickers.blade.php | 4 +- .../views/showcase/sections/type.blade.php | 2 +- resources/views/showcase/shell.blade.php | 6 +-- 25 files changed, 107 insertions(+), 95 deletions(-) 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..ed7cb436 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/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)