Draw the data table without Tailwind

<x-table> 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 15:06:51 +02:00
co-authored by Claude Opus 5
parent 4141ebe7b0
commit 25e1bb4788
7 changed files with 75 additions and 42 deletions
+1
View File
@@ -29,6 +29,7 @@
@import './components/choices.css';
@import './components/slider.css';
@import './components/search.css';
@import './components/table.css';
/* Containment */
+35 -30
View File
@@ -2,26 +2,32 @@
* Data tables: `<x-table>` (resources/views/components/table.blade.php), whose callers write plain
* `<thead>`, `<tr>`, `<th>` and `<td>` 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);
}
-1
View File
@@ -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';