From 25e1bb4788bf37f3b8811157f4ce0379311116ea Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 15:06:51 +0200 Subject: [PATCH] Draw the data table without Tailwind renders data-md-table with data-md-size and data-md-dense, and table.css moves into material.components on px and spacing tokens, its cell selectors still inside :where() so a caller's cell rule wins (plan step 36). The showcase's layout table follows the hook. A browser test pins 52px rows and 36px dense ones. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components.css | 1 + resources/css/components/table.css | 65 ++++++++++--------- resources/css/tailwind.css | 1 - resources/views/components/table.blade.php | 10 +-- .../views/showcase/sections/layout.blade.php | 2 +- tests/Browser/DataTest.php | 17 +++++ tests/Feature/Components/DataTest.php | 21 ++++-- 7 files changed, 75 insertions(+), 42 deletions(-) diff --git a/resources/css/components.css b/resources/css/components.css index 6301bffd..ff8b7723 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -29,6 +29,7 @@ @import './components/choices.css'; @import './components/slider.css'; @import './components/search.css'; +@import './components/table.css'; /* Containment */ diff --git a/resources/css/components/table.css b/resources/css/components/table.css index 17feaa5b..eb200f4f 100644 --- a/resources/css/components/table.css +++ b/resources/css/components/table.css @@ -2,26 +2,32 @@ * Data tables: `` (resources/views/components/table.blade.php), whose callers write plain * ``, ``, `` and `` inside it. * - * So the styling is descendant selectors on the one attribute the component sets, all inside - * `:where()` and `@layer components`: a caller's `text-end` or `whitespace-nowrap` on a cell always - * wins. Header cells in title-small on-surface-variant over an outline-variant rule, body cells in - * body-medium between outline-variant rules. A row that opens something is `data-list-row` and + * So the styling is descendant selectors on the one attribute the component sets, `data-md-table`, + * with every cell and row selector inside `:where()`: a caller's alignment or wrapping on a cell + * always wins, whatever layer it comes from. Header cells in title-small on-surface-variant over an + * outline-variant rule, body cells in body-medium between outline-variant rules — the role itself, + * never a fraction of it, since M3 reserves opacity for state layers and disabled. A selected row + * (`aria-selected="true"`) is secondary-container. A row that opens something is `data-list-row` and * answers a pointer as a list row does (components/list.css). * - * A row is 52px: 16px above and below a body-medium line. Density is never applied by itself — - * "don't apply density by default; offer an explicit density opt-in instead, keeping opt-out targets - * at >= 48x48 CSS px" (docs/reference/m3/foundations-supplement.md § Accessibility) — so `dense` - * (`data-dense`) is the caller's decision, and it is the caller's to justify: a dense row is 36px, - * and a dense row inside an `xs` table 24px. + * A row is 52px: 16px above and below a body-medium line, 12px beside each cell. Density is never + * applied by itself — "don't apply density by default; offer an explicit density opt-in instead, + * keeping opt-out targets at >= 48x48 CSS px" (docs/reference/m3/foundations-supplement.md + * § Accessibility) — so `dense` (`data-md-dense`) is the caller's decision, and it is the caller's + * to justify: a dense row is 36px (8px above and below). `data-md-size="xs"`, for a table inside a + * panel inside a panel, is body-small with label-medium headers, 8px around each cell (32px rows), + * and 4px above and below when dense (24px). */ -@layer components { - /* `relative` makes the table the containing block for anything absolute inside it — an `sr-only` - header label, a tooltip. Without it they escape the scroll box and widen a phone's layout - viewport. */ - [data-table] { - --cell-x: 0.75rem; - --cell-y: 1rem; +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.components { + /* `position: relative` makes the table the containing block for anything absolute inside it — + a visually hidden header label, a tooltip. Without it they escape the scroll box and widen a + phone's layout viewport. */ + [data-md-table] { + --cell-x: 12px; + --cell-y: var(--md-sys-measurement-space200); position: relative; width: 100%; @@ -31,29 +37,29 @@ letter-spacing: var(--md-sys-typescale-body-md-tracking); } - [data-table][data-size="xs"] { - --cell-x: 0.5rem; - --cell-y: 0.5rem; + [data-md-table][data-md-size='xs'] { + --cell-x: var(--md-sys-measurement-space100); + --cell-y: var(--md-sys-measurement-space100); font: var(--md-sys-typescale-body-sm); letter-spacing: var(--md-sys-typescale-body-sm-tracking); } - [data-table][data-dense] { - --cell-y: 0.5rem; + [data-md-table][data-md-dense] { + --cell-y: var(--md-sys-measurement-space100); } - [data-table][data-size="xs"][data-dense] { - --cell-y: 0.25rem; + [data-md-table][data-md-size='xs'][data-md-dense] { + --cell-y: var(--md-sys-measurement-space50); } - [data-table] :where(th, td) { + [data-md-table] :where(th, td) { padding: var(--cell-y) var(--cell-x); text-align: start; vertical-align: middle; } - [data-table] :where(thead th) { + [data-md-table] :where(thead th) { color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-title-sm); letter-spacing: var(--md-sys-typescale-title-sm-tracking); @@ -61,21 +67,20 @@ border-bottom: 1px solid var(--md-sys-color-outline-variant); } - [data-table][data-size="xs"] :where(thead th) { + [data-md-table][data-md-size='xs'] :where(thead th) { font: var(--md-sys-typescale-label-md); letter-spacing: var(--md-sys-typescale-label-md-tracking); } - /* The role itself, not a fraction of it: M3 reserves opacity for state layers and disabled. */ - [data-table] :where(tbody tr) { + [data-md-table] :where(tbody tr) { border-bottom: 1px solid var(--md-sys-color-outline-variant); } - [data-table] :where(tbody tr:last-child) { + [data-md-table] :where(tbody tr:last-child) { border-bottom: 0; } - [data-table] :where(tbody tr[aria-selected="true"]) { + [data-md-table] :where(tbody tr[aria-selected='true']) { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); } diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css index 32b14226..0ce235c8 100644 --- a/resources/css/tailwind.css +++ b/resources/css/tailwind.css @@ -24,7 +24,6 @@ /* Inputs, selection and data — leaving in step 36 */ @import './components/datepicker.css'; @import './components/timepicker.css'; -@import './components/table.css'; /* Containment — leaving in step 36 */ @import './components/list.css'; diff --git a/resources/views/components/table.blade.php b/resources/views/components/table.blade.php index 1d15cd9c..c10f0cd9 100644 --- a/resources/views/components/table.blade.php +++ b/resources/views/components/table.blade.php @@ -1,20 +1,20 @@ {{-- A data table. Its callers write ``, ``, `` and `` as they always do; how they - look is resources/css/components/table.css, keyed on the `data-table` attribute set here (the - top of that file says why). + look is resources/css/components/table.css, keyed on the `data-md-table` attribute set here (the + top of that file says why), with `data-md-size` and `data-md-dense`. Rows are 52px, a target a finger can hit. `dense` tightens them to 36px — M3 asks that density always be an opt-in and never a default, so it is the caller's decision and the caller's to justify. `size="xs"` for a table inside a panel inside a panel (32px rows, 24px with `dense`). A selected row is ``. Horizontal scrolling stays the caller's — wrap the - table in `
` where the page needs it. A column that sorts is - ``. --}} + table in an element that scrolls sideways (`overflow-x: auto`) where the page needs it. A column + that sorts is ``. --}} @props([ 'size' => 'sm', 'dense' => false, ]) - +
{{ $slot }}
diff --git a/resources/views/showcase/sections/layout.blade.php b/resources/views/showcase/sections/layout.blade.php index ae42928b..ffca9ae8 100644 --- a/resources/views/showcase/sections/layout.blade.php +++ b/resources/views/showcase/sections/layout.blade.php @@ -47,7 +47,7 @@
- +
diff --git a/tests/Browser/DataTest.php b/tests/Browser/DataTest.php index 41a1c960..408e7781 100644 --- a/tests/Browser/DataTest.php +++ b/tests/Browser/DataTest.php @@ -48,6 +48,13 @@ function dataProbe() {{ $files->links() }} + + + + + + + BLADE); app('view')->addNamespace('probe', $views); @@ -103,3 +110,13 @@ it('pages through Livewire results and marks the current page', function () { $page->resize(400, 800) ->assertSee('Page 2 of 3'); }); + +it('draws 52px rows, and 36px ones only in a table that asks to be dense', function () { + $row = fn (string $table): string => "document.querySelector('{$table} tbody tr').getBoundingClientRect().height"; + + dataProbe() + ->assertScript("Math.abs(({$row('[data-md-table]:not([data-md-dense])')}) - 52) <= 1") + ->assertScript("Math.abs(({$row('#dense-table')}) - 36) <= 1") + ->assertScript("getComputedStyle(document.querySelector('#dense-table tr[aria-selected=\"true\"]')).backgroundColor !== 'rgba(0, 0, 0, 0)'") + ->assertScript("getComputedStyle(document.querySelector('[data-md-table] thead th')).borderBottomWidth === '1px'"); +}); diff --git a/tests/Feature/Components/DataTest.php b/tests/Feature/Components/DataTest.php index 8669f34c..32ff94f1 100644 --- a/tests/Feature/Components/DataTest.php +++ b/tests/Feature/Components/DataTest.php @@ -8,14 +8,25 @@ use Livewire\WithPagination; it('marks a table for its styles and takes a size', function () { expect((string) $this->blade('')) - ->toContain('
Class
dense-a.zip1
dense-b.zip2
a
') - ->and((string) $this->blade(''))->toContain('data-size="xs"') - ->and((string) $this->blade(''))->toContain('data-size="sm"'); + ->toContain('
') + ->and((string) $this->blade(''))->toContain('data-md-size="xs"') + ->and((string) $this->blade(''))->toContain('data-md-size="sm"'); }); it('tightens a table only when the caller asks for it', function () { - expect((string) $this->blade(''))->not->toContain('data-dense') - ->and((string) $this->blade(''))->toContain('data-dense'); + expect((string) $this->blade(''))->not->toContain('data-md-dense') + ->and((string) $this->blade(''))->toContain('data-md-dense'); +}); + +it('draws 52px rows, 36px dense ones, from its stylesheet in the components layer', function () { + $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/table.css'); + + expect($css)->toContain('@layer material.components') + ->toMatch('/\\[data-md-table\\] \\{\\s*--cell-x: 12px;\\s*--cell-y: var\\(--md-sys-measurement-space200\\);/') + ->toMatch('/\\[data-md-table\\]\\[data-md-dense\\] \\{\\s*--cell-y: var\\(--md-sys-measurement-space100\\);/') + ->toContain('[data-md-table] :where(tbody tr[aria-selected=\'true\'])') + ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/table.css';") + ->and((string) file_get_contents(__DIR__.'/../../../resources/css/tailwind.css'))->not->toContain('table.css'); }); it('sorts by its column, ascending first and then flipping', function () {