Commit Graph
88 Commits
Author SHA1 Message Date
Andreas Reinhold / reiniandClaude Opus 5 25e1bb4788 Draw the data table without Tailwind
<x-table> renders data-md-table with data-md-size and data-md-dense,
and table.css moves into material.components on px and spacing tokens,
its cell selectors still inside :where() so a caller's cell rule wins
(plan step 36). The showcase's layout table follows the hook. A browser
test pins 52px rows and 36px dense ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 15:06:51 +02:00
Andreas Reinhold / reiniandClaude Opus 5 4141ebe7b0 Draw the search without Tailwind
<x-search> renders data-md-search with data-md-trigger and binds
data-md-open and data-md-full-screen; its parts become data-md-search-*
and the empty row data-md-search-empty. search.css moves into
material.components on px, spacing and state tokens (plan step 36).
search.js and the showcase layout follow the renamed hooks.

Browser tests cover the polite result count as it changes, the icon
entry point expanding full screen and returning focus, and suggestions
swapping to results on the first key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 15:04:50 +02:00
Andreas Reinhold / reiniandClaude Opus 5 281c21b0a4 Draw the slider without Tailwind
<x-slider> renders data-md-slider with its size, colour, value label,
orientation and centring as data-md-* attributes and its parts as
data-md-slider-*; every size, colour role, disabled state, the vertical
quarter turn, RTL mirroring and the no-script fallback move into
components/slider.css as custom properties per size and colour (plan
step 36). slider.js reads the props from the root and marks the
renamed handle, tick and icon hooks.

Browser tests pin the handle centred on the track at each size, Space
held with an arrow, and the vertical slider's drag along Y, keys and
value label beside the handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 15:01:19 +02:00
Andreas Reinhold / reiniandClaude Opus 5 1bb5adab0e Draw the choices without Tailwind
<x-choices> renders data-md-choices (data-md-searchable) and its
"Nothing matches" row as data-md-choices-empty, drawn by
components/choices.css, which brings the field, chip set and chip it
renders (plan step 36).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:55:29 +02:00
Andreas Reinhold / reiniandClaude Opus 5 55cf8ceed7 Draw the chip set without Tailwind
<x-chip-set> renders data-md-chip-set (data-md-scroll), its row, label,
hint, errors and scroll buttons as data-md-* attributes, and its row,
fade mask, scroll padding and pointer-fine buttons move into
components/chip-set.css on tokens (plan step 36). chips.js marks the
row data-md-scroll-start and data-md-scroll-end. Browser tests cover the
scroll buttons in LTR and RTL and the arrow keys with a roving tab stop
that scrolls the focused chip clear of the button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:54:41 +02:00
Andreas Reinhold / reiniandClaude Opus 5 d8072d878a Draw the chips without Tailwind
<x-chip> renders data-md-chip with its type and its states and parts as
data-md-* attributes (elevated, disabled, selected, actionable, the
icon, check slot, avatar, action and remove button), and every colour,
size, target and transition moves into components/chip.css on tokens
(plan step 36). Icons take size 18; the filter checkbox is
md-visually-hidden. chips.js follows the renamed chip hooks.

A disabled filter chip drawn as a label no longer shows a hover state
layer, which M3 never gives a disabled control. Browser tests cover the
32px chip and the remove button's 48px target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:50:29 +02:00
Andreas Reinhold / reiniandClaude Opus 5 b42069503d Draw the switch without Tailwind
<x-toggle> renders data-md-toggle, its row (data-md-right), track,
handle and icons as data-md-* attributes; the handle's icons take size
16 and a label-less track md-touch-target (plan step 36). toggle.css
gains the root; a browser test pins the handle's 16px/24px sizes and
16px/36px centres off and on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:43:17 +02:00
Andreas Reinhold / reiniandClaude Opus 5 8747163fce Draw the radio group without Tailwind
<x-radio> renders data-md-radio (data-md-inline), its legend, options,
rows and support as data-md-* attributes, and its layout moves into
components/radio.css: the 16px stack, the inline row from 600px, the
legend and the hint on the type and spacing tokens (plan step 36). A
label-less ring wears md-touch-target; the caller's style now lands on
the fieldset with its class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:41:38 +02:00
Andreas Reinhold / reiniandClaude Opus 5 f86afc0542 Draw the checkbox and the selection controls' row without Tailwind
<x-checkbox> renders data-md-checkbox, its box, tick and dash, and its
row, text and errors as data-md-* attributes; the 18px tick takes a size
prop and a label-less box md-touch-target (plan step 36). selection.css
moves into material.components as the row, text and state layer the
three controls share, and each control's drawing goes to its own file:
checkbox.css, radio.css, toggle.css, on the state tokens. The radio and
the switch take the renamed shared hooks now and lose their layout
classes in their own commits. field.js follows data-md-indeterminate.

Renaming the switch's data-handle also stops selection.css matching the
slider's handles, which it drew 24px above the track.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:34:00 +02:00
Andreas Reinhold / reiniandClaude Opus 5 3083fe28b2 Move the file picker's rules out of the field into file.css
<x-file> marks its field root data-md-file, and the tonal
::file-selector-button with its hover and disabled states moves from
field.css to components/file.css on the state tokens (plan step 36). A
browser test pins the field's 56px box and 16px padding and the pill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:12:40 +02:00
Andreas Reinhold / reiniandClaude Opus 5 23f2bdabf9 Move the select's rules out of the field into select.css
<x-select> marks its field root data-md-select, and what a <select>
changes in the field — covering the whole box, the customizable
select's closed button, the focused look and turned arrow while open —
moves from field.css to components/select.css (plan step 36). The open
list stays in menu.css until the menu leaves Tailwind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:11:40 +02:00
Andreas Reinhold / reiniandClaude Opus 5 c2fe64e00d Move the textarea's rules out of the field into textarea.css
<x-textarea> marks its field root data-md-textarea, and what a
<textarea> changes in the field — the grip in the corner, the label on
the first line, autogrow between rows and max-rows, the filled offsets —
moves from field.css to components/textarea.css (plan step 36).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:11:22 +02:00
Andreas Reinhold / reiniandClaude Opus 5 cfdb4619ad Give the password field its own stylesheet entry
<x-password> marks its field root data-md-password, and
components/password.css brings the field and its icons (plan step 36).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:10:14 +02:00
Andreas Reinhold / reiniandClaude Opus 5 172629face Give the one-line text field its own stylesheet entry
<x-input> marks its field root data-md-input, and components/input.css
brings what it renders, the field and its icons, so an application
imports one file per component (plan step 36).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:09:58 +02:00
Andreas Reinhold / reiniandClaude Opus 5 0341f9d4ca Draw the text field's chrome without Tailwind
<x-field> renders data-md-field with its props and parts as data-md-*
attributes (box, control, label, outline, support, counter, trailing
buttons), and field.css moves into material.components on tokens: px
geometry, spacing and state tokens, the 600px breakpoint (plan step 36).
Its icons take a size prop, 24/20/16 by the field's size.

Every control the field wraps now marks itself data-md-field-control,
so the inputs, the pickers, choices, field.js, menu.css's select and
listbox rules and timepicker.css follow the renamed hooks. Browser tests
cover the error icon, the disabled field's hover, the counter and the
width bound from 600px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:08:22 +02:00
Andreas Reinhold / reiniandClaude Opus 5 a6e5a593d5 Draw the form without Tailwind
<x-form> renders data-md-form and data-md-form-actions, and its column
and actions row move to components/form.css in material.components, on
the spacing tokens (plan step 36). The actions slot's own attributes
still land on the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 13:59:32 +02:00
Andreas Reinhold / reiniandClaude Opus 5 bdbb76a0bd Size icons and shapes with a prop instead of a Tailwind class
<x-icon size> writes --md-icon-size (24px by default) and takes the optical-size-20
cut for 20px or less, as M3 asks; mirror-rtl flips a directional symbol. <x-shape
size> writes --md-shape-size. Both draw from plain-CSS stylesheets in
material.components, gathered in components.css until all.css replaces it; a
caller's size class still wins while the other components are rewritten.
Plan steps 35 and 36, the prop every component rewrite depends on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 13:49:24 +02:00
Andreas Reinhold / reini 40defeaaf5 Merge branch 'worktree-agent-a9c6ef32512bc556d' 2026-09-14 11:31:19 +02:00
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