Merge branch 'worktree-agent-ab53265ffbd837742'
This commit is contained in:
@@ -38,10 +38,12 @@
|
||||
type="button"
|
||||
aria-label="{{ $label }}"
|
||||
data-account-menu
|
||||
class="focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-corner-full bg-primary-container type-label-lg text-on-primary-container"
|
||||
class="state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full bg-primary-container type-label-lg text-on-primary-container"
|
||||
>
|
||||
@if ($image)
|
||||
<img src="{{ $avatar }}" alt="" class="size-full object-cover" />
|
||||
{{-- Behind the state layer (`state-layer` paints its ::before at z-index -1), so an
|
||||
avatar image still shows the hover and pressed states over it. --}}
|
||||
<img src="{{ $avatar }}" alt="" class="relative -z-20 size-full rounded-corner-full object-cover" />
|
||||
@elseif (filled($initials))
|
||||
{{ $initials }}
|
||||
@else
|
||||
|
||||
@@ -45,13 +45,23 @@
|
||||
it), `bar` (`false` keeps it out of the bottom bar; M3 wants three to five there) and
|
||||
`navigate` (`false` for a full page load instead of `wire:navigate`).
|
||||
|
||||
Slots, each rendered once: `brand` (beside the rail's menu button while it is expanded),
|
||||
`rail-header` (under it: a FAB — see `<x-navigation-rail>` for its two shapes), `rail-footer`
|
||||
(at the foot of the rail: footer destinations, an account), `actions` (a row of icon buttons at
|
||||
the very foot, stacked when the rail is collapsed: a theme toggle, sign out), `top` (the app
|
||||
bar, above the page at every width) and the page itself. The rail is one element at every
|
||||
width, so what is in it is also in the modal rail a phone opens. `label` names both navigation
|
||||
landmarks ("Main"); `rail-width` is the expanded rail's width.
|
||||
Slots, each rendered once: `banner` (a bar across the whole window, above the rail and the
|
||||
page — M3's scaffold is bars, then rails, then panes), `brand` (beside the rail's menu button
|
||||
while it is expanded), `rail-header` (under it: one `<x-fab label="…" icon="…">`, which the
|
||||
rail morphs between a FAB and an extended FAB as it opens), `rail-footer` (at the foot of the
|
||||
rail: footer destinations, an account), `actions`
|
||||
(a row of icon buttons at the very foot, stacked when the rail is collapsed: a theme toggle,
|
||||
sign out), `top` (the page's own bar, above the page and *beside* the rail) and the page
|
||||
itself. The rail is one element at every width, so what is in it is also in the modal rail a
|
||||
phone opens. `label` names both navigation landmarks ("Main"); `rail-width` is the expanded
|
||||
rail's width.
|
||||
|
||||
`banner` or `top` is a decision about what the bar belongs to: an application-wide bar — one
|
||||
search, one account menu, the same on every page — spans the window and the rail starts under
|
||||
it; a bar that titles the page belongs to the page, beside the rail. Put an app bar in one or
|
||||
the other, never both. A banner that pins itself to the top of the window says how tall it is
|
||||
— `style="--material-banner: 4rem"` on `<x-app-shell>` — so the rail sticks under it instead
|
||||
of behind it.
|
||||
|
||||
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;
|
||||
@@ -102,7 +112,7 @@
|
||||
<div
|
||||
data-app-shell
|
||||
@class([
|
||||
'min-h-dvh bg-surface text-on-surface [--material-margin:1rem] medium:flex medium:[--material-margin:1.5rem]',
|
||||
'flex min-h-dvh flex-col bg-surface text-on-surface [--material-margin:1rem] 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(),
|
||||
])
|
||||
>
|
||||
@@ -112,48 +122,54 @@
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:start-4 focus:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1rem)] focus:z-[60] focus:rounded-corner-full focus:bg-inverse-surface focus:px-4 focus:py-2 focus:type-label-lg focus:text-inverse-on-surface focus:shadow-elevation-3 focus:outline-none"
|
||||
>{{ __('Skip to content') }}</a>
|
||||
|
||||
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
|
||||
@isset($brand)
|
||||
<x-slot:brand>{{ $brand }}</x-slot:brand>
|
||||
@endisset
|
||||
@isset($banner)
|
||||
<div data-app-shell-banner class="shrink-0">{{ $banner }}</div>
|
||||
@endisset
|
||||
|
||||
@isset($railHeader)
|
||||
<x-slot:header>{{ $railHeader }}</x-slot:header>
|
||||
@endisset
|
||||
<div class="flex-1 medium:flex">
|
||||
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
|
||||
@isset($brand)
|
||||
<x-slot:brand>{{ $brand }}</x-slot:brand>
|
||||
@endisset
|
||||
|
||||
@foreach ($groups as $group)
|
||||
@if ($group->first()['section'] !== null)
|
||||
<x-livewire-material::navigation-rail-section :label="$group->first()['section']">
|
||||
@isset($railHeader)
|
||||
<x-slot:header>{{ $railHeader }}</x-slot:header>
|
||||
@endisset
|
||||
|
||||
@foreach ($groups as $group)
|
||||
@if ($group->first()['section'] !== null)
|
||||
<x-livewire-material::navigation-rail-section :label="$group->first()['section']">
|
||||
@foreach ($group as $item)
|
||||
<x-livewire-material::navigation-rail-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']" />
|
||||
@endforeach
|
||||
</x-livewire-material::navigation-rail-section>
|
||||
@else
|
||||
@foreach ($group as $item)
|
||||
<x-livewire-material::navigation-rail-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']" />
|
||||
@endforeach
|
||||
</x-livewire-material::navigation-rail-section>
|
||||
@else
|
||||
@foreach ($group as $item)
|
||||
<x-livewire-material::navigation-rail-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']" />
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
@if (isset($railFooter) || isset($actions))
|
||||
<x-slot:footer>
|
||||
{{ $railFooter ?? '' }}
|
||||
|
||||
@isset($actions)
|
||||
<div data-app-shell-actions class="flex items-center gap-1 px-5 pt-2 rail-collapsed:flex-col">
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endisset
|
||||
</x-slot:footer>
|
||||
@endif
|
||||
@endforeach
|
||||
</x-livewire-material::navigation-rail>
|
||||
|
||||
@if (isset($railFooter) || isset($actions))
|
||||
<x-slot:footer>
|
||||
{{ $railFooter ?? '' }}
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
{{ $top ?? '' }}
|
||||
|
||||
@isset($actions)
|
||||
<div data-app-shell-actions class="flex items-center gap-1 px-5 pt-2 rail-collapsed:flex-col">
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endisset
|
||||
</x-slot:footer>
|
||||
@endif
|
||||
</x-livewire-material::navigation-rail>
|
||||
|
||||
<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 px-(--material-margin) outline-none max-expanded:overflow-x-clip max-medium:pb-(--material-bottom-bar)">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@if ($barItems->isNotEmpty())
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<x-navigation-rail mode="collapsible">
|
||||
<x-slot:brand><span class="type-title-lg">Mail</span></x-slot:brand>
|
||||
<x-slot:header>
|
||||
<x-fab icon="edit" tooltip-right="Compose" />
|
||||
<x-fab label="Compose" icon="edit" />
|
||||
</x-slot:header>
|
||||
|
||||
<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />
|
||||
@@ -36,21 +36,30 @@
|
||||
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
|
||||
slot, which alone scroll when the window is too short; `footer`, pinned to the foot. Header and
|
||||
footer never scroll, so nothing in them is cut off by the scroller's edge.
|
||||
Slots: `brand` beside the menu button, only while expanded; `header` under it — one
|
||||
`<x-fab label="…" icon="…">`, which the rail morphs: the label's width springs open and shut
|
||||
with the rail, so the FAB becomes an extended FAB and back rather than one being swapped for
|
||||
the other, and its label names it at both widths. It also rests flat, because M3 puts a FAB
|
||||
nested in another component at elevation 0, not the 3 a standalone one has. Then the
|
||||
destinations in the default slot, which alone scroll when the window is too short, and
|
||||
`footer`, pinned to the foot. Header and footer never scroll, so nothing in them is cut off by
|
||||
the scroller's edge.
|
||||
|
||||
Anything inside can take both shapes with the `rail-collapsed:` variant, which applies while
|
||||
the rail is drawn collapsed for whatever reason:
|
||||
`<span class="rail-collapsed:hidden"><x-fab label="Compose" icon="edit" /></span>`
|
||||
`<span class="hidden rail-collapsed:inline-flex"><x-fab icon="edit" tooltip-right="Compose" /></span>`.
|
||||
Anything else inside can take both shapes with the `rail-collapsed:` variant, which applies
|
||||
while the rail is drawn collapsed for whatever reason:
|
||||
`<span class="rail-collapsed:hidden">…only while expanded…</span>`
|
||||
`<span class="hidden rail-collapsed:inline-flex">…only while collapsed…</span>`.
|
||||
Nothing that shows while collapsed may be wider than 96px.
|
||||
|
||||
Props: `label` names the landmark ("Main"); `width` is the expanded width (`16rem`, held
|
||||
between M3's 220 and 360dp); `menu` shows the menu button (by default for `collapsible`,
|
||||
`modal` and `adaptive`). The rail does not scroll with the page: in a flex row it sticks to
|
||||
the top of the viewport, as tall as the viewport at most.
|
||||
`modal` and `adaptive`); `divider` draws M3's optional vertical divider on the edge the page
|
||||
is on — which is also what M3 asks for when a page scrolls underneath a fixed rail; `fill`
|
||||
(`false`) drops the container colour for a transparent rail over the page's own background,
|
||||
which M3 allows as long as the items keep a 3:1 contrast against what is behind them. A rail
|
||||
open over a scrim keeps its fill and drops the divider whatever those say: it is a surface
|
||||
over the page then. The rail does not scroll with the page: in a flex row it sticks to the top
|
||||
of the viewport, as tall as the viewport at most.
|
||||
|
||||
Values from androidx Compose Material 3 (Apache-2.0), androidx-main
|
||||
27cf9a7d5788aa0f5f2d8b6699ce279560daf326: NavigationRailCollapsedTokens.kt,
|
||||
@@ -66,6 +75,8 @@
|
||||
'label' => null,
|
||||
'width' => '16rem',
|
||||
'menu' => null,
|
||||
'divider' => false,
|
||||
'fill' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -78,6 +89,8 @@
|
||||
|
||||
<div
|
||||
data-navigation-rail="{{ $mode }}"
|
||||
@if ($divider) data-divider @endif
|
||||
@unless ($fill) data-fill="false" @endunless
|
||||
@if ($interactive)
|
||||
x-data="materialNavigationRail('{{ $mode }}')"
|
||||
x-bind:data-open="open"
|
||||
@@ -109,7 +122,7 @@
|
||||
x-on:click="menu()"
|
||||
x-bind:aria-label="expanded ? @js(__('Collapse navigation')) : @js(__('Expand navigation'))"
|
||||
x-bind:aria-expanded="expanded.toString()"
|
||||
class="state-layer focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant after:absolute after:top-1/2 after:left-1/2 after:size-12 after:-translate-x-1/2 after:-translate-y-1/2"
|
||||
class="state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant"
|
||||
>
|
||||
<span class="contents rail-collapsed:hidden"><x-livewire-material::icon name="menu_open" class="size-6" /></span>
|
||||
<span class="hidden rail-collapsed:contents"><x-livewire-material::icon name="menu" class="size-6" /></span>
|
||||
|
||||
@@ -13,10 +13,15 @@
|
||||
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 `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
|
||||
Up to four sections share the row as fixed tabs. From five the row is M3's scrollable tab bar
|
||||
— each tab as wide as its own label, the set scrolling sideways, offset 52dp from the leading
|
||||
edge so it reads as scrollable — which is M3's own answer to a row that will not fit, and the
|
||||
one its accessibility page blesses ("horizontal scrolling tabs meet accessibility requirements
|
||||
because they need to increase in width to respond to label text without affecting the
|
||||
layout"). An earlier version wrapped them onto a grid instead, which left the bar's divider
|
||||
under the last row only and stranded the upper rows' indicators against nothing.
|
||||
|
||||
`label` names the navigation ("Sections"). Links use `wire:navigate` unless
|
||||
`no-wire-navigate`. --}}
|
||||
|
||||
@props([
|
||||
@@ -31,11 +36,8 @@
|
||||
$isCurrent = fn (array $item): bool => ($item['active'] ?? false) || (filled($item['url'] ?? null) && $page === url($item['url']));
|
||||
$current = collect($items)->first($isCurrent) ?? ($items[0] ?? null);
|
||||
|
||||
$layout = match (true) {
|
||||
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',
|
||||
};
|
||||
// Four fit a row at the widths this bar is used at; from five they are M3's scrollable tabs.
|
||||
$scrollable = count($items) >= 5;
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->class(['min-w-0']) }} data-section-nav>
|
||||
@@ -45,7 +47,7 @@
|
||||
<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">
|
||||
@isset($current['icon'])
|
||||
<x-livewire-material::icon :name="$current['icon']" class="size-5 text-on-surface-variant" />
|
||||
<x-livewire-material::icon :name="$current['icon']" optical="20" class="size-5 text-on-surface-variant" />
|
||||
@endisset
|
||||
<span class="min-w-0 flex-1 truncate">{{ $current['title'] }}</span>
|
||||
<x-livewire-material::icon name="arrow_drop_down" class="size-6 text-on-surface-variant" />
|
||||
@@ -60,7 +62,7 @@
|
||||
@endif
|
||||
|
||||
<nav aria-label="{{ $label }}" class="max-medium:hidden">
|
||||
<ul data-tabs-bar data-variant="secondary" class="{{ $layout }}">
|
||||
<ul data-tabs-bar data-variant="secondary" @if ($scrollable) data-scrollable @endif>
|
||||
@foreach ($items as $item)
|
||||
@php($on = $isCurrent($item))
|
||||
|
||||
@@ -73,7 +75,7 @@
|
||||
>
|
||||
<span data-tab-content>
|
||||
@isset($item['icon'])
|
||||
<x-livewire-material::icon :name="$item['icon']" :filled="$on" class="size-5" />
|
||||
<x-livewire-material::icon :name="$item['icon']" :filled="$on" optical="20" class="size-5" />
|
||||
@endisset
|
||||
<span class="truncate">{{ $item['title'] }}</span>
|
||||
@if (filled($item['badge'] ?? null))
|
||||
|
||||
@@ -1,17 +1,43 @@
|
||||
{{-- One tab's panel, inside `<x-tabs>`, which draws the bar from its own `tabs` list. Shown while
|
||||
its `name` is the chosen one; every panel is rendered, so switching never waits on the server.
|
||||
`class` lands on the panel (it has `pt-6` above its content by default). --}}
|
||||
`class` lands on the panel (it has `pt-6` above its content by default).
|
||||
|
||||
The id and the `hidden` panels come from the server, not from Alpine: until Alpine boots, every
|
||||
panel would otherwise be on screen at once — four `role="tabpanel"` regions for a screen reader
|
||||
to walk — and the tab buttons' `aria-controls` would point at nothing. `<x-tabs>` renders this
|
||||
slot before its own view, so it cannot hand the panel anything; the panel reads the attributes
|
||||
written on `<x-tabs>` off the component stack (Blade's `@aware` mechanism) and repeats the two
|
||||
lines tabs.blade.php runs on them. The two must agree, so change them together. --}}
|
||||
|
||||
@props([
|
||||
'name',
|
||||
])
|
||||
|
||||
@php
|
||||
$aware = fn (string $key, $default = null) => $__env->getConsumableComponentData($key, $default);
|
||||
|
||||
$tabs = $aware('tabs', []);
|
||||
// `wire:model` is a key, not a prop, and its modifiers are part of the key: the spellings that
|
||||
// make sense on a tab set. An exotic one only means the panel falls back to the first tab.
|
||||
$model = collect(['wire:model', 'wire:model.live', 'wire:model.blur', 'wire:model.change', 'wire:model.lazy'])
|
||||
->map(fn (string $key) => $aware($key))
|
||||
->first(fn ($value): bool => filled($value));
|
||||
|
||||
$tabsId = $aware('id') ?? 'tabs-'.substr(md5($model.'|'.implode('|', array_column($tabs, 'name'))), 0, 8);
|
||||
|
||||
$initial = $aware('selected') ?? (collect($tabs)->first(fn (array $tab): bool => ! ($tab['disabled'] ?? false))['name'] ?? null);
|
||||
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null && filled(data_get($component, $model))) {
|
||||
$initial = data_get($component, $model);
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-show="selected === @js($name)"
|
||||
role="tabpanel"
|
||||
x-bind:id="tabsId + '-' + @js($name) + '-panel'"
|
||||
x-bind:aria-labelledby="tabsId + '-' + @js($name)"
|
||||
id="{{ $tabsId }}-{{ $name }}-panel"
|
||||
aria-labelledby="{{ $tabsId }}-{{ $name }}"
|
||||
tabindex="0"
|
||||
@if ((string) $name !== (string) $initial) style="display: none" @endif
|
||||
{{ $attributes->class(['pt-6 outline-none']) }}
|
||||
>
|
||||
{{ $slot }}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
The bar is rendered on the server from `tabs`, a list of `['name' => …, 'label' => …]` with an
|
||||
optional `icon`, `badge` and `disabled` — so it arrives with the page, never a moment after it.
|
||||
The panels are `<x-tab name="…">` in the slot; every panel is rendered, so switching never waits
|
||||
on the server.
|
||||
on the server, and each arrives with its id and with every panel but the chosen one already
|
||||
hidden, so nothing flashes and no screen reader meets four panels at once.
|
||||
|
||||
The chosen tab is the Livewire property in `wire:model` (entangled; `wire:model.live` tells the
|
||||
server at once), or, without it, `selected` (the first tab by default) and `x-model`. The bar is
|
||||
@@ -24,6 +25,8 @@
|
||||
@php
|
||||
$model = $attributes->wire('model')->value() ?: null;
|
||||
$variant = $variant === 'secondary' ? 'secondary' : 'primary';
|
||||
// tab.blade.php repeats these two lines off the component stack, because a slot renders before
|
||||
// the component around it: change them together, or a panel's id stops matching aria-controls.
|
||||
$id = $attributes->get('id') ?? 'tabs-'.substr(md5($model.'|'.implode('|', array_column($tabs, 'name'))), 0, 8);
|
||||
|
||||
$initial = $selected ?? (collect($tabs)->first(fn (array $tab): bool => ! ($tab['disabled'] ?? false))['name'] ?? null);
|
||||
|
||||
@@ -6,76 +6,79 @@
|
||||
you — a sun while the page is dark, a moon while it is light — and it is a toggle button,
|
||||
"Dark theme", pressed while dark.
|
||||
- `cycle`: an icon button that steps light → dark → system, showing the choice it is on.
|
||||
- `picker`: M3's segmented buttons for the three choices, for a settings page.
|
||||
- `picker`: the three choices as a connected button group, for a settings page.
|
||||
- `contrast`: the same row for M3's three contrast levels — standard, medium (3:1) and high
|
||||
(7:1), the three the scheme is generated in. The row marks the level in force
|
||||
(`resolvedContrast`), so the operating system's setting shows while the choice is
|
||||
`system`, and choosing one is an explicit choice from then on.
|
||||
|
||||
The two rows are `<x-group>` — M3 Expressive's connected button group, the successor of the
|
||||
segmented button, which the expressive update deprecates. Its segments are native radios, so
|
||||
the browser supplies the radiogroup semantics, both arrow axes, the wrap and the roving tab
|
||||
stop; the store is bound through an `x-model` accessor on the wrapper, since the level a
|
||||
contrast row marks is the resolved one but the level it writes goes through `setContrast()`.
|
||||
`label` adds a visible legend (a settings page); without one the row is still named for a
|
||||
screen reader. `class` lands on the button or on the group.
|
||||
|
||||
The theme is the visitor's, known only in the browser, so the parts that depend on it wait for
|
||||
Alpine (`x-cloak`) rather than render a guess. `class` lands on the button or the group. --}}
|
||||
Alpine (`x-cloak`, an unchecked radio) rather than render a guess. --}}
|
||||
|
||||
@props([
|
||||
'mode' => 'toggle',
|
||||
'label' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$mode = in_array($mode, ['toggle', 'cycle', 'picker', 'contrast'], true) ? $mode : 'toggle';
|
||||
|
||||
$row = [
|
||||
'picker' => [
|
||||
'name' => __('Theme'),
|
||||
'field' => 'material-theme',
|
||||
'read' => '$store.theme.choice',
|
||||
'write' => '$store.theme.set(value)',
|
||||
'options' => [
|
||||
['id' => 'light', 'name' => __('Light'), 'icon' => 'light_mode'],
|
||||
['id' => 'dark', 'name' => __('Dark'), 'icon' => 'dark_mode'],
|
||||
['id' => 'system', 'name' => __('System'), 'icon' => 'brightness_auto'],
|
||||
],
|
||||
],
|
||||
'contrast' => [
|
||||
'name' => __('Contrast'),
|
||||
'field' => 'material-contrast',
|
||||
'read' => '$store.theme.resolvedContrast',
|
||||
'write' => '$store.theme.setContrast(value)',
|
||||
'options' => [
|
||||
['id' => 'standard', 'name' => __('Standard'), 'icon' => 'contrast'],
|
||||
['id' => 'medium', 'name' => __('Medium'), 'icon' => 'contrast_circle'],
|
||||
['id' => 'high', 'name' => __('High'), 'icon' => 'contrast_square'],
|
||||
],
|
||||
],
|
||||
][$mode] ?? null;
|
||||
@endphp
|
||||
|
||||
@if ($mode === 'contrast')
|
||||
@if ($row !== null)
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="{{ __('Contrast') }}"
|
||||
x-data
|
||||
data-theme-toggle="contrast"
|
||||
{{ $attributes->class(['inline-flex h-10 rounded-corner-full border border-outline']) }}
|
||||
role="group"
|
||||
aria-label="{{ $label ?? $row['name'] }}"
|
||||
data-theme-toggle="{{ $mode }}"
|
||||
x-data="{
|
||||
get chosen() {
|
||||
return this.{{ $row['read'] }}
|
||||
},
|
||||
set chosen(value) {
|
||||
this.{{ $row['write'] }}
|
||||
},
|
||||
}"
|
||||
>
|
||||
@foreach (['standard' => ['Standard', 'contrast'], 'medium' => ['Medium', 'contrast_circle'], 'high' => ['High', 'contrast_square']] as $level => [$text, $icon])
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked="false"
|
||||
x-bind:aria-checked="($store.theme.resolvedContrast === '{{ $level }}').toString()"
|
||||
x-bind:tabindex="$store.theme.resolvedContrast === '{{ $level }}' ? 0 : -1"
|
||||
x-on:click="$store.theme.setContrast('{{ $level }}')"
|
||||
x-on:keydown.arrow-right.prevent="$el.nextElementSibling?.click(); $el.nextElementSibling?.focus();"
|
||||
x-on:keydown.arrow-left.prevent="$el.previousElementSibling?.click(); $el.previousElementSibling?.focus();"
|
||||
data-contrast-option="{{ $level }}"
|
||||
class="state-layer focus-ring inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 border-outline px-3 type-label-lg text-on-surface not-first:border-s first:rounded-s-corner-full last:rounded-e-corner-full aria-checked:bg-secondary-container aria-checked:text-on-secondary-container"
|
||||
>
|
||||
<x-livewire-material::icon name="check" class="hidden size-4.5 in-aria-checked:block" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
|
||||
{{ __($text) }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif ($mode === 'picker')
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="{{ __('Theme') }}"
|
||||
x-data
|
||||
data-theme-toggle="picker"
|
||||
{{ $attributes->class(['inline-flex h-10 rounded-corner-full border border-outline']) }}
|
||||
>
|
||||
@foreach (['light' => ['Light', 'light_mode'], 'dark' => ['Dark', 'dark_mode'], 'system' => ['System', 'brightness_auto']] as $choice => [$text, $icon])
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked="false"
|
||||
x-bind:aria-checked="($store.theme.choice === '{{ $choice }}').toString()"
|
||||
x-bind:tabindex="$store.theme.choice === '{{ $choice }}' ? 0 : -1"
|
||||
x-on:click="$store.theme.set('{{ $choice }}')"
|
||||
x-on:keydown.arrow-right.prevent="$el.nextElementSibling?.click(); $el.nextElementSibling?.focus();"
|
||||
x-on:keydown.arrow-left.prevent="$el.previousElementSibling?.click(); $el.previousElementSibling?.focus();"
|
||||
data-theme-option="{{ $choice }}"
|
||||
class="state-layer focus-ring inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 border-outline px-3 type-label-lg text-on-surface not-first:border-s first:rounded-s-corner-full last:rounded-e-corner-full aria-checked:bg-secondary-container aria-checked:text-on-secondary-container"
|
||||
>
|
||||
<x-livewire-material::icon name="check" class="hidden size-4.5 in-aria-checked:block" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
|
||||
{{ __($text) }}
|
||||
</button>
|
||||
@endforeach
|
||||
<x-livewire-material::group
|
||||
inline
|
||||
x-model="chosen"
|
||||
:label="$label"
|
||||
:name="$row['field']"
|
||||
:options="$row['options']"
|
||||
{{ $attributes }}
|
||||
/>
|
||||
</div>
|
||||
@else
|
||||
<button
|
||||
@@ -93,7 +96,7 @@
|
||||
x-bind:aria-pressed="($store.theme.resolved === 'dark').toString()"
|
||||
x-on:click="$store.theme.toggle()"
|
||||
@endif
|
||||
{{ $attributes->class(['state-layer focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant transition-[border-radius] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast active:rounded-corner-sm']) }}
|
||||
{{ $attributes->class(['state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant transition-[border-radius] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast active:rounded-corner-sm']) }}
|
||||
>
|
||||
@if ($mode === 'cycle')
|
||||
<x-livewire-material::icon name="light_mode" x-cloak x-show="$store.theme.choice === 'light'" />
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
toolbar sits where it is written. A `fab` slot sets an `<x-fab>` beside a floating toolbar.
|
||||
`label` names it for screen readers.
|
||||
|
||||
A docked toolbar and a navigation bar occupy the same region of the screen and M3 says never to
|
||||
show both at once: the bar belongs on a primary page, the toolbar on a secondary or contextual
|
||||
one. Either way a toolbar placed at `bottom` clears `--material-bottom-bar`, so it is never
|
||||
buried under `<x-app-shell>`'s bar.
|
||||
|
||||
Put `<x-button icon="…" tooltip="…" />` controls in the slot (`:selected` for toggles). It is a
|
||||
`role="toolbar"`: the arrow keys move between its controls (resources/js/toolbar.js,
|
||||
resources/css/components/toolbar.css). --}}
|
||||
|
||||
Reference in New Issue
Block a user