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
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 14:50:47 +02:00
co-authored by Claude Opus 5
parent 8de053f15c
commit da7a05a619
6 changed files with 241 additions and 0 deletions
@@ -795,6 +795,19 @@ Children side by side inside a pane: `<x-row gap="space100" justify="between" st
- `stack-below`: `medium`, `expanded`, `large` or `extra-large` — below that breakpoint the row is a column, its children stretched unless `align` was given.
- It runs in the inline direction, so it mirrors in a right-to-left document by itself.
#### `<x-grid>`
Children in columns inside a pane.
```blade
<x-grid :columns="['compact' => 1, 'medium' => 2, 'expanded' => 3]" gap="space300">…</x-grid>
<x-grid min-item="280px" gap="space200">…</x-grid>
```
- `columns`: a map from breakpoint to count, or one number for all. A breakpoint left out takes the nearest smaller one's count; compact is 1 unless given. Each grid writes all five (`--md-columns-compact``--md-columns-extra-large`), so a grid inside another never inherits its parent's.
- `min-item` fills each row with as many columns as fit at that width — following the grid's own width, the choice inside a pane narrower than the window. With `columns` too, the counts are a ceiling.
- Children keep their source order. M3 publishes no column table for the web; choose counts by the content.
### `<x-app-shell>`
The adaptive app shell, a whole layout's body: one navigation per M3 window size class, the page as `<main id="content" wire:transition.navigate>` behind a skip link, and the snackbar host (do not add another `<x-toast />`). It needs `<x-theme-script />` in `<head>`.
+1
View File
@@ -8,3 +8,4 @@
@import './layout/stack.css';
@import './layout/row.css';
@import './layout/grid.css';
+68
View File
@@ -0,0 +1,68 @@
/*
* <x-grid>: children in columns, in a pane, with a column count per breakpoint.
*
* The count at each of M3's breakpoints arrives as five inline custom properties,
* `--md-columns-compact` … `--md-columns-extra-large`, every one written on every grid, each filled
* from the nearest smaller breakpoint (src/Support/Layout.php). One rule per breakpoint picks its
* own into `--md-columns` — compact below 600px, then medium 600, expanded 840, large 1200 and
* extra-large 1600, in px (docs/reference/m3/foundations.md § Layout → Breakpoints) — so a grid
* nested in another reads its own five and never its parent's. M3 publishes no column table for
* the web ("no explicit numeric column-count or gutter table is given on the current site", same
* section), only that columns grow with the breakpoint; the counts are the caller's.
*
* `data-md-min-item` (`--md-min-item`, a length) fills the row with as many columns as fit at that
* minimum width instead, the room a pane actually has deciding rather than the window. With both,
* the count is a ceiling: never more columns than the breakpoint's count, never narrower than the
* minimum. The column width the ceiling implies — the grid's width, less its gaps, over the count —
* needs the gap as a length, which is why a gap is `--md-gap` (spacing.css) and not `gap` alone.
* Every column is `minmax(0, 1fr)` or at most the grid's width, so a long word never pushes a
* column past the pane. Children keep their source order, which is the reading order.
*
* In `material.layout`; `hide-below`/`hide-from` come from visibility.css.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './spacing.css';
@import './visibility.css';
@layer material.layout {
:where([data-md-grid]) {
--md-gap: 0px;
}
[data-md-grid] {
--md-columns: var(--md-columns-compact, 1);
display: grid;
grid-template-columns: repeat(var(--md-columns), minmax(0, 1fr));
gap: var(--md-gap);
@media (width >= 600px) {
--md-columns: var(--md-columns-medium, 1);
}
@media (width >= 840px) {
--md-columns: var(--md-columns-expanded, 1);
}
@media (width >= 1200px) {
--md-columns: var(--md-columns-large, 1);
}
@media (width >= 1600px) {
--md-columns: var(--md-columns-extra-large, 1);
}
}
[data-md-grid][data-md-min-item] {
grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--md-min-item)), 1fr));
}
[data-md-grid][data-md-min-item][data-md-columns] {
grid-template-columns: repeat(
auto-fill,
minmax(min(100%, max(var(--md-min-item), (100% - (var(--md-columns) - 1) * var(--md-gap)) / var(--md-columns))), 1fr)
);
}
}
+49
View File
@@ -0,0 +1,49 @@
{{-- Children in columns, inside a pane.
<x-grid :columns="['compact' => 1, 'medium' => 2, 'expanded' => 3]" gap="space300"></x-grid>
<x-grid min-item="280px" gap="space200"></x-grid>
`columns` is the column count per breakpoint `compact`, `medium` (600px), `expanded` (840),
`large` (1200), `extra-large` (1600), M3's (docs/reference/m3/foundations.md § Layout →
Breakpoints) — as a map, or one whole number for all of them. A breakpoint left out takes the
nearest smaller one's count, and compact is 1 unless given. All five are written on the grid as
`--md-columns-compact` `--md-columns-extra-large`, so a grid nested in another never inherits
its parent's.
`min-item` (`280px`, `18rem`, `20ch`, or a number of px) fills each row with as many columns as
fit at that width, which follows the room the grid has rather than the window — the right
choice inside a pane whose width is not the window's. With `columns` as well, the counts become
a ceiling. `gap` is a spacing token's name, `space25` … `space900`
(docs/reference/m3/styles-supplement.md § Spacing), none when left out or unknown. Children
keep their order, which is the reading order.
It takes `as`, `hide-below` and `hide-from` like every layout component, and the caller's
`class` and `style` land on it untouched (the caller's style after the column properties).
Drawn by resources/css/layout/grid.css. --}}
@props([
'as' => null,
'columns' => null,
'gap' => null,
'minItem' => null,
'hideBelow' => null,
'hideFrom' => null,
])
@php
$layout = \NoNameWeb\LivewireMaterial\Support\Layout::class;
$element = $layout::element($as);
$minItem = $layout::length($minItem);
$style = $layout::columnStyle($layout::columns($columns)).($minItem !== null ? " --md-min-item: {$minItem};" : '');
$attributes = $attributes->merge(array_filter([
'data-md-grid' => true,
'data-md-gap' => $layout::spacing($gap),
'data-md-min-item' => $minItem !== null ? true : null,
'data-md-columns' => $minItem !== null && $columns !== null ? true : null,
'style' => $style,
] + $layout::visibility($hideBelow, $hideFrom), fn ($value): bool => $value !== null));
@endphp
<{{ $element }} {{ $attributes }}>{{ $slot }}</{{ $element }}>