An error or warning alert was role="alert", an assertive live region — but the
usual alert is part of the page as it renders, which some screen readers then
announce over the page title, and a Livewire morph re-announces. It is now
role="status" whatever its colour, with an `assertive` prop for a notice put on
screen in answer to something the person just did. Its 40px dismiss button takes
the shared touch-target. Plan step 18, actions.md ACT-18, ACT-36.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
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
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
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
Plan step 19, containment.md C-10. The action row was right-aligned with
24px above it; the side-sheet specs table puts bottom actions on the
left in a 72dp row with 16dp above and 24dp below. A dialog's actions
stay trailing-aligned — M3 specifies the two differently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-19 and C-09. A modal sheet opened at 90%
of the viewport where M3 caps the initial position at 50% and lets the
person pull it up; `height` now defaults to `50dvh` and whatever is
passed is held under a ceiling of the screen less M3's 72dp top margin.
The drag handle is `on-surface-variant` undiluted, as
SheetBottomTokens' DockedDragHandleColor and the component's own header
comment already said.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
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
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
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
M3's chip keyboard table makes a set one tab stop that the arrows move
through; ten filter chips cost ten Tab presses today. The set now keeps a
roving tabindex — re-applied whenever a Livewire morph rewrites the chips
— and walks left, right, Home and End, following the writing direction.
The input chip's remove button also grows to a full 48x48 target, where
it was 34px wide.
Plan step 20, findings IN-13 and IN-14.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 is explicit twice over — never disable a FAB, remove it instead — and the
prop painted the full disabled treatment with nothing warning against it. It is
gone; hiding the FAB is the documented answer. The extended FAB's icon-to-label
gap goes to M3's 16dp at md and 20dp at lg, the small one takes the baseline's
80dp minimum width, and the root gains a `data-fab` hook for a place that draws
a nested FAB its own way. Plan step 18, actions.md ACT-08, ACT-16, ACT-31.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-14, C-15 and C-26. The leading and
trailing gap was 12px where ListItem.kt's LeadingContentEndPadding and
TrailingContentStartPadding are 16dp; a three-line item was
middle-aligned and padded 10px where M3 top-aligns an 88dp item and
Compose pads it 12; and a segmented list's icons stayed at the baseline
24dp instead of ListTokens' expressive 20dp, which the item now reads
from its list with `@aware` and draws from the 20 optical cut.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
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
M3 asks a FAB's icon and a default (non-toggle) icon button's icon to be filled,
never outlined; the toggle half of that rule was already right, so only the
`$selected === null` case changed. Every glyph these components draw at 20px now
passes `optical="20"` for the cut M3 draws at that size — button xs/sm, group,
menu item, the alert, snackbar and FAB-menu close buttons, the stat's icon.
Plan step 18, actions.md ACT-14, ACT-15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-06, C-17 and C-20. The whole box scrolled,
so a long dialog scrolled its headline and its buttons away; the header,
the body and the action row are now separate rows of the flex column,
each carrying the 24dp padding the box used to hold, and only the body
scrolls. `separator` draws the divider under the header and above the
actions, where it now stays put. The full-screen header is M3's 56dp
(was 64) and its action bar 56 (was ~72). New `alert` prop for M3's "on
web, basic dialogs should have the alert dialog role", and a `subtitle`
is always the dialog's `aria-describedby`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
`mode="picker"` hand-rolled an outlined segmented button, point for point the
design M3 Expressive deprecates, and its own arrow keys neither wrapped nor
handled Up/Down. Both rows are now <x-group>: native radios in a fieldset, so the
browser supplies the radiogroup, both axes, the wrap and the roving tab stop, and
the store is bound through an x-model accessor — a contrast row still marks the
resolved level but writes through setContrast(). A new `label` prop legends the
row. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-04, N-17).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
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
Plan step 12, containment.md C-04. `with-close-button` defaulted to
false, so a drawer could render with no exit at all once
`close-on-escape` and the scrim were off. It now defaults to true, and
`:with-close-button="false"` is ignored where nothing else closes the
sheet: Escape off, the scrim off, or a pane, which has neither.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 12, containment.md C-03. `selected` was colour alone on a
`listitem`, which cannot carry a selected state at all. `<x-list
selectable>` (or `selection="single|multi"`) now makes the container a
`role="listbox"` and each item an `option` with `aria-selected`; a plain
list keeps `role="list"` and marks a selected item `aria-current`. A
selected option draws a trailing check as M3's second cue, which
`icon-right` replaces. The item reads its list's mode with `@aware`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The md handle is 52px, not the 44 the MDC tokens gave it; the value
indicator is M3's 44x48 label container, not Flutter's 32px pill; and a
stop indicator, a tick and the inset icon now take the "on" colour of the
part of the track they sit on, which M3's nine colour roles spell out and
the library had inverted. Space held with an arrow moves by the same
large interval as PageUp, M3's "Space & Arrows" row.
Plan step 20, findings IN-04, IN-05, IN-06 and IN-24.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3's accessibility page forbids it outright: an actioned snackbar has to wait
for the person to read it and reach its action. An entry with an action and no
timeout of its own is now untimed, the close button it already draws being the
way out; a timeout written out still wins. Plan step 11, actions.md ACT-03.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
`place="bottom"` ignored --material-bottom-bar, so a floating toolbar overlapped
the shell's navigation bar and a docked one landed squarely on it. Both now clear
it — max(), not a sum, since that height already swallows the bottom safe area —
and the docs say what M3 says: never show a docked toolbar and a navigation bar
together. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-02).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The skill's colour and theme sections and the README's scheme and theme.* bullets
now carry what the command writes: the three levels, --harmonize, the state colours
built as error is, data-contrast, theme.contrast, motion.scheme, and the store's
contrast, resolvedContrast and setContrast(). Plan: docs/plans/material-3-alignment.md,
steps 6 and 7 (core C1, C2, C15).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan steps 15-17. Every breakpoint the skill named is now a window size class:
the wrapper idiom (`max-medium:hidden`), `<x-app-shell>`'s section gets a table
of the four classes with their navigation and margin, `<x-drawer pane>` is
"from expanded" at 360dp, and `<x-section-nav>`, `<x-search>`, `<x-datepicker>`,
`<x-modal fullscreen>`, `<x-button fab|responsive>`, `<x-navigation-bar>`,
`<x-navigation-rail mode="adaptive">`, `$store.rail` and the FAB and fab-menu
margins follow. A new Tokens bullet states the rule once: `medium:` 600,
`expanded:` 840, `large:` 1200, `extra-large:` 1600, Tailwind's screens cleared,
scripts through resources/js/breakpoints.js, a container query (`@md:`) a
different thing.
The README gains the same warning where it tells an application how to import
the package's CSS, since a leftover `sm:` from another project now compiles to
nothing rather than to 640px.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The "Testing the design" section of the README and of the Boost skill list the
cleared breakpoints and scales with their M3 replacement, the arbitrary-colour
check, and the two opt-in checks for the absolutes and for opacity on ink.
Plan: docs/plans/material-3-alignment.md, step 9 (core C4a, C13).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
bin/springs.mjs is the sampler behind motion.css: damping ratio and stiffness in,
the 49-point linear() and the settle time out. Run with --settle it reproduces
the six curves the file carried byte for byte, so only the window changed.
Each spring is now sampled over the duration M3 publishes for the web (spatial
350/500/650 ms, effects 150/200/300 ms) instead of stopping at its own settle
time, so the *-duration tokens read Google's numbers. Sampling stays in real
time — point i is the spring at duration × i / 48 — so the bounce lands at the
same millisecond as before; a longer duration just holds the tail flat at 1, and
a shorter one pins the last point, at most 0.3% of the travel in one frame. Each
token's comment records damping, stiffness, settle time and sampled duration.
M3's second motion scheme is here too: [data-motion="standard"] swaps the three
spatial springs (damping 0.9, 0.2% overshoot instead of 9%) and their durations;
the effects springs sample identically in both schemes, so they are shared. The
reduced-motion block names both selectors, so it still zeroes every duration.
Plan: docs/plans/material-3-alignment.md step 5, findings core C8, C9.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Tracking now comes from androidx's TypeScaleTokens.kt as sp/16 rem, so Display
Large is -0.0125rem, Title Medium and Body Medium 0.0125rem where material-web's
pre-Expressive numbers stood. The emphasized set gets its own
--md-sys-typescale-emphasized-<style>-tracking, which the emphasized utilities
read: M3 widens four of them and takes Display Large back to zero. Every regular
utility sets `font-variation-settings: normal`, because the property inherits and
body copy inside an emphasized heading is meant to read as itself.
bin/check-font.mjs (fontkit) prints the packaged woff2's fvar axes, so a
re-subset cannot silently drop the ROND axis the emphasized styles depend on;
`npm run check:font` runs it and tests/Feature/FontTest.php runs it through the
configured node binary, skipping when node cannot run (PHP cannot open a woff2
without Brotli).
Plan: docs/plans/material-3-alignment.md step 4, findings core C5, C6, C7.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3's optical size axis redraws a symbol so its strokes look equally heavy at
every size; drawing the 24 cut at 20px thins them by about a sixth (styles.md
§ Icons). `optical` picks the cut — 24 by default, 20 for an icon drawn at 20px
or smaller — and combines with `filled`; anything else falls back to 24, so no
caller can land on a folder that does not exist. SvgFile::symbol() takes the
size as a fourth argument and resolves the `-20` folders; the catalogue is the
same in both cuts, so symbolNames() and DesignGuard's icon-name check are
unchanged. The callers that size their own icons pass it in a later step.
Plan step 10; finding C16.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
resources/boost/skills/material-3-design/SKILL.md: the tables the guideline
compresses — every colour role with its purpose, pair and utility, the surface
container hierarchy, contrast levels, elevation by component, the corner scale
and the corner of each component, the type scale with roles and the emphasized
set, the spring tokens and direction rules, the state table and targets, the
window size classes with navigation, panes and margins, spacing tokens, icon
axes, an accessibility checklist and the don'ts — each M3 name beside the
library's utility, each chapter with Google's source pages, and an Attribution
chapter. Loaded on demand by Boost. Plan step 27 (core C24, C19).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
resources/boost/guidelines/material-3.blade.php: the rules that change what an
agent writes — colour roles and pairs, surface containers and elevation, the
corner scale by component, type roles, spatial vs effects motion, states and
48px targets, the five window size classes, accessibility, icons, and the
don'ts M3 states — one line each with the utility to use, under 120 lines
because Boost inlines it into every session. Plan step 26 (core C24).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
<x-app-shell> passed no badge label to its rail and bar items, so a badge
like 0/3 reached a screen reader bare; a destination's badgeLabel now does.
The head script indexed the profiles with a null scheme when none are
configured, which PHP 8.5 reports as a deprecated null array offset on every
page with theme.meta on. It reads the default scheme without the lookup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
A status label came tonal (the colour's container) or outlined; a label that
has to stand out in the colour itself fell back to the filled count, which is
16px and hidden from screen readers. solid draws the label shape in the
colour's filled pair and is spoken like the others.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
A drawer that is a pane from xl ignored Escape, whatever close-on-escape
said: the page beside a pane stays in use, so that is the right default. An
application whose pane is a transient detail (ReStride's activity and
workout panes) can now pass pane-close-on-escape and have Escape close it at
every width.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
The phone picker of <x-section-nav> drew the current section as a checked
menuitemcheckbox in tertiary-container, which a screen reader announces as a
choice rather than where you are, and it dropped the sections' badges, so an
open support count showed on the tabs but not on a phone. <x-menu-item> gains
current (aria-current="page", the selected shape in secondary-container,
the navigation indicator's colour) and badge (a dot or a count at the end of
the row), and the picker uses both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
<x-menu>, <x-fab-menu> and <x-rich-tooltip> gave their popover an id
that is new with every render, and Livewire's morph matches an element
without a wire:key by its id. So every render of the component around
them swapped the popover for a closed copy: an open menu closed, its
listeners stayed behind on the old element (Escape or a press outside
then left aria-expanded="true" on the trigger and focus unreturned), a
keep-open item's wire:click closed its menu when the response came, and
a rich tooltip went on showing the detached bubble, so it never opened
again. <x-carousel>'s row had the same kind of id: after a render it
scrolled without its listeners, and its items stopped re-masking.
The popover, the bubble and the row now carry a wire:key, so the morph
patches them in place and changes the id and anchor name together with
the trigger's, as it already did for everything else. The key goes
through an attribute bag: Livewire compiles a wire:key written in a
template into the key of the loop iteration around it, which would have
given every child component after the menu in a row the same key.
A morph also removes the menu button's ARIA attributes, which only
script writes. menu.js now writes them again after every morph, so the
button of a menu that stays open, and a FAB menu's close look, still
say it is open, and aria-controls names the popover's new id.
A second press on an open menu's button opened it again, render or not:
the popover closes on the press, and the guard against the click that
follows was timed from the toggle event, which is queued and arrives
after that click. It is timed from beforetoggle now.
Browser tests with Livewire probes in Chromium, Firefox and WebKit, and
a render test for the keys and the keys of the child components after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
<x-group hint-class> adds classes to the hint, as the text fields'
hint-class does, and a validation message still replaces the hint.
<x-menu-item icon-class> adds classes to the leading icon, for an icon
whose colour means something of its own, such as a sport's glyph.
A colour class there has to win over the component's own colour, and
which of two colour utilities wins depends on the order Tailwind emits
them (text-error comes before text-on-surface-variant). So when either
prop is given, the component's own colour is written with a :where()
variant that carries no specificity, as the fields' hint colour sits
in the components layer. A disabled item's icon stays disabled. Without
the props the markup is unchanged. The skill now also lists the fields'
hint-class, which it had left out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
<x-menu> named its CSS anchor on the <span> around the trigger slot.
A trigger taken out of the flow, such as <x-button fab> fixed to the
bottom corner of a phone's window, left that span behind as an empty
box where the page put it, and the menu opened there. menu.js now moves
the name onto the menu button, beside any name the button already
carries for its tooltip, and moves it again whenever a Livewire morph
puts the server's attributes and a fresh name back. The wrapper keeps
the name until Alpine starts, or when there is no menu button.
A menu that fits neither below nor above its start edge now also tries
the opposite side and end together, so a default-position menu on a
FAB in the bottom-right corner opens above it, end-aligned. Before, it
fell back to its base position and overflowed the window.
This also applies to <x-fab-menu>, <x-account-menu>, <x-split-button>
and the <x-section-nav> picker, which share menu.js.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
An application that lets each person choose when their week starts and
how dates are written needs the picker to follow that choice rather
than the locale. <x-datepicker> takes week-start, 0 (Sunday) to 6
(Saturday), and format, dd, MM and yyyy each once around one delimiter
(dd.MM.yyyy, dd/MM/yyyy, MM/dd/yyyy, yyyy-MM-dd). They replace what
Intl derives for firstDayOfWeek() and inputFormat(), so the field, the
server-rendered value, the typed-date reader (year first too), the
calendar's columns and weekday header, Home and End, the dialog's text
fields and ranges all follow them. Month and weekday names stay the
locale's, and wire:model still stores Y-m-d. Values that are neither
are ignored, and without the props nothing changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
<x-section-nav> and <x-app-shell> mark an item current when its URL is
the request's. While a Livewire component on the page re-renders, the
request is Livewire's update endpoint, so no section or destination
stayed lit after the morph. Both now compare with the page's URL from
Livewire::originalUrl() during a Livewire update request; a normal
request compares exactly as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
An application with its own artwork for an empty collection puts it in
the `illustration` slot, which is drawn in place of the shape and icon.
The slot's attributes go on the element around it, so its class sets the
colour an SVG's currentColor takes. A slot holding only whitespace or
comments counts as empty (hasActualContent), and without the slot the
empty state renders as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
An application keeps a disclosure's state in a Livewire property
(`<x-collapse wire:model="fineTuning">`) or an Alpine one. Either binding
now works both ways: toggling the details writes the property, and the
property changing, in an action or in Alpine, opens or closes it.
`wire:model` is entangled as the dialog and sheets entangle theirs, and
taken off the element. The server renders `open` from the property, so
the first paint matches it. `x-model` binds through `x-modelable`, with
`open` as the first paint until Alpine starts. The state is named
`collapseOpen` so it never hides an `open` that a dialog in the slot reads
from a scope around it.
Without a binding the element has no Alpine and renders as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
The slot was cast to a string and escaped, so an icon beside a badge's
word showed as literal markup. It renders as HTML now; `value` stays
escaped, and a slot holding only whitespace or comments is still a dot
(hasActualContent rather than isNotEmpty).
`color="neutral"` draws neutral ink on every variant: a dot or count in
on-surface-variant with surface text (the ink the outline badge already
writes in), a tonal label in surface-container-high with
on-surface-variant text, and an outline in outline-variant.
`color="plain"` emits no background, text or border colour in any
variant, keeping shape, size and type, so an application's own colour
classes paint it without racing the badge's.
Every existing colour renders the same classes as before, and an unknown
colour still falls back to error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
A sticky toast stays until it is dismissed or its action pressed, for a
question that must be answered ("A new version is ready" with Reload).
It is kept aside rather than queued, so it never holds up ordinary
toasts: one that arrives while it shows takes its place, and the sticky
toast comes back once the queue is empty. One is kept at a time; a newer
sticky toast replaces it. Every other toast keeps today's queue.
An action's `event` names a window event dispatched when it is pressed,
beside `handler`, for toasts whose detail cannot carry a function. The
snackbar now closes before either runs, so a toast they show is not the
one dismissed.
`data-toast` and `data-toast-action` give applications stable hooks for
their tests.
The queue now forgets a cleared timer in next(): a toast dismissed by a
click that neither hovered nor focused it first, as a screen reader
activates a button, left its timer running and cut the next toast short.
The showcase's snackbar buttons had no Alpine scope and did nothing; they
are wrapped in one, with a sticky example.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
Components read the device's safe-area insets straight from env(), which a
browser test cannot fake and an application drawing its own status strip
cannot extend. Every inset the package reads, in its CSS and its views, is
now var(--material-safe-top|bottom|left|right, env(safe-area-inset-…)):
unchanged while the variables are unset.
The app shell's --material-bottom-bar also adds
var(--material-bottom-extra, 0px), so an application that docks something on
the phone's navigation bar (an offline banner) sets its height once and the
snackbar, a fab button and the page's bottom padding clear it.
A guard test fails on any env(safe-area-inset-*) outside such a variable, and
AppShellTest's assertion on the bar height's class follows the new value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
An app installed as a PWA, and a mobile browser, colour their bar from
<meta name="theme-color">, which the package left alone: the bar kept the
server's one colour whatever theme the visitor chose.
With the new `theme.meta` option (off by default), <x-theme-script> sets the
content of every theme-color meta without a `media` attribute to the
resolved theme's surface, from the scheme data and for the profile in
<html data-scheme>, adding one to <head> when the page has none. A
MutationObserver on <html> follows every later change of data-theme or
data-scheme: $store.theme's set() and toggle(), an OS change while
`system`, a profile preview, an application's own script. wire:navigate's
head merge puts the next page's server-rendered meta in place, so it is
painted again as the page is swapped in and on livewire:navigated. Turned
off, the script is byte-for-byte what it was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2