From 4cc905b14485887edb7d43b5834fbb8b3145c0b0 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 16:49:47 +0200 Subject: [PATCH] Draw the stat card without Tailwind Plan step 36 (actions). renders data-md-stat with data-md-stat-header/-value/-description/-extra; no class list. stat.css draws the surface-container panel, the label-large header, the value as an emphasized-headline-md hero with tabular figures, and the body-small description. StatTest is rewritten on the hooks and ComponentStylesheet. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components.css | 1 + resources/css/components/stat.css | 50 +++++++++++++++++++ resources/views/components/stat.blade.php | 16 +++--- .../Components/ActionStylesheetsTest.php | 1 + tests/Feature/Components/StatTest.php | 26 ++++++++-- 5 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 resources/css/components/stat.css diff --git a/resources/css/components.css b/resources/css/components.css index c46c7ed8..0ab517cb 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -30,6 +30,7 @@ @import './components/toast.css'; @import './components/progress.css'; @import './components/alert.css'; +@import './components/stat.css'; /* Inputs, selection and data */ @import './components/form.css'; diff --git a/resources/css/components/stat.css b/resources/css/components/stat.css new file mode 100644 index 00000000..8795dda0 --- /dev/null +++ b/resources/css/components/stat.css @@ -0,0 +1,50 @@ +/* + * : a figure and what it counts — not an M3 component; drawn in M3's terms, a panel that + * separates from the page in surface-container, a label-large title beside its icon, the value as + * an emphasized-headline-md hero with tabular figures (so the panel never reflows as it counts), + * and a body-small description. resources/js/figure.js counts the value up on first appearance + * and on every change after, and rests under reduced motion. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@import './icon.css'; + +@layer material.components { + [data-md-stat] { + display: flex; + flex-direction: column; + gap: var(--md-sys-measurement-space50); + border-radius: var(--md-sys-shape-corner-lg); + background-color: var(--md-sys-color-surface-container); + padding: var(--md-sys-measurement-space200); + } + + [data-md-stat-header] { + display: flex; + align-items: center; + gap: var(--md-sys-measurement-space100); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-label-lg); + letter-spacing: var(--md-sys-typescale-label-lg-tracking); + font-variation-settings: normal; + } + + [data-md-stat-value] { + font: var(--md-sys-typescale-emphasized-headline-md); + letter-spacing: var(--md-sys-typescale-emphasized-headline-md-tracking); + font-variation-settings: "ROND" 100; + font-variant-numeric: tabular-nums; + } + + [data-md-stat-description] { + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-body-sm); + letter-spacing: var(--md-sys-typescale-body-sm-tracking); + font-variation-settings: normal; + } + + [data-md-stat-extra] { + padding-top: var(--md-sys-measurement-space100); + } +} diff --git a/resources/views/components/stat.blade.php b/resources/views/components/stat.blade.php index 10800a64..d85a374f 100644 --- a/resources/views/components/stat.blade.php +++ b/resources/views/components/stat.blade.php @@ -5,7 +5,9 @@ headline with tabular figures, and a body-small `description`. The value counts up once on first appearance and again when it changes (`x-figure`), and rests under reduced motion. `icon` sits beside the title. The slot, if any, goes under the description — a progress bar - for a quota. --}} + for a quota. + + Drawn by resources/css/components/stat.css from `data-md-stat`; no class list. --}} @props([ 'title' => null, @@ -14,21 +16,21 @@ 'icon' => null, ]) -
class('flex flex-col gap-1 rounded-corner-lg bg-surface-container p-4') }}> -
+
+
@if ($icon) - + @endif {{ $title }}
-

{{ $value }}

+

{{ $value }}

@if ($description) -

{{ $description }}

+

{{ $description }}

@endif @if ($slot->isNotEmpty()) -
{{ $slot }}
+
{{ $slot }}
@endif
diff --git a/tests/Feature/Components/ActionStylesheetsTest.php b/tests/Feature/Components/ActionStylesheetsTest.php index 0e81cdc4..3876e49c 100644 --- a/tests/Feature/Components/ActionStylesheetsTest.php +++ b/tests/Feature/Components/ActionStylesheetsTest.php @@ -29,6 +29,7 @@ dataset('action components', [ 'toast', 'progress', 'alert', + 'stat', ]); it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) { diff --git a/tests/Feature/Components/StatTest.php b/tests/Feature/Components/StatTest.php index 42e8b0b4..ed735b7d 100644 --- a/tests/Feature/Components/StatTest.php +++ b/tests/Feature/Components/StatTest.php @@ -1,14 +1,34 @@ blade('quota'); expect($html) - ->toContain('bg-surface-container') + ->toContain('data-md-stat') ->toContain('Shares') + ->toContain('data-md-stat-value') ->toContain('x-figure>1,204

') - ->toContain('type-emphasized-headline-md tabular-nums') + ->toContain('data-md-stat-description') ->toContain('12 this week') + ->toContain('data-md-stat-extra') ->toContain('quota') - ->toContain('toContain('and(ComponentStylesheet::read('stat')->declarations('[data-md-stat]'))->toMatchArray([ + 'background-color' => 'var(--md-sys-color-surface-container)', + 'border-radius' => 'var(--md-sys-shape-corner-lg)', + ]) + ->and(ComponentStylesheet::read('stat')->declarations('[data-md-stat-value]'))->toMatchArray([ + 'font' => 'var(--md-sys-typescale-emphasized-headline-md)', + 'font-variant-numeric' => 'tabular-nums', + ]); +}); + +it('leaves out the description and slot when there is neither', function () { + $html = (string) $this->blade(''); + + expect($html) + ->not->toContain('data-md-stat-description') + ->not->toContain('data-md-stat-extra'); });