Commit Graph
171 Commits
Author SHA1 Message Date
Andreas Reinhold / reiniandClaude Opus 5 fb7007c976 Take Tailwind out of the package, and its detection out of the guard
tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
Tailwind left the stack in 2.0.0, but the package still carried about 330
mentions of it. What the guard's Tailwind detection protected — a class
that compiles to nothing — is now protected by a check that does not care
where a dead class came from.

DesignGuard: about 500 lines of Tailwind tables, scales, palettes and
"2.0.0 replacement" hints give way to one check — a class a view or PHP
file writes that neither the application's stylesheets nor the package's
own declare. It catches a utility of any framework, a typo and a class
whose rules were deleted alike, so it also found two classes ReStride
draws nothing with. A stylesheet has to be in reach for it: the `.css`
files among the scanned paths, or what the `missingStylesheets()` entry
imports. The class reader no longer mistakes an array index for a class
list (`$block['base']`), and it reads the array a class helper is given,
where it read nothing before.

The package's own three Tailwind self-guards go with it. Only their one
unique check stays, as a test of its own: every `matchMedia` width in
resources/js is an M3 breakpoint.

The pagination views are `material.blade.php` and
`simple-material.blade.php`; only Laravel's and Livewire's default theme
names ever made them `tailwind`. The provider sets `Paginator`'s default
views and switches `livewire.pagination_theme` to `material` when it is
still Livewire's own default, so no application can forget the config; a
theme an application chose, and a component's own `$paginationTheme` or
`paginationView()`, still win.

The rest is prose: the layer-order guidance for an application that still
builds Tailwind, the Tailwind wording in the README, the Boost guidelines
and the development skill, and about 25 "this used to be a Tailwind
utility" comments, along with every "plan step NN" pointer into a
gitignored folder. The reset keeps its credit, and NOTICE now carries it
too.

Feature suite 1159 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 21:07:39 +02:00
Andreas Reinhold / reiniandClaude Opus 5 247c596c3a Cut duplicated and speculative code across the package
An over-engineering audit of the whole tree, applied in five reviewed
batches. Behaviour stays the same except where UPGRADE.md says otherwise.

PHP: the showcase and error-page stylesheets are prebuilt into
resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import
(first occurrence kept, the order an application's build gives), instead
of Stylesheets::bundle() inlining imports on every request; only the
import walk DesignGuard needs stays. SchemeStylesheet::withProfiles()
replaces three copies of the scheme-plus-profiles loop, material:scheme
leaves spec and contrast checks to the node script that already made
them, and the error page's scheme cache, the hashed view namespace, the
translations path with no lang/ folder and DesignGuard's 1.x-name hints
are gone.

JS: the androidx shape port progress.js and both bin scripts each carried
lives once in resources/js/shapes.js (the generated SVGs are unchanged);
util.js holds ringIndex(), ms(), reopenGuard() and remember(), which
were written out several times; listeners are released through
AbortController; tooltip.js's hoverPopover() serves the rich tooltip too.

CSS: every rule for an element inside the navigation rail queries
`--md-navigation-rail-value` instead of repeating the seven collapsed
conditions under five media branches; badge, alert, progress, slider and
button read one non-inheriting colour-role table (components/color.css);
the dialog chrome, the submenu's popover chrome, the chip's state layer
and touch target, and the visually-hidden inputs use the shared rules
they copied; foundation/tokens.css is folded into foundation.css.

Views: Support\Field and Support\Link replace the error-key, bound-value
and link-attribute blocks copied into the fields and link components;
the timepicker period group, the menu filter and the showcase head are
partials; the datepicker's steppers and entry fields are loops; component
docblocks no longer restate SKILL.md.

Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces
four per-group files, DesignGuardTest and the layout-component tests use
datasets, browser tests share one ready() helper, CSS parsing lives in
ComponentStylesheet alone. docs/audits and the finding IDs citing it are
removed, as are pestphp/pest-plugin-laravel, the unused composer scripts
and check:font; the lint job runs in the feature job, which now installs
node packages so the prebuilt-stylesheet staleness test runs in CI.

Feature suite 1177 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:29:21 +02:00
Andreas Reinhold / reiniandClaude Opus 5 471d927e64 Count a row written by hand in the design guard's stylesheet checks
`data-md-list-row` on an application's own `<li>`, `<div>`, `<tr>` or
`<x-row>` draws its hover, focus and press state layer, its cursor and
its `data-md-selected` fill from list-item.css, but missingStylesheets()
and unusedStylesheets() read only component tags and `->links()`.
ReStride had the stylesheet only because table.css imports it: had its
last `<x-table>` gone, every hand-made row would have lost its states
and its fill without a finding, and importing list-item.css for the
rows directly was reported unused.

A Blade view outside the package's own `resources/views` that writes a
hook in the new `HOOK_STYLESHEETS` now needs that hook's stylesheet in
both checks, named at the earliest line that needs it. The one entry is
`data-md-list-row` → `components/list-item.css`, except on `<x-card>`
(any spelling), whose row card.css draws; the card's opening tag is
blanked out before the hook is read, and a selector such as
`[data-md-list-row]` or a name in backticks writes no row. Nothing else
list-rows.js, the skill or the README has an application write needs a
stylesheet of its own: `data-md-list-open` and a row's
`data-md-selected` are drawn only through the row, a selected row in
`<x-table>` by table.css, `data-md-dragged` by foundation.css's
`md-state-layer` or card.css, `data-md-field-control` by field.css
inside `<x-field>`. The constant's docblock says so, and the guard's
docblocks, the README, the skill and UPGRADE.md describe the check.

A fixture test reads a hand-made `<li>` and `<tr>` row and a card-only
view (plain and namespaced `<x-card data-md-list-row>`, and a selector
in a script) against both checks; it fails without the change. A
second test scans the package's list-item and card views and expects
no row finding; it fails when package views are read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 08:05:33 +02:00
Andreas Reinhold / reiniandClaude Opus 5 b62c83f244 Show no tooltip on the control a sheet or dialog focuses as it opens
A modal side sheet, bottom sheet and the modal rail move the focus to
their first control as they open (x-trap), and `<x-modal>` does in
`showModal()`. On a page loaded with the sheet already open (a
`wire:model` set from `?workout=` in the URL) the browsers count that
focus as `:focus-visible`, and so they do for a sheet or dialog opened
from the keyboard, so tooltip.js showed the close button's "Close"
tooltip the moment the layer appeared, over its title, with no keyboard
use. Confirmed in Chrome, Firefox and Safari for the deep-linked sheet
and a full-screen dialog opened with Enter; a deep-linked `<x-modal>`'s
own focus was not counted as keyboard focus. M3 lets a tooltip appear
on hover or focus and says nothing that asks for one on a focus the
component moves itself.

layers.js gains `openingFocus(event)`: a focus is a modal layer's own
when the nearest open dialog, `aria-modal` panel or modal rail around
its target is entered from outside it, or from nowhere, and no Tab is
moving it (a Tab moves the focus while its keydown is handled, by the
browser or by focus-trap wrapping round the panel). tooltip.js and
rich-tooltip.js leave such a focus out; focus moved within the layer,
a Tab onto the control and a focus returned to it from a menu inside
still show the tooltip. The docblocks, the skill and UPGRADE.md say so.

A browser test loads a page with a modal sheet open at 393px, opens a
full-screen dialog and a dialog whose first control is a rich tooltip's
trigger from the keyboard, and checks that no tooltip is up on the
focused control, then that Tab wrapping round to the close button shows
its tooltip; it fails without the change in Chrome, Firefox and Safari,
each part on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 07:16:32 +02:00
Andreas Reinhold / reiniandClaude Opus 5 7aa6dc7d33 Take full on the searchable choice and the pickers, as on every field
`full` takes the 40rem bound off `[data-md-field]` from `medium`, and
`<x-input>`, `<x-password>`, `<x-textarea>`, `<x-select>` and `<x-file>`
pass it to the field they draw. `<x-choices searchable>`,
`<x-datepicker>` and `<x-timepicker>` draw the same field but did not
declare the prop, and their roots only forward `class` and `style`, so
`full` reached no element and the field stopped at 40rem: ReStride's
time zone choice stood narrower than the rows filling its card.

The three now declare `full` and pass it to their field (the date
picker's own field, not the dialog's entry fields). `<x-choices>`' chips
are not a field and have no bound, so they take `full` and render
nothing for it, and a call site can switch `searchable` on and off
without it. The docblocks and the skill list every component that takes
it.

The browser test that bounds a field at 600px now measures a bounded
and a `full` searchable choice, date picker and time picker too; it
fails without the change in Chrome, Firefox and Safari. A feature test
renders `full` on every field-shaped component and none on the chips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 07:04:12 +02:00
Andreas Reinhold / reiniandClaude Opus 5 6d4487feb3 Hide a disabled fab button on a compact window, as M3 removes a FAB
`<x-button fab>` is an extended FAB below `medium` and a filled button
from there, but a disabled one was still drawn as the FAB below
`medium`: the compact FAB's rule set no state of its own, so the button
stood greyed out and fixed over the content. M3 never disables a FAB:
"if its action is unavailable, remove the FAB entirely".

A `fab` given `disabled` (a `<button disabled>`, or a link's
`aria-disabled="true"`) now renders `data-md-unavailable`, and
button.css draws it `display: none` below `medium`, which also takes it
out of the accessibility tree and the Tab order; from `medium` it is
the disabled filled button it was. The mark comes from the prop, not
from `:disabled`, because a `spinner` puts `disabled` on its button
while the action runs, and that FAB is busy rather than unavailable:
hidden, it would vanish instead of showing its loading indicator. The
docblocks and the skill's button table say so.

A browser test renders an enabled fab with a slow spinner action, a
disabled one and a disabled link on a 393px window: the disabled two
are not rendered, the busy one stays on screen with its indicator, and
at 600px both disabled ones are the disabled button again; it fails
without the change in Chrome, Firefox and Safari (and in Chrome with
`:disabled` alone as the condition, on the busy FAB). A feature test
reads the mark and the rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 07:00:40 +02:00
Andreas Reinhold / reiniandClaude Opus 5 cf63f11bfb Keep a switch's track whole beside long text in a row
`[data-md-toggle] { min-width: 0 }` let the switch's root shrink below
its 52px track as a flex item. In `<x-row justify="between">` beside a
paragraph, flex shrinking weighs each item by its width, so the root of
a bare switch gave up a share of its 52px, and a switch with a long
label of its own gave its label a column one word wide and less; the
track, which does not shrink inside the root, ran out of it and past a
card's edge on a phone. `<x-checkbox>`'s root did the same with its box.

Both roots now keep their automatic minimum, the control and, beside a
label, its longest word, so the text beside the control, or the
control's own label, wraps instead. The toggle's docblock and the skill
say so.

A browser test lays a bare switch, a labelled switch and a bare
checkbox beside long text in rows on a 393px window and checks each
control's size and that it stays inside its row, and that the switch's
own label wraps inside its root; it fails without the change in Chrome,
Firefox and Safari.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 06:55:36 +02:00
Andreas Reinhold / reiniandClaude Opus 5 1885c3e54e End a select's long value before its arrow, with an ellipsis
Where the browser has the customizable select, select.css made the
select `display: flex` with `text-overflow: ellipsis`, but the closed
select draws its value in a block of its user-agent shadow tree: a flex
item at its automatic minimum width that takes no `text-overflow` from
the select, and no author style reaches it (`display: block` on the
select does not help either). So a long option in a narrow field, or
at `size="sm"` and `xs`, ran on past the end padding under the arrow,
which `overflow: hidden` only clips at the select's edge. The native
select in Firefox cut the value at the padding with no ellipsis.

The view now gives the select a first child `<button type="button"
wire:ignore>` holding `<selectedcontent>`, the customizable select's own
copy of the chosen option, and select.css makes that the flex item that
shrinks and ends in an ellipsis; the native select takes `nowrap`,
`overflow: hidden` and the ellipsis on itself. `wire:ignore`, because a
Livewire render parses the server's empty element into the first
option's label and morphed it back over the chosen one. A browser
without the customizable select never renders the button; `type` keeps
it from being a form's default button.

A browser test measures the value against the arrow in md, sm, xs,
filled and icon selects 240px wide, and again after choosing another
option re-renders the component; it fails without the change in Chrome
and Firefox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 06:50:53 +02:00
Andreas Reinhold / reiniandClaude Opus 5 207270a23d Stick a standard side sheet under the top safe area and the scaffold's sticky app bar
The standard sheet was sticky at `top: 0` and `100dvh` tall, so in an
installed app with a status bar its head row and close button slid under
the status bar once the page scrolled, where the modal sheet already
starts at `--material-safe-top`. Inside `<x-scaffold>` a sticky
`<x-app-bar>` in the `top` slot spans the content region, sheet and all,
and covered it the same way, safe area or not. The sheet now sticks at
`--drawer-top`, the top safe area, or under a scaffold's sticky bar that
safe area plus the bar's 64px row (just the row, if taller than the safe
area, for a medium or large bar, which collapses to it), and is as tall
as the window below that. Every side sheet keeps the bottom safe area
inside its bottom padding. A browser test scrolls a standard sheet in a
scaffold with no bar, a small bar and a large bar at a 47px top and 34px
bottom inset and measures where it sticks, its foot, its close button
and its padding; it fails without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 06:06:23 +02:00
Andreas Reinhold / reiniandClaude Opus 5 94a376a4fd Close only the topmost layer on Escape, and keep a layer opened over a sheet readable
A modal side sheet, bottom sheet or the modal rail closed on any Escape
the window heard, so a dialog opened from a sheet, a menu, select list
or searchable choice inside one, a sheet opened from a sheet, and a
sheet inside a dialog each closed two layers on one press. And a dialog
or a second sheet rendered elsewhere on the page sat inside the
`aria-hidden` the first sheet's `x-trap.inert` put on its siblings, so a
screen reader could not read it, while the sheet's focus trap took every
Tab inside the dialog back to the inert sheet.

resources/js/layers.js adds `x-layer`, on each of those panels beside
its `x-trap`. An Escape is the panel's only when nothing has handled it
and the nearest open layer around its target is the panel itself - not
an open dialog, popover or customizable select, nor a panel inside it;
the panel claims it with preventDefault(), which also keeps a dialog
around it from cancelling, and dispatches `material-escape`, which the
views close on. A panel that opens lifts `aria-hidden` from its own
ancestors and puts it back on close only where a panel still open hides
them; materialShowModal() does the same for `<x-modal>`, whose new
`x-trap.noautofocus.noreturn` pauses the sheet's focus trap while it is
open and moves no focus of its own. The searchable choice, the search
view and the supporting pane's sheet now preventDefault() the Escape
they act on, so the dialog or sheet around them stays.

Four browser tests stack the layers every way above and fail without
the change in Chrome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 05:59:32 +02:00
Andreas Reinhold / reiniandClaude Opus 5 0641071a06 Leave a Markdown mail component's classes to the mail theme in the design guard
A mail component is drawn by the mail theme, not by the application's
CSS entry, and the theme's classes share Tailwind's names: ReStride's
`x-mail::sessions` wraps its table in `<div class="table">`, the class
the package's own mail theme styles, and the guard reported it as
Tailwind's `display: table`. A view under a path `mail.markdown.paths`
names (Laravel's `resources/views/vendor/mail` by default) now skips
family (i) and the breakpoint, scale, palette and colour-value checks;
its icon names, the directives in its component tags, forbidColours()
and forbid() are still read. A mail theme stylesheet under that path,
which has to write literal values, is left out of check (iii) and of
the exempt classes, so its `.table` no longer hides `table` in the
application's other views either. The guard's docblock, the README and
the skill's "Testing the design" say so; a fixture test reads the same
mail view and theme with and without the path configured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 04:18:17 +02:00
Andreas Reinhold / reiniandClaude Opus 5 b7e16476cd Fill a selected row written by hand in secondary-container again
1.x drew `data-selected` on any `data-list-row` that was not a card in
secondary-container with on-secondary-container ink, whatever element
the row was. 2.0.0 kept that only for `<x-list-item selected>` and a
table row's `aria-selected="true"`, so an application's own `<li>`,
`<div>` or `<tr>` with `data-md-list-row` and `data-md-selected` - the
contract list-rows.js still describes - lost its selected state without
a word. list-item.css now fills such a row, cards and list items
excluded, and names the fill `--md-list-row-fill`, so hover, focus and
press tint it as they tint a selected list item rather than replacing
it (1.x's selected fill hid them). In `<x-table>`, `data-md-selected`
selects a plain row too, beside `aria-selected="true"`, which ARIA
allows on a row only in a grid. A browser test compares a hand-made
selected `<li>` and a plain selected `<tr>` with the role pair in the
light and dark themes, checks that a selected card keeps its own fill
and that hover tints the row's fill; it fails without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 04:18:10 +02:00
Andreas Reinhold / reiniandClaude Opus 5 613ac20013 Report a colour utility on the application's own theme colour in the design guard
The guard knew M3's roles and Tailwind's palette, so `bg-off-plan`,
`text-sport-run`, `from-brand` or `border-l-zone-4` - colours an
application's own Tailwind theme named before 2.0.0 - were the leftovers
its count never showed, though none has compiled since. After every
other family, a class-list token shaped like a colour utility on a name
that starts with a letter is now reported with the application's own
`var(--...)` as its replacement (`color-mix()` with an opacity), unless
the application's CSS declares the class.

So that none of Tailwind's own utilities on those prefixes reads as a
theme colour, each one Tailwind 4.2 lists is now named by its family:
`border-collapse`, `border-spacing-*`, the block borders `border-bs`/
`border-be`, `bg-blend-*` and the other background positions and
angles, gradient stops, `fill-none`/`stroke-*`, `text-shadow-*`,
`shadow-inner`, `accent-auto`, `inherit`/`initial` as colour keywords,
and the `mauve`, `olive`, `mist` and `taupe` palettes; only
`inset-ring-*` is still not read. BoostVocabularyTest no longer reads
a span holding a single property, prop or role name (`border-color`,
`placeholder-value`, `outline-variant`) as a class list. A fixture
test covers the theme colours and the Tailwind names beside them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 04:18:10 +02:00
Andreas Reinhold / reiniandClaude Opus 5 394c9326ac Flip what the rail on the page draws from $store.rail.toggle()
toggle() was set(!collapsed), and while nothing is stored `collapsed`
is only `rail.default`: an adaptive rail between 840 and 1199px is
drawn collapsed whatever that says, so an application's shortcut
collapsed a collapsed rail and the first press changed nothing. Each
interactive rail now registers its root, and toggle() asks the first
one on the page, the way its menu button asks itself: an open rail is
hidden, a rail with no room to expand (modal, or adaptive below
expanded) opens over its scrim without storing anything, and any
other rail stores the opposite of what it draws, so a rail that hid
itself docks back into the layout as before. With no rail on the
page it still flips the choice. collapse() and expand() stay the
choice itself. The browser tests toggle the scaffold's rail at 960
and 1512px (in place) and at 768 and 393px (the modal), and a hiding
rail back into the layout; the first two fail without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 03:50:00 +02:00
Andreas Reinhold / reiniandClaude Opus 5 f2a3c4d056 Size the loading indicator and publish the rail's value
Two gaps found rewriting the Boost skill for 2.0's plain CSS, each
closed the way M3 describes it.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:06:16 +02:00
Andreas Reinhold / reiniandClaude Opus 5 24eb811f34 Teach 2.0's plain CSS in the Boost guidelines and skills
2.0.0 took Tailwind out of the stack, but the texts Boost copies into
every application still taught its utilities: the core guideline said
"built on Tailwind CSS", the always-on material-3 guideline wrote every
rule as `bg-primary`, `type-*`, `rounded-corner-*`, `state-layer` and
`medium:`, and both skills' tables and examples did the same. None of
those classes exists in 2.0's stylesheets, so an agent following the
guideline wrote markup that compiled to nothing. Found moving ReStride
onto 2.0: its CLAUDE.md, and SealShare's, carry these texts.

The guidelines and skills now teach what 2.0 has: component and layout
component props (`gap="space200"`, `hide-from="medium"`), the `md-type-*`,
`md-ink-*` and interaction classes, and `--md-sys-*` tokens in the
application's own CSS, with breakpoints as range media queries.
UPGRADE.md §1, §2 and §4 describe the finished move instead of the
in-between state, and header comments that pointed at the removed
tokens/utilities.css and tailwind.css, or called a component "still
Tailwind", say what is true now.

BoostVocabularyTest runs DesignGuard over the code in every shipped
guideline and skill (fenced Blade and CSS, and each inline class list),
so a text that teaches a class the stylesheets do not define fails the
suite; it finds 268 violations in the texts as 2.0.0 shipped them. The
guard's own table of what it reports is exempt. BoostResourcesTest now
asks the design skill for each breakpoint's prop value and media query
instead of the removed `medium:` variants.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 11:07:54 +02:00
Andreas Reinhold / reiniandClaude Opus 5 70c983b7bb Inline the error layout's own rules beside the app's build
Plan step 46, from SealShare's breakpoint walk: an application imports
the stylesheets its own views render, and none renders the error layout,
so under a real build its 403/404/500 pages drew unstyled. With a build
the page now keeps the app's Vite tags (foundation, tokens, scheme, font)
and inlines Stylesheets::bundle() of components/error-page.css and its
imports beside them (ErrorPage::layoutStyles(), cached). Without a build
the fallback is unchanged. An application imports nothing for its error
pages; one that imports all.css gets the same layered rules twice, which
is harmless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 16:10:01 +02:00
Andreas Reinhold / reiniandClaude Opus 5 05f115ab36 Report the stylesheets an entry imports that no view needs any more
missingStylesheets() names an import the views need, but nothing named
one they had stopped needing: when SealShare's last card left its views,
card.css stayed in every page until a reviewer noticed by hand.
unusedStylesheets($cssEntry) reads the entry's direct package imports
and reports each one no scanned view needs, counting what a needed
stylesheet imports itself and foundation.css always; an entry that
imports all.css is left alone. Opt-in, beside missingStylesheets()
(plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 14:31:01 +02:00
Andreas Reinhold / reiniandClaude Opus 5 ff5349759b Let a card's title take the heading level the page needs
<x-card> always titled itself with an <h3>, so a card straight under a
page's <h1> skipped a level, which SealShare's download page did with
no way round it from the application. `heading` takes h2 to h6, or div
or p for a title that is not a heading, as <x-pane> and <x-app-bar>
already do; h3 stays the default. The skill also says a card holds one
subject, as M3 does (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 14:09:06 +02:00
Andreas Reinhold / reiniandClaude Opus 5 f307079a45 Name the monospace faces once, as a typeface token and md-mono
Both applications show values a person reads or types character by
character: SealShare's share tokens and recovery codes, ReStride's file
and folder names and its keyboard shortcuts. The package wrote the
platform's monospace stack twice (the reset's <code>, <kbd>, <samp> and
the mono field) and gave everything else nothing, so an app copied the
stack by hand. --md-ref-typeface-mono now holds it, read by the reset,
the field and a new md-mono text class; the guard points font-mono at
<code> or md-mono. Not an M3 token: M3 names a brand and a plain
typeface and nothing for code. Decided with the user (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 12:56:55 +02:00
Andreas Reinhold / reiniandClaude Opus 5 92d3d761f3 Keep a button at its label's width in a stack and a form
<x-stack> stretches its children across by default and <x-form> is a
one-track grid, so a button written directly in either drew as a
full-width pill. M3 keeps a button's width "dynamic to fit label" and
says not to "stretch buttons into long flat shapes on large windows".
A button there now sits at the start edge at its own width; a stack
aligned start, centre or end is untouched, and an application's own
width still wins (a cap on the button itself was rejected for exactly
that: it would have limited an app's inline-size, which CascadeTest
guards). Chromium test in both directions, failing without the rules.
Decided with the user (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 12:56:45 +02:00
Andreas Reinhold / reiniandClaude Opus 5 f5f040516e Smooth the page's text in the foundation, not per surface
The showcase and the error pages set grayscale font smoothing on
themselves, but the foundation did not, so an application that dropped
Tailwind's antialiased class rendered heavier text on macOS than the
package's own pages, silently. base.css now sets it on the page; the
showcase's and error page's copies go, and the guard tells a leftover
antialiased class to go too. Decided with the user (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 12:41:20 +02:00
Andreas Reinhold / reiniandClaude Opus 5 6614b32fd3 Document the icon's size and mirror-rtl props in the skill
The skill still sized an icon with Tailwind's size-* classes and paired
optical="20" with size-5 by hand. 2.0.0's <x-icon> takes size in px and
picks the 20 cut from it; SealShare's settings rewrite found the prop in
the view, not the skill (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:51:51 +02:00
Andreas Reinhold / reiniandClaude Opus 5 a5ad6af698 Name what a component tag takes in place of each Blade directive
The guard said a directive does not compile inside a component tag but
not what to write instead, and the skill's note said only "use {{ }}".
SealShare's two-factor challenge had @js in an x-data on <x-stack>, and
its implementer had to work out {{ Js::from() }} from Laravel's source.
Each message now names the form: Js::from for @js, json_encode for
@json, Arr::toCssClasses and toCssStyles for @class and @style,
$wire.entangle for @entangle, a bound attribute for @disabled and its
kin, and a prop, an echo or a plain element for control structures
(plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:13:49 +02:00
Andreas Reinhold / reiniandClaude Opus 5 d0bed0da69 Lift the snackbar above a toolbar placed at the bottom
SealShare's only navigation is a floating toolbar at place="bottom". In
1.x it set --material-bottom-bar to lift the snackbar over it; in 2.0.0
the toolbar reads that variable to place itself, so an application can
no longer set it, and the snackbar landed on the toolbar. M3 nudges a
snackbar "upward to avoid overlapping FABs/docked toolbars" and never
puts one in front of navigation.

A bottom-placed toolbar now publishes --material-bottom-toolbar, the
distance from the window's bottom edge to its top: floating 64px (as
tall as an md or lg FAB beside it), docked 64px plus the safe area it
pads, rounded docked floating from 840px. It is declared on :root and
again on the scaffold's root, the only place its formula sees the bar.
The snackbar clears whichever of the bar and the toolbar reaches higher,
and a page pads its end with it. Two Chromium tests, both failing on the
old formula: a docked toolbar on a scaffold's navigation bar, and a
floating toolbar without a scaffold (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 10:47:50 +02:00
Andreas Reinhold / reiniandClaude Opus 5 87004a9035 Document the design guard as it now stands
Plan step 41 review: README and the development skill describe the API
that exists after the review, briefly — what scan() reads, the family
table with the M3 spacing step and 1.x utilities, missingStylesheets()
reading imports only, forbidColours() on the 2.0.0 vocabulary — and
UPGRADE.md lists the removed checks and methods and the retargeted
forbidColours() (the user, 2026-09-15).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 08:51:32 +02:00
Andreas Reinhold / reiniandClaude Opus 5 d5972b8ddb Retarget forbidColours() at where 2.0.0 writes a role
Plan step 41 review (the user, 2026-09-15): an application without Tailwind
names a role through its --md-sys-color-* custom property (CSS, an inline
style, a script), an md-ink-* class, or a component's color/tone prop, not
a bg-tertiary utility. forbidColours() keeps its name and signature and
reports each of those, the role's on- and container roles included;
a Tailwind leftover stays family (i)'s single report. ReStride's
forbidColours(['tertiary', 'primary-container']) keeps its meaning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 08:35:06 +02:00
Andreas Reinhold / reiniandClaude Opus 5 6dc996afdf Retire forbidAbsolutes() and forbidOpacityInk() from the design guard
Plan step 41 review (the user, 2026-09-15): without Tailwind, bg-white and
text-on-surface/60 compile to nothing like any other utility, so family (i)
now reports every colour utility on an M3 role, a 1.x ink name
(text-meta, border-divider), white, black or current, with or without an
opacity, each with its 2.0.0 replacement (an md-ink-* class, <x-divider>,
<x-surface level>, md-state-layer or the role's var()). Check (iii) reads
named colours anywhere in a declaration's value (border: 1px solid white),
hex codes only in values (an id like #add no longer matches), and masks
strings and url() contents first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 08:33:20 +02:00
Andreas Reinhold / reiniandClaude Opus 5 c047fefd8b Remove the design guard's maryUI and daisyUI checks
Plan step 41 review (the user, 2026-09-15): SealShare and ReStride have
both left maryUI and daisyUI, and no application of 2.0.0 carries either,
so the <x-mary-*> tag check, the daisyUI class and colour lists and the
Tailwind look-alike exclusions they needed go, with their fixture lines
and the mentions in the header, README and development skill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 08:28:35 +02:00
Andreas Reinhold / reiniandClaude Sonnet 5 6147221b89 Document the design guard's 2.0.0 API in the README and development skill
Plan step 41: short updates only (the full docs rewrite is Phase H) --
missingStylesheets(), the family-table replacements now named (a layout
component and prop, an md-* class, or a var(--md-sys-*) token instead of a
dead Tailwind utility), and a note that forbidAbsolutes()/forbidOpacityInk()
matter mainly to an application still migrating off Tailwind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 08:18:41 +02:00
Andreas Reinhold / reiniandClaude Sonnet 5 f21943746e Delete Tailwind's half and build the Workbench without it
Plan step 39 (parts 1-3, folded into one commit: PHP tests read the
deleted files directly, so they cannot land apart from it). Tailwind
leaves the whole stack:

- Delete resources/css/tailwind.css, tokens/theme.css and
  tokens/utilities.css. Neither token file declared an --md-sys-*
  custom property of its own (both only referenced tokens declared
  elsewhere), so nothing loses a value; the md-* interaction and text
  classes already mirror utilities.css's declarations exactly
  (foundation/interaction.css, text.css).
- npm uninstall tailwindcss @tailwindcss/vite; vite.config.js drops the
  plugin and its import; composer.json drops the tailwindcss keyword
  (no lock change — keywords are outside Composer's content hash).
- The Workbench now builds one CSS entry, workbench/resources/css/app.css
  (all.css, showcase.css and the scheme; package.css is folded in and
  removed) instead of two, used by ErrorPage::assets() and every
  browser-test probe page's raw @vite() call; the showcase's own pages
  still take their CSS from the bundle route.
- TokensTest and StylesheetsTest: the two facts theme.css and
  utilities.css carried (every scheme role becomes a colour, resolved
  on the element; md-type-* matches the type-* utilities) are asserted
  directly against the scheme and text.css now that there is no second
  copy to cross-check; StylesheetsTest gained a full-tree scan (every
  .css file under resources/css/ is reached from all.css or
  showcase.css, no exclusions left for Tailwind); the Workbench-entry
  test and every "moved out of tailwind.css" assertion updated for the
  single entry and its removal.
- DesignGuard.php's comments and the development skill's setup section
  no longer name the deleted files or a second Tailwind entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 06:49:03 +02:00
Andreas Reinhold / reiniandClaude Opus 5 3365730610 Import the components in the documented installation
Plan step 37 review. Removing material.css left README.md, the
development skill, UPGRADE.md and the tailwind.css and foundation.css
headers telling an application to import foundation.css, tailwindcss and
tailwind.css in its place. material.css was also what brought layout.css
and components.css, so that install rendered every component unstyled,
and without a layer statement Tailwind's preflight would have outranked
the package's layers.

The skill's snippet is now the Tailwind-free one 2.0.0 ends with: the
foundation, then the stylesheet of each component the views render, or
all.css. README.md, whose layout and prose still assume Tailwind, shows
all.css in its own entry and Tailwind in a second, both opening with the
layer statement, the shape the Workbench now builds; the skill and
UPGRADE.md describe that interim in a sentence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 03:15:05 +02:00
Andreas Reinhold / reiniandClaude Sonnet 5 a967cfcc2b Remove material.css, the 1.x single-import shortcut
Plan step 37: an application on 1.x-style imports now imports
foundation.css before @import 'tailwindcss' and tailwind.css after it,
the way foundation.css's own header has described since Phase F
started — material.css only ever saved writing those two lines.
Updates every place that named it: tailwind.css's and
foundation/hidden.css's and tokens/utilities.css's header comments,
the development skill's install snippet and Tokens section, README.md's
install snippet, and a new line in UPGRADE.md's migration notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 02:49:43 +02:00
Andreas Reinhold / reiniandClaude Sonnet 5 cbd7d01c62 Sweep up navigation's last data-navigation-* references
Plan step 36 (navigation group, second batch, cleanup). Every leftover
data-navigation-*, data-tall, data-hide-on-scroll, data-hidden,
data-width, data-align, data-divider, data-fill and component-level
data-open reference outside the rewritten stylesheets and views moves
to data-md-*, in the files the two component commits left alone:
tests/Feature/Components/ScaffoldTest.php (the scaffold's own rewrite
is a later batch, but it renders the bar and the rail today and asserts
their hooks); tests/Browser/NavigationTest.php, ShowcaseTest.php and
ColourProfilesTest.php (selectors only — document-level attributes,
data-rail, data-rail-auto and data-app-shell*, keep their names).
NavigationTest.php also gets the browser tests the plan owed this
group (docs/plans/material-3-browser-tests.md § Navigation): a tall
bar's vertical layout at a width where the short bar would go
horizontal; hide-on-scroll never firing while a pinned snackbar is on
screen; a narrow rail's 80px width and centred destinations; a rail
that hides when collapsed leaving the layout and coming back only
through an application's own menu button.

resources/css/components/navigation.css and its import in tailwind.css
are deleted now that nothing imports the file any more (the previous
commit's message said this already happened there; it did not — this
is where it actually lands).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 00:09:44 +02:00
Andreas Reinhold / reiniandClaude Opus 5 b6259d45f3 Hide a standard side sheet's scrim from 840px again
Plan step 36 review of 9dd527f3. The old view hid a standard sheet's
scrim with `expanded:hidden`; the stylesheet lost that rule (and the
render test lost its assertion), so an open standard sheet at 840px and
up laid a full-window scrim over the page it sits beside, one click from
closing. The rule is back, pinned by a test.

The same review: the scrim now fades out as well as in (with no closed
opacity, allow-discrete only held it at 32% for 200ms and then dropped
it); the modal sheet slides without the fade the old enter and leave
classes never had; it slides from its own edge in a right-to-left
document, which translate-x-full never did; the state rules match the
root's own sheet and scrim, so a sheet nested in an open one keeps its
state; the script's breakpoint is 840px, not 52.5rem, which disagreed
with the stylesheet's px query once the reader's text size grew; and
`width` and the standard sheet's cap are M3's 400px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 22:04:13 +02:00
Andreas Reinhold / reiniandClaude Sonnet 5 9dd527f303 Rewrite the drawer (side sheet) without Tailwind, remove pane
Plan step 36 (containment group): <x-drawer>'s class lists move into
resources/css/components/drawer.css, keyed on data-md-drawer (with
data-md-open, data-md-standard) and data-md-drawer-scrim/-sheet
(data-md-side)/-head/-head-row/-heading/-title/-subtitle/-close/-body/
-actions. The modal sheet slides in on the emphasized-decelerate
easing and out on emphasized-accelerate (an asymmetric CSS transition
expressed by giving the open-state selector its own
transition-duration/-timing-function, which the "after-change style"
rule picks up); the sheet stays x-show-driven, with @starting-style +
transition-behavior: allow-discrete taking the place of x-transition's
enter/leave classes, matching resources/css/components/search.css's
already-rewritten scrim/view. A standard sheet drops the scrim and
trap from 840px and only fades in place (an in-flow box does not
travel); data-md-drawer-collapsed (from the view's `wide` Alpine
state) takes it out of the layout while closed there.

<x-drawer pane> is removed: step 35's <x-list-detail> is M3's form of
that job (the plan's decision, docs/plans/material-3-alignment.md
"Decisions"). Removed the prop, its CSS classes and its tests
(OverlayTest's two pane-only tests deleted, the others' pane cases
dropped); pointed each mention at <x-list-detail> in the material-3
guideline, the material-3-design and livewire-material-development
skills, and added a line to UPGRADE.md's 2.0.0 notes. No other living
file named `<x-drawer pane>` (grepped views, tests, README, boost,
workbench excluding build output); docs/plans and docs/audits keep
their history unchanged.

Imported from the Containment block of components.css, importing
button.css and divider.css for what the view renders.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 21:07:04 +02:00
Andreas Reinhold / reiniandClaude Sonnet 5 f04e31b3aa Rewrite the card without Tailwind
Plan step 36 (containment group): <x-card>'s class lists move into
resources/css/components/card.css, keyed on data-md-card (per variant),
data-md-card-figure/-body/-header/-heading/-title/-subtitle/-menu/
-content/-actions. A row (data-md-list-row) renders the shared
md-state-layer and md-focus-ring classes for its tint and ring
(foundation/interaction.css) instead of copying their rules; card.css
adds only the per-state elevation the shared class has no opinion on
(ElevatedCardTokens.kt/FilledCardTokens.kt/OutlinedCardTokens.kt), the
press/hover exclusion for a card's own nested buttons, and the ring for
a non-actionable row whose focus lands on its opener rather than the
card. data-md-dragged is read automatically by the shared class's own
16% tint; card.css only adds its elevation levels 3/4.

Hooks renamed data-list-row -> data-md-list-row, data-list-open ->
data-md-list-open, data-list-actionable -> data-md-list-actionable,
data-dragged -> data-md-dragged, updated in the same commit:
resources/js/search.js and list-rows.js's consumers, table.css's
comment, the showcase (index, data and containment sections),
tests/Browser/ContainmentTest.php, and the development skill's card
and table examples.

Imported from the Containment block of components.css.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 20:30:14 +02:00
Andreas Reinhold / reiniandClaude Opus 5 682029b813 Name the snackbar's renamed hooks in the development skill
Plan step 36. The toast rewrite renamed data-toast and data-toast-action
to data-md-toast-snackbar and data-md-toast-action, added
data-md-toast on the host and data-md-toast-dismiss on the close button;
the skill still told applications to target the old names in tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 17:10:11 +02:00
Andreas Reinhold / reiniandClaude Opus 5 97ba035b68 Let a rich tooltip linger after the focus leaves too
Plan step 36. The rewrite moved the rich tooltip's pointer-leave grace
from 200ms to M3's 1.5s — kept: the reference gives plain and rich
tooltips the same 1.5s, and ACT-25 names this file's 200ms. Its header
claimed the same for the focus, which still hid the bubble at once; the
focus now leaves on the same 1.5s the plain tooltip uses, and the skill
says so. The stylesheet header no longer cites chip.css and fab-menu.css
for a pulled-back actions row neither has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 17:09:33 +02:00
Andreas Reinhold / reini 82ed683df4 Merge branch 'worktree-agent-a41c756369d016a29' 2026-09-14 15:40:58 +02:00
Andreas Reinhold / reiniandClaude Opus 5 0013e0154d Rename the app shell to the scaffold and give it a FAB slot
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
2026-09-14 14:51:48 +02:00
Andreas Reinhold / reiniandClaude Opus 5 acd6ba21ae Add the feed canonical layout
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
2026-09-14 14:51:30 +02:00
Andreas Reinhold / reiniandClaude Opus 5 9eb06323aa Add the supporting-pane canonical layout
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
2026-09-14 14:51:22 +02:00
Andreas Reinhold / reiniandClaude Opus 5 b5c5be1fd7 Add the list-detail canonical layout
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
2026-09-14 14:51:13 +02:00
Andreas Reinhold / reiniandClaude Opus 5 b4f1e005de Add the pane layout component
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
2026-09-14 14:51:04 +02:00
Andreas Reinhold / reiniandClaude Opus 5 e99c5993d8 Add the surface layout component
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
2026-09-14 14:50:55 +02:00
Andreas Reinhold / reiniandClaude Opus 5 da7a05a619 Add the grid layout component
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
2026-09-14 14:50:47 +02:00
Andreas Reinhold / reiniandClaude Opus 5 8de053f15c Add the row layout component
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
2026-09-14 14:50:39 +02:00
Andreas Reinhold / reiniandClaude Opus 5 b677e60876 Add the stack layout component and the layout foundation
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
2026-09-14 14:50:27 +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