Commit Graph
170 Commits
Author SHA1 Message Date
Andreas Reinhold / reiniandClaude Opus 5 deb1442dfb Open a menu in a bottom sheet on a compact window
Plan step 22, item 8 (actions.md § Missing, "Adaptive menu → bottom sheet
at compact"): M3 says "at compact breakpoints, consider swapping a menu for
a bottom sheet", and nothing connected <x-menu> to <x-bottom-sheet>.

<x-menu sheet-at-compact> draws its slot twice, in the popover and in a
modal <x-bottom-sheet> teleported to <body>, and below `medium`
(upTo('medium')) the trigger opens the sheet. The trigger says
aria-haspopup="dialog" there and "menu" from medium, aria-expanded in
both. Items keep their roles and the APG keyboard in the sheet; choosing
one, Escape or Tab close it and return focus to the trigger; a submenu
opens in place under its item; `filter` works in both; a resize across
600px closes whichever is open. The sheet has a fixed id that menu.js
makes unique and keeps as its wire:key, and the lists are keyed, so a
Livewire render patches an open sheet instead of swapping it.

bottom-sheet.blade.php is used as is. The filter now finds its field and
empty row per list instead of through x-refs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 11:30:55 +02:00
Andreas Reinhold / reiniandClaude Opus 5 ff16b12b3b Draw M3's focus ring on a dialog's scrolling body
Chrome makes a scroll container with nothing focusable inside a keyboard stop, and
showModal() gives it the dialog's first focus, which showed the browser's own ring.
The body now takes M3's 3px secondary indicator, inset so the dialog's rounded,
overflow-hidden box does not clip it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 11:04:40 +02:00
Andreas Reinhold / reini 8a2d8c0327 Merge branch 'worktree-agent-ae17d09efb642a925' 2026-09-14 11:03:41 +02:00
Andreas Reinhold / reiniandClaude Opus 5 cbc0fa76d2 Divide a dialog's scrolling body from its pinned header and actions
Plan step 23 (containment), containment.md § Missing: "no divider pinned
between a scrolling body and the header/actions". Every dialog now draws a
1px outline-variant rule under the header once the body is scrolled away
from its top, and over the actions while more of it is below; a body that
fits shows neither. The rules are pseudo-elements in the rows' own padding
(dialog.css), so showing one moves nothing, and x-dialog-dividers
(dialog.js) marks the wire:ignore.self <dialog>, which a morph leaves
alone, watching scroll and a ResizeObserver on the body and a wrapper
around the slot. The M3 gaps are split around the rules (8/8 under the
header, 8/16 over the actions). A full-screen dialog's rule sits under its
phone bar, and its action bar's always-on border follows the scroll too.
`separator` now means "draw both rules always" instead of rendering two
<x-divider> elements. The fade uses the effects-fast token, zero under
reduced motion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 11:03:05 +02:00
Andreas Reinhold / reiniandClaude Opus 5 f33fb8be64 Give a docked toolbar M3's rounded large-screen form
Plan step 25, item 8 (navigation Missing: "Toolbar, large-screen
treatments"). M3's toolbar guidelines (Adaptive design, Resizing) say it
of the docked toolbar: "On web and large screens, the docked toolbar can
be rounded. Dividers can be used to organize large amounts of items",
and it can be placed in different parts of the page. `rounded` does that
from expanded (840px): fully rounded, spanning its container, lifted
16px off the window's edges at place="bottom"; below expanded it stays
the square full-width bar. An <x-divider vertical /> between groups of
controls stands as tall as the icon buttons inside any toolbar. The
floating toolbar is already fully rounded and gets no new form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 11:02:43 +02:00
Andreas Reinhold / reiniandClaude Opus 5 092100c6ae Overflow an app bar's trailing actions below medium
Plan step 25, item 7 (navigation Missing: "App bar, trailing-action
overflow at small widths"). `<x-app-bar :actions="[...]">` takes the
trailing actions as a list whose entries take <x-menu-item>'s props plus
any action attribute. M3 allows up to two trailing icon buttons, and up
to four on larger screens, and lets trailing actions collapse into an
overflow menu at smaller breakpoints: below medium the bar keeps at most
two icon buttons, from medium four, the "More options" button counted
among them, the rest in its menu. Both forms render and media queries
pick one, so there is no script and no flash; each width has its own
menu so the arrow keys never reach a hidden row. The actions slot still
takes markup and never overflows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 10:57:50 +02:00
Andreas Reinhold / reini 12cdeaaf67 Merge branch 'worktree-agent-aed4618fbd0b6aeac' 2026-09-14 10:38:57 +02:00
Andreas Reinhold / reini cca87e58d7 Merge branch 'worktree-agent-a3082a7e6ef1bee94' 2026-09-14 10:38:57 +02:00
Andreas Reinhold / reini 0805ad8a48 Merge branch 'worktree-agent-a0d44b9ad5ca7814d' 2026-09-14 10:38:56 +02:00
Andreas Reinhold / reiniandClaude Opus 5 c975c5c593 Let a docked toolbar carry a FAB
Plan step 25 (navigation Missing): M3's toolbar configuration table lists
"With FAB" for both toolbars, but the `fab` slot was gated to the floating one.
A docked toolbar now takes it at its end, inside the toolbar where the arrow
keys reach it; its controls gather at the start, and the FAB rests flat on the
bar — elevation 0 for a FAB nested in another component, as Compose's
bottomAppBarFabElevation is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 07:16:26 +02:00
Andreas Reinhold / reiniandClaude Opus 5 7e4e7b44c6 Collapse an extended FAB to a FAB while the page scrolls
Plan step 22, actions.md § Missing (FAB to extended FAB scroll collapse):
M3 has an extended FAB collapse to a FAB on scroll-down and re-extend on
scroll-up, and `<x-button fab>` only swapped on window width.

`<x-fab collapse-on-scroll>` (extended, with an icon) takes a
`materialFab` flag from the new resources/js/fab.js, which reads the
window's scroll once a frame, ignores moves under 8px and extends again
near the top. The morph is CSS in actions.css: the label sits in a grid
track that closes to zero, the gap and minimum width follow on the
default spatial spring, and the label fades on the effects one; reduced
motion swaps outright through the motion tokens. The label is clipped, not
removed, so the collapsed FAB keeps its accessible name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 07:15:56 +02:00
Andreas Reinhold / reiniandClaude Opus 5 9c047a8fb1 Let a rail hide when collapsed instead of narrowing
Plan step 25 (navigation Missing): M3's "Expanded behavior | Hide when
collapsed" was only implicit below `medium` in the adaptive rail.
`<x-navigation-rail hide-when-collapsed>` (collapsible and adaptive rails) takes
the rail out of the layout when it is collapsed; `$store.rail.show()` from an
app-bar menu button brings it back expanded over a scrim, sliding in, and its
own menu button docks it again. It does not reach the bands where the window,
not the visitor, collapses a rail (below `medium` for collapsible, `medium` for
adaptive), since M3's collapsed rail may never hide. Every `rail-collapsed`
branch now stops while the rail is open. `<x-app-shell hide-rail-when-collapsed>`
exposes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 07:15:20 +02:00
Andreas Reinhold / reini c5f764a78c Merge branch 'worktree-agent-a4fc1072037346c95' 2026-09-14 06:44:46 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 549d96b0dc Give search M3's icon entry point and its suggestions
M3 § Search names three entry points and only the bar existed; suggestions
before the first keystroke were missing too (plan step 24, audit
docs/audits/m3-alignment/inputs.md § Missing). `trigger="icon"` is the search
icon button — search as a secondary action, one 48px button that expands into
the full-screen view at any width, since an icon button has nothing to dock
under, and takes its focus back on close. The `suggestions` slot stands where
the results do until something is typed; the live region counts whichever of
the two is on screen and names suggestions as suggestions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:40:48 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 a5acbe7f6b Let the rail centre its destinations
Plan step 25 (navigation Missing): M3's "alignment: top or center; on tablets
prefer center alignment for reach" had no implementation — the rail was
top-aligned only. `<x-navigation-rail align="center">` centres the destinations
between the header and the footer, which stay at their ends because M3 keeps the
menu icon and the FAB top-aligned. `safe center` gives the top back the moment
there are more destinations than fit, where centring would push the first one
out of reach above the scroller.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:40:36 +02:00
Andreas Reinhold / reiniandClaude Opus 5 5d0e9bc12f Square a button group and the choice drawn as one
Plan step 22, actions.md § Missing (Square-by-default button groups): M3
lists "Default shape | Round, square" as a button-group configuration and
neither `<x-button-group>` nor `<x-group>` had a `shape` prop, so a group
of square buttons still drew round outer corners.

`shape="square"` passes the square corner scale to every child through
`data-shape` and two new variables in groups.css: `--group-outer` is what
the two ends of a group round to (half the height when round, the corner
its inner edges take when square — M3's square table, 4/8/8/16/20dp), and
`--group-square`/`--group-square-pressed` give a standard group's buttons
the scale `<x-button shape="square">` draws. The selected rules keep
`--group-full`, so a selected button in a square group rounds: M3 has a
toggle inside a group swap shape on selection either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:40:14 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 cef350d739 Add the rail's narrow collapsed width
Plan step 25 (navigation Missing): NavigationRailCollapsedTokens
.NarrowContainerWidth, 80dp, had no way in — only the default 96.
`<x-navigation-rail width="narrow">` takes it; the collapsed width is now a
variable, so the four rules that each have their own reason to name it follow
without a specificity fight. No label fits under a 56px indicator at 80px, so a
narrow rail's items are their icons alone, with the label taken out of the
drawing and left in the accessibility tree, where it names the destination.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:39:31 +02:00
Andreas Reinhold / reiniandClaude Opus 5 88c46001fb Filter a menu from a field at the top of it
Plan step 22, actions.md § Missing (Menus as a filtering surface): M3's
menus page describes a menu that embeds a text field and filters its
options as you type, and nothing in the library did that.

`<x-menu filter>` renders the field, sticky above the list, and hides the
rows the query leaves out — client-side over the items already rendered,
so nothing is fetched and a `wire:click` stays where it was. The field
keeps the focus and the arrow keys move a highlight it names through
`aria-activedescendant`, the APG combobox keyboard `<x-choices
searchable>` already uses; Enter chooses the highlighted row, and a query
that leaves nothing says so. The list becomes a `role="menu"` inside the
popover, because a text field is not something a menu may contain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:38:02 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 ce1cd6eec0 Let the navigation bar hide on a scroll down
Plan step 25 (navigation Missing): M3's "hides on scroll-down, reappears on
scroll-up" had no implementation. `<x-navigation-bar hide-on-scroll>` slides the
bar out on the default spatial spring — an instant swap under reduced motion,
which zeroes the duration token — and never while a snackbar, a bottom sheet or
a drawer is anchored to its edge; focus reaching the bar brings it back.
`<x-app-shell hide-bar-on-scroll>` picks it, and --material-bottom-bar follows
the bar down and up so a `fab` button and the snackbar keep their offsets.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:36:57 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 2662040c24 Open a range picker full screen on a compact window
M3 § Date Pickers, Anatomy describes a 14-element full-screen range picker for
compact breakpoints; `<x-datepicker range>` always used the 360px modal dialog,
which on a phone is cramped for a two-month scroll (plan step 24, audit
docs/audits/m3-alignment/inputs.md § Missing). A third presentation, "full",
joins docked and modal: the same dialog grown to the screen, an app bar with a
close button and Save, the supporting text and the range as the headline over a
divider, the weekday labels held at the top, and the months in one vertically
scrolling list, each under its own label. The grid now draws from one `rows`
getter, which is this month's weeks everywhere else, so the docked and
single-date pickers render exactly as before. The list is a window of months
grown by the scroll and by the keyboard, since nothing here is lazy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:36:49 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 b1f64771f3 Draw M3's dragged state on a card
Plan step 23, containment.md § Missing ("Cards: the dragged state").
`data-dragged` on `<x-card>` takes the top of M3's card elevation scale
— Level4 (8dp) elevated, Level3 (6dp) filled and outlined — under the
16% dragged state layer from tokens/state.css. The row's hover and press
rules now exclude a dragged card, so the card a pointer is holding stays
at its dragged level. The application owns the attribute: nothing in the
browser tells a card it is being carried.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:34:35 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 4d00d87cd2 Add M3's tall navigation bar
Plan step 25 (navigation Missing): NavigationBarTokens.TallContainerHeight,
80dp, had no prop. `<x-navigation-bar tall>` picks it, and the tall container
keeps the vertical item layout at every width — the 600px horizontal layout is
the short bar's alone, so every rule behind that container query now names
`:not([data-tall])`. `<x-app-shell tall-bar>` passes it through and grows
`--material-bottom-bar` to 5rem with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:33:24 +02:00
Andreas Reinhold / reiniandClaude Opus 5 25091ebf21 Open a submenu from a menu item
Plan step 22, actions.md § Missing (Submenus): M3's Expressive vertical
menu specifies submenus and the Left/Right keys that open and close them,
and the library had neither.

`<x-menu-item submenu>` puts its slot in a second popover on the item's
end, flipping to the start where there is no room. `materialSubmenu`
reuses the menu button pattern one level in: the item is its own trigger,
so the anchor move and the button lookup are overridden away; Right,
Enter and Space open it, Left and Escape close it and return the focus,
and a fine pointer resting on the item opens it after a moment. `items()`
now stops at the popover it belongs to, so the arrows never walk between
a menu and an open submenu. The menu publishes its container as custom
properties so a vibrant menu's submenus are vibrant too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:31:56 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 72a3e22fbc Bound a text field's width from medium
M3 § Text Fields, Behaviour: compact may let a field span full width, but
medium and expanded must bound it — "never let it span the full width of a
large screen". Nothing in the package capped a field (plan step 24, audit
docs/audits/m3-alignment/inputs.md § Missing). From `medium` a field now stops
at 40rem — the site names no number, so the header says where this one comes
from — which a `max-w-*` class beats and `full` takes off. The search bar
already carries M3's own 720px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:23:08 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 c8c2d2e13e Count a text field's characters against its maximum
M3 lists "supporting text + character counter" as a configuration of both
text-field variants; nothing in the package drew one (plan step 24, audit
docs/audits/m3-alignment/inputs.md § Missing). `counter` on `<x-input>` and
`<x-textarea>` now puts `n/max` at the end of the supporting-text row, counted
from the control on every input, in the error colour past the maximum, and said
as M3's own "Character count, 5/20" from a polite region once typing settles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:21:22 +02:00
Andreas Reinhold / reini ce04101f89 Merge branch 'worktree-agent-ab53265ffbd837742' 2026-09-14 06:21:01 +02:00
Andreas Reinhold / reini 24520402c1 Merge branch 'worktree-agent-a6769a5cd2c4b070e' 2026-09-14 06:19:47 +02:00
Andreas Reinhold / reiniandClaude Opus 5 2482a0b6d4 Square the rail's collapsed FAB whatever size it is
The actions stream has since given the small extended FAB an 80px minimum width,
which the morph would have kept once the label closed. Dropping that minimum and
squaring the box against the height its size already sets gives back the 56, 80
or 96px FAB without this file naming any of those numbers.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-23).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:19:18 +02:00
Andreas Reinhold / reini d74fa250ee Merge branch 'worktree-agent-a6b15d4e17dad5970' 2026-09-14 06:17:26 +02:00
Andreas Reinhold / reiniandClaude Opus 5 7d14f2bd03 Morph the rail's FAB, and offer a divider, no fill and a width floor
M3 asks the rail to animate its FAB into an extended FAB as it expands; the
documented pattern was two FABs swapped by `display`, so the collapsed one
appeared at once while the rail's width was still springing. The header now takes
one <x-fab label icon> whose label springs open and shut on the rail's own
spring — and names it at both widths, where the collapsed FAB had needed a
tooltip to have a name at all.

M3 also lists two container treatments as optional that the library had neither
of: `divider` draws the vertical divider on the page's edge, which is its own
answer to a page scrolling under a fixed rail, and `fill="false"` drops the
container colour. Neither reaches a rail open over a scrim. And a `collapsible`
rail, whose width is the visitor's choice rather than the window's, is held to
the collapsed 96px below `medium`, where M3 asks for a navigation bar instead.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-22, N-23, N-24).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:16:37 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 71442831ff Name the error icon in the field's anatomy
Plan step 20, finding IN-18.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:15:10 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 0ffb2b06db Hold the docked search view's 240px floor to a view with results
A search whose slot renders nothing would otherwise open as an empty
240px box; M3's minimum is about a container with something in it.

Plan step 20, finding IN-20.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:13:45 +02:00
Andreas Reinhold / reiniandClaude Opus 5 5205aa8842 Offer the app shell a window-wide banner above the rail
M3's scaffold is bars, then rails, then panes, and the shell had no slot that
rendered above the rail: an application-wide bar could only be a pane bar beside
it. `banner` is that slot — the shell is a column now, with the rail and the page
as one row inside it — and the rail sticks under a pinned banner through
--material-banner rather than behind it. `top` still means the page's own bar.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-14).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:11:49 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 856b6f476b Say a field is in error twice, and stop dashing a read-only one
M3 wants two visual indicators per state and names the pair for a text
field: the error colours and a trailing error icon. The field now draws
one, labelled "Error", unless the caller already trails it or the field
is xs. A read-only field loses its dashed outline: M3 keeps "the same
visual style as an editable field", and the native readonly attribute is
what tells a screen reader. The data-readonly hook stays.

Plan step 20, findings IN-18 and IN-19.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:09:57 +02:00
Andreas ReinholdandClaude Opus 5 7bc4f740e7 Take the icon off the snackbar and lift the FAB clear of it
M3 says to avoid an icon in a snackbar and gives the supporting text no fourth
colour, so the state glyph and the 80% description are gone: a type now only
picks the announcement role. The 40px action and close buttons take the shared
touch-target, Escape dismisses a snackbar that holds the focus, and the host
publishes --material-snackbar-height on <html> so a `fab` button sits above the
snackbar instead of under it. Plan step 18, actions.md ACT-17, ACT-18, ACT-20,
ACT-34, ACT-35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:09:47 +02:00
Andreas Reinhold / reiniandClaude Opus 5 a237d33426 Ease a collapse open, which its own comment already promised
Plan step 19, containment.md C-24. `interpolate-size: allow-keywords`
was set and nothing transitioned, so the content snapped open. A new
resources/css/components/collapse.css transitions `block-size` on
`::details-content` over the fast spatial spring, with
`content-visibility` `allow-discrete` beside it so the section stays
rendered while it closes. The `<details>` carries `data-collapse` for
the rule to find.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:09:23 +02:00
Andreas Reinhold / reiniandClaude Opus 5 c3bed743aa Hold the navigation bar item to its own label and state-layer tokens
NavigationBarTokens names one label font for both icon positions, label-medium;
the horizontal layout had taken label-large from the rail's item, a different
component with a token of its own. And the item's state layer washed in
on-surface over a secondary-container pill, where the only state-layer tokens M3
states for a navigation item — the rail's, which the rail already follows — are
on-secondary-container for all six states.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-08, N-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:09:05 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 42d62ed00c Make a table's density the caller's decision
Density was applied to every fine-pointer device with no way out, which
M3 forbids outright: "don't apply density by default; offer an explicit
density opt-in instead, keeping opt-out targets at >= 48x48 CSS px". A
row is now 52px and `dense` is a prop that tightens it to 36. The row
divider also takes outline-variant itself rather than 60% of it, since M3
keeps opacity for state layers and disabled.

Plan step 20, findings IN-16 and IN-30.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:08:13 +02:00
Andreas Reinhold / reiniandClaude Opus 5 59ca88a0cf Give a vertical toolbar 24dp and a standard one primary buttons
M3 asks a vertical toolbar for a 24dp margin from the window edge where a
horizontal one keeps 16, and its standard-toolbar colour list ends "Standard
button (Primary)" — the vibrant row of the same list was already honoured, so
the two were being read inconsistently. Both rules are unlayered, because an
icon button's ink is a utility.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-12, N-13).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:08:13 +02:00
Andreas Reinhold / reiniandClaude Opus 5 789e8b2970 Centre the app bar's headline on a grid and curve its search bar
The centred headline was absolutely positioned 56px in from each edge — room for
exactly one of M3's two trailing icon buttons, so a long title ellipsised behind
them. It is now the middle column of a three-column row and shrinks against
whatever is really beside it. The search container follows M3's curve, filling
the row to 312dp and then only half of what is left, so the showcase drops the
hand-written max-width it had been working around it with.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-09, N-11).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:07:30 +02:00
Andreas ReinholdandClaude Opus 5 eac7f6a1de Name the FAB menu, scroll it, and let its items leave
Its trigger holds two aria-hidden icons and wrote aria-label only when a caller
passed `label`, so an unnamed button was the default; `label` now falls back to
"Toggle menu" and is always written. The list had no ceiling, so six items ran
off a landscape phone — it now scrolls under the FAB, which stays put — and its
items rose into place but vanished on close, because the popover took its
display away before they could move. Plan step 18, actions.md ACT-21, ACT-32,
ACT-33.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:07:22 +02:00
Andreas Reinhold / reiniandClaude Opus 5 675cfc806a Give a card per-state elevation and M3's two keyboard models
Plan step 19, containment.md C-13, C-23 and the directly actionable card
of C-13/C-18. A card had one elevation and never changed it, and its
hover replaced the elevated card's shadow with the state layer; it now
names its variant in `data-card` and moves a step on hover (elevated
1 to 2, filled and outlined 0 to 1) with the layer over it. The 12 to 16
corner morph on hover is gone: M3 gives a card one shape and lists shape
morph for buttons, FABs and list items only. New `actionable` prop for
M3's directly actionable card — the card is the one tab stop, with
`role="button"` (or `link`), its `title` as the name, and Enter or Space
reaching the opener from resources/js/list-rows.js.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:07:14 +02:00
Andreas Reinhold / reiniandClaude Opus 5 0aef7f1f96 Scroll a long section nav instead of wrapping it onto a grid
Five or more sections wrapped onto a 3- or 4-column grid, which left the tab
bar's divider under the last row only and stranded every upper row's active
indicator against nothing. From five they are now what M3 prescribes and its
accessibility page defends: a scrollable tab bar, each tab as wide as its label,
offset 52dp. Four or fewer still share the row. The 20px icons take the 20 cut.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-16).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:06:15 +02:00
Andreas Reinhold / reiniandClaude Opus 5 a9b5efb4b2 Put the tab bar's offsets, indicator inset and ring on M3's numbers
A scrollable set now starts 52dp in, so it reads as scrollable; a primary
indicator is inset 2dp at each side while a secondary one still spans the tab;
a link marked aria-current takes the active label colour the indicator already
followed; and the focus ring is 2px outside the tab, as everywhere else in the
package, paid for with 5px of room around the bar that the state layer and the
indicator reach back into.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-10, N-18,
N-20, N-21).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:04:23 +02:00
Andreas ReinholdandClaude Opus 5 9828c9258a Put the menu on M3's Expressive spacing and behaviour
Items were 44px tall with the baseline menu's 12px sides, the separator had half
its 8dp padding, a disabled item was skipped by the keyboard entirely, a chosen
item was told by colour and shape alone, and the list simply faded in place. Now:
48px rows with 16px either side, an 8px separator, disabled items focusable but
never activatable, a trailing check on a selected item, and an enter-exit that
scales the list out of the corner nearest its trigger — the scale on the spatial
spring, the fade on the effects one. Plan step 18, actions.md ACT-11, ACT-12,
ACT-13, ACT-26, ACT-27, ACT-28.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:04:03 +02:00
Andreas Reinhold / reiniandClaude Opus 5 dcd28707dd Let a segmented row answer, and inset a list's dividers
Plan step 19, containment.md C-08, C-21, C-22 and C-16. The segmented
fill was declared unlayered, so it beat every state layer in
@layer components and a segmented row lit up for nothing: it now sits in
the layer, `:where()`d to weigh nothing, ahead of the state rules. Its
tone is `surface`, which is ListTokens' ItemSegmentedContainerColor, and
a keyboard-focused row morphs to the large corner like a pressed one.
`<x-list dividers>` draws ListTokens' 16dp-inset rule instead of a
full-bleed `divide-y`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:03:12 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 9624d4bfd4 Hold the time picker's two selectors apart
The 24-hour clock has no period selector beside the hour and minute
boxes, so M3 widens them to 114px; AM/PM takes its own
tertiary-container rather than borrowing the boxes' primary-container;
a disabled period button uses M3's 12% disabled container, not 10%; and
the pair is a radio group, which is what M3's labelling table asks for,
so the arrows move between AM and PM.

Plan step 20, findings IN-10, IN-11, IN-26 and IN-27.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:01:23 +02:00
Andreas ReinholdandClaude Opus 5 573d94fe6e Give the xs button M3's 16dp padding, and stop groups wrapping
ButtonXSmallTokens puts 16dp either side of an xs label, where the button, the
<x-group> segment and --group-pad all wrote 12px; the variants table calls 16dp
the Expressive value twice over. A standard button group also dropped onto a
second line on a narrow window, which M3 forbids — and the press expansion only
ever reaches a neighbour on the same line. Plan step 18, actions.md ACT-09,
ACT-10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:00:12 +02:00
Andreas ReinholdandClaude Opus 5 597656c88a Take colour off the overshooting spatial spring
Four components animated background-color and color on ease-spatial-fast, whose
fast variant peaks at 1.094 — the one thing tokens/motion.css says a colour must
never do. Tailwind cannot emit two springs on one element, so a small unlayered
file gives shape and size the spatial spring and colour and elevation the
effects one in a single property list. Plan step 18, actions.md ACT-19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 05:59:30 +02:00
Andreas Reinhold / reiniandClaude Fable 5.1 f30fd575cf Draw the search bar and its view as the contained style
Expressive deprecates the divided style, so the results lose their
divider; the full-screen layout takes its own surface-container-low, one
step from the docked one; the docked view opens over a scrim, as M3's
variants table says it does; and the bar is bounded at M3's 720px, grows
to that width while focused, and rests with the 24px leading and trailing
padding the specs table gives an unfocused bar. --search-width sets the
resting width for M3's 360px bar.

Plan step 20, findings IN-07, IN-08, IN-09, IN-20 and IN-21.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 05:58:42 +02:00