Size the loading indicator and publish the rail's value

Two gaps found rewriting the Boost skill for 2.0's plain CSS, each
closed the way M3 describes it.

`<x-loading size="96">`: M3 gives the loading indicator a responsive
size, 48dp by default and flexible from 24 to 240dp ("never exceed that
range"), with the container and the active shape in a fixed ratio. The
size had no prop, so an application wrote a width and height of its own;
`size` now takes a whole number of px in that range, written as
`--md-loading-size`, and the SVG keeps the 48:38 ratio as it scales. A
value outside the range is ignored, as `<x-icon>` and `<x-shape>` ignore
theirs, and an application's own width and height still win.

`--md-navigation-rail-value`: M3 Expressive's rail has two values,
collapsed and expanded (Compose's WideNavigationRailValue), and content
in a rail follows it. Without a hook an application copied the rail's
seven conditions — mode, `data-rail`, `data-rail-auto`, open, and the
window band — out of navigation-rail.css. The rail now publishes the
answer from the same branches that narrow it: `expanded` by default,
`collapsed` wherever it is drawn collapsed, so a style query in the
application's CSS switches at the first paint and at the same moment
as the rail's own items. A rail open over a scrim reads `expanded`, and
outside a rail the property is unset. A JS attribute would have missed
the first paint; a width container query would have lagged the collapse
animation.

Style queries on a custom property need Firefox 151, so the browser
floor moves from Firefox 147 to 151 (README, the CI note, UPGRADE's new
2.1.0 section); Chrome 125 and Safari 18.4 are unchanged.

Browser tests pin both in Chrome, Firefox and Safari: the indicator's
drawn box at 96 and 32px, and the rail's value — with a style query
acting on it — at the first paint for fixed modes, across the window
classes and the menu button for an adaptive rail, and open and closed
for a modal one. NavigationRailTest pins the value in each of the five
collapsed branches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-16 12:06:16 +02:00
co-authored by Claude Opus 5
parent 24eb811f34
commit f2a3c4d056
13 changed files with 228 additions and 30 deletions
+9 -2
View File
@@ -1,8 +1,12 @@
{{-- M3 Expressive's loading indicator: a shape that morphs through seven Expressive shapes while
it turns, for a wait that has no known length.
In the text colour — primary unless the caller colours it — and 48px unless the caller's CSS
sizes it. `contained` puts it on a primary-container circle, for a spinner over content. It
In the text colour — primary unless the caller colours it — and 48px, M3's default. `size` is
its size in px, any whole number from 24 to 240, M3's responsive range ("never exceed that
range"; scale it up for large and extra-large windows): the container and the shape keep
their 48:38 ratio at every size, because the drawing scales with its box. It is written as
`--md-loading-size`; anything outside the range is ignored, and without it the caller's CSS
may size it. `contained` puts it on a primary-container circle, for a spinner over content. It
is a `progressbar` named `label` ("Loading" by default); pass `:label="false"` where something
else already says what is happening, as a button's own spinner does.
@@ -16,15 +20,18 @@
@props([
'contained' => false,
'label' => null,
'size' => null,
])
@php
$size = filter_var($size, FILTER_VALIDATE_INT, ['options' => ['min_range' => 24, 'max_range' => 240]]) ?: null;
$decorative = $label === false;
$label = $decorative ? null : ($label ?? __('Loading'));
$attributes = $attributes->merge(array_filter([
'data-md-loading' => true,
'data-md-contained' => $contained ? true : null,
'style' => $size !== null ? "--md-loading-size: {$size}px" : null,
'role' => $decorative ? null : 'progressbar',
'aria-label' => $label,
'aria-hidden' => $decorative ? 'true' : null,
@@ -45,11 +45,19 @@
`footer`, pinned to the foot. Header and footer never scroll, so nothing in them is cut off by
the scroller's edge.
Anything else inside can take both shapes by matching the rail's own state directly, the same
selectors resources/css/components/navigation-rail.css uses for every branch of "collapsed"
`data-md-navigation-rail`'s value, `:not([data-md-open])`, and the window band each mode
collapses in (its header lists them). Every rule in the package that draws a collapsed shape
writes them out, and tests/Feature/Components/NavigationRailTest.php keeps each copy to the same
Anything else inside takes both shapes from the rail's value: the rail publishes M3's two
(Compose's WideNavigationRailValue) as `--md-navigation-rail-value`, `collapsed` or
`expanded`, from the first paint and in step with the rail's own items, and every descendant
inherits it, so an application's CSS asks a style query instead of repeating the conditions:
@container style(--md-navigation-rail-value: collapsed) {
.account-summary { display: none; }
}
A rail open over a scrim reads `expanded`; outside a rail the property is unset and neither
value matches. Style queries on a custom property need Chrome 111, Safari 18 or Firefox 151.
The package's own rules write the conditions out (resources/css/components/navigation-rail.css
lists them), and tests/Feature/Components/NavigationRailTest.php keeps each copy to the same
conditions. Nothing that shows while collapsed may be wider than 96px.
Props: `label` names the landmark ("Main"); `width` is the expanded width (`256px`, held