Add cards, lists, dialogs, sheets and the rest of M3's containment
tests / lint (push) Failing after 2m6s
tests / feature (8.4) (push) Successful in 1m3s
tests / feature (8.5) (push) Successful in 1m7s
tests / browser (safari, webkit) (push) Successful in 3m14s
tests / browser (chrome, chromium) (push) Successful in 2m9s
tests / browser (firefox, firefox) (push) Successful in 2m29s

<x-card> (filled, elevated, outlined), <x-list> and <x-list-item>
(plain or M3 Expressive's segmented list), <x-divider>, <x-collapse> on
<details>, <x-modal> on the native <dialog>, <x-drawer> as a side sheet or
list-detail pane, and <x-bottom-sheet> with drag to dismiss. Dialogs and
sheets bind to a Livewire flag or id and write back false or null on close,
or use the surrounding Alpine scope. Rows open from anywhere on them through
data-list-row and data-list-open. DesignGuard now also reports Blade
directives written inside a component tag, where they do not compile.

Browser test helpers wait for a complete document with Alpine and Livewire
running: in Firefox, networkidle alone could return before a repeated visit
had loaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 07:28:05 +02:00
co-authored by Claude Opus 5
parent e57063b0f4
commit fef20a9178
30 changed files with 1490 additions and 16 deletions
+101
View File
@@ -0,0 +1,101 @@
/*
* Rows a person can go into (`data-list-row`, resources/js/list-rows.js), and M3 Expressive lists.
*
* A row answers a pointer with M3's state layer — hover 8%, press and focus 10% — but not while the
* pointer is on one of its own controls, which light only themselves. Hover only under
* `(hover: hover)`, because a touch screen keeps the last hover after a tap. The opener draws no
* focus ring inside a row; the row draws it, inset, so keyboard focus shows which row Enter opens.
* `data-selected` is M3's selected list item, in secondary-container.
*
* Unlayered where a component paints its fill as a utility (`<x-card>`): anything in a @layer loses
* to a utility whatever its specificity.
*/
@layer components {
:where([data-list-row]) {
cursor: pointer;
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast),
border-radius var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
}
@media (hover: hover) {
:where([data-list-row]:not([data-card]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover))) {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}
}
:where([data-list-row]:not([data-card]):has([data-list-open]:focus-visible)) {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: -3px;
}
:where([data-list-row]:not([data-card]):active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active))) {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
}
}
[data-list-row] [data-list-open]:focus-visible {
outline: none;
}
[data-list='segmented'] > [data-list-item] {
background-color: var(--md-sys-color-surface-container);
}
:is([data-list-row], [data-list-item])[data-selected]:not([data-card]) {
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
/* A card that opens answers with its container one tone up and its corner opening a step. */
[data-card][data-list-row] {
transition-property: border-radius, background-color, box-shadow;
transition-duration: var(--md-sys-motion-spatial-default-duration);
transition-timing-function: var(--md-sys-motion-spatial-default);
}
@media (hover: hover) {
[data-card][data-list-row]:hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):hover)) {
border-radius: var(--md-sys-shape-corner-lg);
box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}
}
[data-card][data-list-row]:active:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-list-open]):active)) {
box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
}
[data-card][data-list-row]:has([data-list-open]:focus-visible) {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
/*
* M3 Expressive's segmented list (`<x-list segmented>`): each item its own surface, 2px apart,
* extra-small corners that open to large at the ends, and to large while hovered, pressed or
* selected (ListTokens, androidx Compose Material 3, Apache-2.0).
*/
[data-list='segmented'] > [data-list-item] {
border-radius: var(--md-sys-shape-corner-xs);
}
[data-list='segmented'] > [data-list-item]:first-child {
border-start-start-radius: var(--md-sys-shape-corner-lg);
border-start-end-radius: var(--md-sys-shape-corner-lg);
}
[data-list='segmented'] > [data-list-item]:last-child {
border-end-start-radius: var(--md-sys-shape-corner-lg);
border-end-end-radius: var(--md-sys-shape-corner-lg);
}
@media (hover: hover) {
[data-list='segmented'] > [data-list-item][data-list-row]:hover {
border-radius: var(--md-sys-shape-corner-md);
}
}
[data-list='segmented'] > [data-list-item]:is([data-selected], [data-list-row]:active) {
border-radius: var(--md-sys-shape-corner-lg);
}