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
+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="{