<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
Plan step 35: <x-app-shell> becomes <x-scaffold>, M3's and Compose's name
for the structure of bars, rails and panes, with no alias; every package
view, the showcase, the tests, the skills, README and UPGRADE follow. Its
markup and Tailwind classes stay for the navigation rewrite. The new fab
slot places the FAB as Compose's Scaffold does - bottom-end, 16px from the
edges below medium and 24px from it, clear of the navigation bar and of a
snackbar on screen (resources/css/layout/scaffold.css) - and the content
region tells a pane inside that the margin is already drawn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 35: <x-feed min-item gap>, following the reference's feed rules
- one column below medium, then as many equal columns of at least the
minimum item width as the feed has room for, 24px apart from medium,
items in their source order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 35: <x-supporting-pane>, following the reference's placement
table - below the focus pane below expanded, beside it from 840px at a
fixed 360px (412px from large) or the two-thirds split. Below expanded
compact="sheet" docks the supporting pane as a bottom sheet whose drag
handle opens it; always on the trailing side, so focus order matches the
panes on screen, and mirrored in RTL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 35: <x-list-detail>, following the reference's visible-panes
table row by row - one pane below expanded, the detail replacing the list
with a back button once something is selected; from 840px the list a
fixed 360px (412px from 1200px) beside the detail, 24px apart. The
selection binds with wire:model or x-model; below expanded focus moves to
the detail and back() returns it to the item (resources/js/layout.js).
Grid columns mirror in RTL, and the back arrow turns with them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 35: <x-pane>, a content region with M3's margins (16px below
medium, 24px from it) drawn once however panes and layouts nest, a width
cap, and an optional pane app bar - <x-app-bar> as a direct child of the
pane with title, subtitle, actions, a back link or action, a leading
slot, and the section navigation under it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 36. <x-button> renders data-md-button with its variant, colour,
size, shape, icon-button width, selected state and compact FAB as
data-md-* attributes, and passes the glyph's size to <x-icon>.
button.css draws Button*Tokens' geometry per size, the colour roles
through custom properties that the disabled treatment replaces, the
state layer, focus ring and 48px target, and the compact FAB below
600px. Its corner rules carry no specificity (:where), so a group's
stylesheet reshapes the buttons inside it with any selector; the
split button no longer passes an empty corners prop, which is gone.
data-icon-button becomes data-md-icon-button, also in groups.css,
toolbar.css and theme-toggle's own button (navigation group), and the
AppBarTest assertions follow the new hooks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 35: <x-surface level padding corner outlined>, a tonal region
in surface or a surface-container step, padded with a spacing token,
rounded with a corner token and optionally edged in outline-variant. A
pane on a surface keeps its margin again, since the surface is a new edge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 35: <x-grid :columns gap min-item>. The column count per
breakpoint is written as five inline custom properties, each filled from
the nearest smaller breakpoint, so a nested grid never inherits its
parent's; min-item fills a row by the room the grid has, and with columns
the counts become a ceiling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 35: <x-row gap align justify wrap stack-below>, children side
by side in a pane and stacked under one another below an M3 breakpoint,
in the inline direction so a right-to-left document mirrors it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
<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
Plan step 35: <x-stack gap align>, the first of M3's layout components,
with what they all share - src/Support/Layout.php reading the props into
data-md-* attributes, the spacing-token gap and padding rules, the
hide-below/hide-from rules in material.visibility, the stylesheet checks
for resources/css/layout, the browser test file and the skill's Layout
group. The Workbench puts the material layers above Tailwind's preflight,
which would otherwise zero every layout padding and margin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
<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
<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
Plan step 36. <x-badge> renders data-md-badge with data-md-dot,
data-md-variant (solid, tonal or outline, resolved in that order),
data-md-color and data-md-floating; badge.css draws BadgeTokens' dot and
count, the status label, each colour's pair of roles through custom
properties, and M3's anchor geometry when floating. A plain badge sets
no colour, so the caller's CSS paints it.
NavigationBarTest (navigation group) asserts the dot by data-md-dot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
<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
<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
<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
Plan step 36. <x-tooltip> renders data-md-tooltip and data-md-side on
the bubble and data-md-tooltip-anchor on a standalone trigger's wrapper;
tooltip.css draws PlainTooltipTokens' inverse-surface bubble, its anchor
placement and flips per side, and the fade on the fast effects spring
with @starting-style.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
<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
Plan step 36. <x-loading> renders data-md-loading and data-md-contained,
and loading.css in material.components draws the 48px indicator in
primary, the contained circle and the still shape under reduced motion.
The view no longer reads the caller's classes to decide its size and
colour: a caller's utility outranks the package's layer on its own.
ActionStylesheetsTest checks the group's stylesheets for the package's
shape (layer statement, plain imports, material.components, tokens, px
breakpoints, imports of rendered components) and its views for class
lists; tests/Support/ComponentStylesheet reads a rule's declarations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
<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
<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
<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
Plan step 36. A component rewritten without Tailwind keeps its padding,
margins and borders in material.components, and Tailwind's preflight
(* { padding: 0; margin: 0; border: 0 solid }) in its base layer, named
after material, undid every one of them. The Workbench now names theme and
base before importing the foundation, so the preflight — the same reset
foundation/reset.css carries — sits below the package, while Tailwind's
components and utilities stay above it and a caller's utility still wins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
A rewritten component's padding, margin and border sit in
material.components, which the Workbench declared below all of
Tailwind's layers, so preflight's universal reset beat them. The entry's
layer statement now puts material between base and components (plan
step 36): the material reset already carries preflight, and every
utility still outranks the package.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
<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
Plan steps 33, 34 and 42. StylesheetsTest follows foundation.css through
its imports and checks each file: a header, the exact layer statement,
plain imports only before its first block, every rule in its own
material layer but the two hiding rules, no Tailwind directive, media
queries only at 600/840/1200/1600px. It also pins the spacing tokens to
the reference table, the md-type classes to the type utilities'
declarations, the whole text class set, the interaction classes against
their utilities, one [x-cloak], material.css never reaching the reset,
and the Workbench importing the foundation before Tailwind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 33. The Workbench imports foundation.css ahead of
@import 'tailwindcss', so the material layers are declared first and
sit below Tailwind's, then tailwind.css: the theme, the token utilities
and the component stylesheets still written for Tailwind, with no
tokens, so nothing is declared twice. material.css keeps working for
1.x-style imports after Tailwind by bringing the tokens, the hiding
rules and tailwind.css, and never the reset, which would outrank
Tailwind's utilities from there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 33. Each token file now opens with the material layer
statement and wraps its declarations in material.tokens (the font face
in material.base), so an application's unlayered scheme outranks the
package default whatever the selectors, and the foundation can import
the files with no layer() on the import.
The type-* and state utilities move to tokens/utilities.css, still
imported by material.css, so no file the foundation imports carries a
Tailwind directive. [x-cloak] moves beside the [hidden] rule in
foundation/hidden.css, outside every layer; material.css imports it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
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
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
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
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
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
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
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
Plan step 23, containment.md § Missing ("Divider: the divider-with-text /
subheader configuration"). `<x-divider text="…">` draws the label at the
start and the rule running on from it, with the divider specs table's
geometry: 4dp between the words and the rule, 8dp right margin, 8dp
bottom margin. The label is M3's subhead — title-small in
on-surface-variant, the only subhead type M3 publishes (the rich
tooltip's specs), since the divider page names none. The words stay text
naming the group; only the rule is the separator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
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
Plan step 23, containment.md § Missing ("Carousel: the uncontained
multi-aspect-ratio layout"). `<x-carousel layout="multi-aspect">` is the
layout M3 added in November 2025: each `<x-carousel-item aspect="…">`
keeps its own ratio at the row's fixed height, held inside M3's 9:16 to
16:9 range (a square by default), with 16dp leading padding, 8dp gaps,
the extra-large corner and uncontained (default) scrolling.
The keyline machinery does not fit it: an Arrangement counts items of
one size each, and every snap position and mask follows from that size.
So the layout is a plain flex row, unmasked, and carousel.js only
measures each item's resting position off the DOM, which keeps the
previous/next buttons, the arrow keys, Home/End and bring-into-view
working. The header and SKILL.md say so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 22, actions.md § Missing (two-line snackbar height, snackbar
keyboard shortcut): the container grew organically from `min-h-12` rather
than sitting at SnackbarTokens.TwoLinesContainerHeight, and there was no
way for a keyboard to reach an actioned snackbar at all.
A `description` is M3's second line, so the snackbar is pinned to 68px
whenever one is there, and below `medium` a two-line snackbar with an
action wraps the action under the text — M3's "two lines with longer
action" configuration. Alt+G, the shortcut M3 suggests for the web, moves
the focus to a snackbar that carries an action from wherever the page had
it; `event.code`, because Alt rewrites `event.key` on some layouts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 § Chips, Accessibility asks that a row which overflows horizontally carry a
visible affordance; `<x-chip-set scroll>` faded its edges and left it there
(plan step 24, audit docs/audits/m3-alignment/inputs.md § Missing). Where the
pointer is fine, and so there is no swipe to reach for, a button now sits over
each fading edge and scrolls the row by most of its width. They are pointer
affordances only — no tab stops, since the arrow keys already walk every chip —
and the row's scroll padding grows on a fine pointer so a chip the keyboard
reaches clears the buttons as well as the fade. The scroller became a ref,
because the buttons stand outside it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 22, actions.md § Missing (Selection-required / multi-select
semantics): M3 lists single-select, multi-select and selection-required as
button-group configurations, and `<x-button-group connected>` left
selection entirely to the caller's own `aria-pressed`.
`selection="single|multi"`, with `required`, takes `aria-pressed` over:
pressing a button writes its `value` to `wire:model` or `x-model`,
deselects the others in `single`, and refuses the press that would leave
nothing selected. With no model it reads the buttons' own `aria-pressed`
once and goes on from there.
`<x-group>` and this do not absorb one another, and the header and
SKILL.md say why: `<x-group>` is for a choice whose options are data — it
renders real inputs, posts in a plain form and paints its own segments —
and stays the first thing to reach for; a selection group governs buttons
the caller writes, manages state and shape, and leaves each button to draw
its own colours from its own `:selected`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9