Rewrite the showcase's Colour section without Tailwind

Plan step 38: swatches move from Tailwind's bg-* role utilities to
data-md-showcase-swatch spans whose fill is the caller's own inline
style="background: var(--md-sys-color-{role})" (a swatch's colour
depends on the role it shows, so that one declaration cannot be a
class), sized and edged in showcase.css; the two grids and every
per-theme panel move onto <x-grid>, <x-surface> and <x-stack>; text
onto the md-type-*/md-ink-* classes. The legacy "Ink and lines" group
(bg-body, bg-meta, bg-quiet, bg-structure, bg-chrome, bg-divider) is
dropped — those Tailwind ink/line names end with Phase F and their
roles already show under Surface.

ThemeTest and ColourProfilesTest read the swatch's computed colour
through the new data-md-showcase-swatch="primary" hook instead of the
retired .bg-primary class.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 04:14:22 +02:00
co-authored by Claude Sonnet 5
parent 9c8b39600b
commit 365df791c1
4 changed files with 91 additions and 60 deletions
+28
View File
@@ -16,6 +16,12 @@
* selector this file needs to draw over, only tokens already in `all.css`, which the bundle always * selector this file needs to draw over, only tokens already in `all.css`, which the bundle always
* carries ahead of this file. A later batch that needs a component's own hook imports its * carries ahead of this file. A later batch that needs a component's own hook imports its
* stylesheet here, as every other package file does. * stylesheet here, as every other package file does.
*
* Batch 2 (the foundation and layout sections) adds the swatches, specimens and demo boxes those
* pages draw beyond a layout component and a `md-*` text class: a colour swatch (its own fill is
* the role it shows, so that one declaration is the caller's inline `style`, never a class), a
* type-scale row, a shape swatch, an elevation tile, a motion track and its moving dot, an icon
* search result and the Layout page's breakpoint cards.
*/ */
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; @layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@@ -114,4 +120,26 @@
background-color: var(--md-sys-color-surface-container-highest); background-color: var(--md-sys-color-surface-container-highest);
padding: var(--md-sys-measurement-space125); padding: var(--md-sys-measurement-space125);
} }
/* Colour: a role swatch. Its fill is the one declaration that has to be the caller's inline
`style` (the role it shows decides it), so this rule only gives it a size and an edge; `lg`
is the big per-theme grid's, the plain attribute the smaller contrast-level list's. */
[data-md-showcase-swatch] {
display: block;
flex-shrink: 0;
width: var(--md-sys-measurement-space300);
height: var(--md-sys-measurement-space300);
border-radius: var(--md-sys-shape-corner-xs);
border: 1px solid var(--md-sys-color-outline-variant);
}
[data-md-showcase-swatch][data-md-showcase-size='lg'] {
width: var(--md-sys-measurement-space400);
height: var(--md-sys-measurement-space400);
}
/* Lets the role name beside a swatch ellipsize inside its row instead of widening the grid. */
[data-md-showcase-swatch-label] {
min-width: 0;
}
} }
@@ -1,25 +1,24 @@
@php @php
$groups = [ $groups = [
'Primary' => ['bg-primary', 'bg-on-primary', 'bg-primary-container', 'bg-on-primary-container', 'bg-inverse-primary', 'bg-primary-fixed', 'bg-primary-fixed-dim', 'bg-on-primary-fixed', 'bg-on-primary-fixed-variant'], 'Primary' => ['primary', 'on-primary', 'primary-container', 'on-primary-container', 'inverse-primary', 'primary-fixed', 'primary-fixed-dim', 'on-primary-fixed', 'on-primary-fixed-variant'],
'Secondary' => ['bg-secondary', 'bg-on-secondary', 'bg-secondary-container', 'bg-on-secondary-container', 'bg-secondary-fixed', 'bg-secondary-fixed-dim', 'bg-on-secondary-fixed', 'bg-on-secondary-fixed-variant'], 'Secondary' => ['secondary', 'on-secondary', 'secondary-container', 'on-secondary-container', 'secondary-fixed', 'secondary-fixed-dim', 'on-secondary-fixed', 'on-secondary-fixed-variant'],
'Tertiary' => ['bg-tertiary', 'bg-on-tertiary', 'bg-tertiary-container', 'bg-on-tertiary-container', 'bg-tertiary-fixed', 'bg-tertiary-fixed-dim', 'bg-on-tertiary-fixed', 'bg-on-tertiary-fixed-variant'], 'Tertiary' => ['tertiary', 'on-tertiary', 'tertiary-container', 'on-tertiary-container', 'tertiary-fixed', 'tertiary-fixed-dim', 'on-tertiary-fixed', 'on-tertiary-fixed-variant'],
'Error' => ['bg-error', 'bg-on-error', 'bg-error-container', 'bg-on-error-container', 'bg-inverse-error'], 'Error' => ['error', 'on-error', 'error-container', 'on-error-container', 'inverse-error'],
'Success' => ['bg-success', 'bg-on-success', 'bg-success-container', 'bg-on-success-container', 'bg-inverse-success'], 'Success' => ['success', 'on-success', 'success-container', 'on-success-container', 'inverse-success'],
'Warning' => ['bg-warning', 'bg-on-warning', 'bg-warning-container', 'bg-on-warning-container', 'bg-inverse-warning'], 'Warning' => ['warning', 'on-warning', 'warning-container', 'on-warning-container', 'inverse-warning'],
'Info' => ['bg-info', 'bg-on-info', 'bg-info-container', 'bg-on-info-container', 'bg-inverse-info'], 'Info' => ['info', 'on-info', 'info-container', 'on-info-container', 'inverse-info'],
'Surface' => ['bg-surface', 'bg-surface-dim', 'bg-surface-bright', 'bg-surface-container-lowest', 'bg-surface-container-low', 'bg-surface-container', 'bg-surface-container-high', 'bg-surface-container-highest', 'bg-on-surface', 'bg-on-surface-variant', 'bg-inverse-surface', 'bg-inverse-on-surface', 'bg-outline', 'bg-outline-variant'], 'Surface' => ['surface', 'surface-dim', 'surface-bright', 'surface-container-lowest', 'surface-container-low', 'surface-container', 'surface-container-high', 'surface-container-highest', 'on-surface', 'on-surface-variant', 'inverse-surface', 'inverse-on-surface', 'outline', 'outline-variant'],
'Ink and lines' => ['bg-body', 'bg-meta', 'bg-quiet', 'bg-structure', 'bg-chrome', 'bg-divider'],
]; ];
// The roles a contrast level moves most: the page keeps its surface, the ink and the // The roles a contrast level moves most: the page keeps its surface, the ink and the
// outlines carry the level. // outlines carry the level.
$levels = ['standard' => 'Standard', 'medium' => 'Medium · 3:1', 'high' => 'High · 7:1']; $levels = ['standard' => 'Standard', 'medium' => 'Medium · 3:1', 'high' => 'High · 7:1'];
$levelRoles = ['bg-primary', 'bg-on-primary', 'bg-primary-container', 'bg-on-primary-container', 'bg-on-surface-variant', 'bg-outline', 'bg-error', 'bg-success', 'bg-warning', 'bg-info']; $levelRoles = ['primary', 'on-primary', 'primary-container', 'on-primary-container', 'on-surface-variant', 'outline', 'error', 'success', 'warning', 'info'];
$profile = \NoNameWeb\LivewireMaterial\Support\Scheme::profile(); $profile = \NoNameWeb\LivewireMaterial\Support\Scheme::profile();
$examples = [ $examples = [
'Colour profiles' => <<<'BLADE' 'Colour profiles' => <<<'BLADE'
<div x-data="{ profile: $store.theme.scheme }" class="w-full max-w-3xl"> <div x-data="{ profile: $store.theme.scheme }">
<x-scheme-picker label="Colour profile" name="profile" x-model="profile" hint="Previews on this page; an application stores the choice and names it with Scheme::resolveProfileUsing()." /> <x-scheme-picker label="Colour profile" name="profile" x-model="profile" hint="Previews on this page; an application stores the choice and names it with Scheme::resolveProfileUsing()." />
</div> </div>
BLADE, BLADE,
@@ -29,25 +28,25 @@
]; ];
@endphp @endphp
<section id="colour" class="scroll-mt-24 space-y-6"> <x-livewire-material::stack as="section" id="colour" gap="space300">
<h2 class="type-headline-md">Colour</h2> <h2 class="md-type-headline-md">Colour</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant"> <p class="md-type-body-md md-ink-variant">
Every role as <code>bg-*</code>, <code>text-*</code> and <code>border-*</code>, generated by Every role as a <code>--md-sys-color-*</code> token, generated by
<code>php artisan material:scheme</code>. Both themes side by side, whatever the page is showing. <code>php artisan material:scheme</code>. Both themes side by side, whatever the page is showing.
</p> </p>
<p class="max-w-3xl type-body-md text-on-surface-variant" data-test="when-to-use"> <p class="md-type-body-md md-ink-variant" data-test="when-to-use">
When to use which: <code>primary</code> is the one key action on a screen; <code>secondary-container</code> is the quiet fill (tonal buttons, selected navigation and chips); <code>tertiary</code> is a rare accent; <code>error</code>, <code>success</code>, <code>warning</code> and <code>info</code> mean state the <code>-container</code> for a tinted panel, the role itself for its text and icon. Pair a role only with its <code>on-</code> partner. Ink is <code>on-surface</code>, lower emphasis <code>on-surface-variant</code>, decoration <code>outline</code>; never an opacity, because 38% means disabled. Every role changes with the contrast level below, which is why only roles are allowed. When to use which: <code>primary</code> is the one key action on a screen; <code>secondary-container</code> is the quiet fill (tonal buttons, selected navigation and chips); <code>tertiary</code> is a rare accent; <code>error</code>, <code>success</code>, <code>warning</code> and <code>info</code> mean state the <code>-container</code> for a tinted panel, the role itself for its text and icon. Pair a role only with its <code>on-</code> partner. Ink is <code>on-surface</code>, lower emphasis <code>on-surface-variant</code>, decoration <code>outline</code>; never an opacity, because 38% means disabled. Every role changes with the contrast level below, which is why only roles are allowed.
</p> </p>
<p class="max-w-3xl type-body-md text-on-surface-variant"> <p class="md-type-body-md md-ink-variant">
With colour profiles in <code>livewire-material.profiles</code>, the command generates each one under With colour profiles in <code>livewire-material.profiles</code>, the command generates each one under
<code>&lt;html data-scheme&gt;</code>, and <code>&lt;x-scheme-picker&gt;</code> chooses between them. Without <code>&lt;html data-scheme&gt;</code>, and <code>&lt;x-scheme-picker&gt;</code> chooses between them. Without
profiles the picker draws nothing. profiles the picker draws nothing.
</p> </p>
<p class="max-w-3xl type-body-md text-on-surface-variant"> <p class="md-type-body-md md-ink-variant">
Every scheme is generated at M3's three contrast levels — standard, medium (3:1) and high (7:1) — and the Every scheme is generated at M3's three contrast levels — standard, medium (3:1) and high (7:1) — and the
head script writes the visitor's to <code>&lt;html data-contrast&gt;</code>, following the operating system head script writes the visitor's to <code>&lt;html data-contrast&gt;</code>, following the operating system
until they choose. The page keeps its surface; the ink, the outlines and the containers carry the level. until they choose. The page keeps its surface; the ink, the outlines and the containers carry the level.
@@ -57,53 +56,57 @@
<x-showcase::example :$title :$code /> <x-showcase::example :$title :$code />
@endforeach @endforeach
<div class="grid gap-4 large:grid-cols-3"> <x-livewire-material::grid :columns="['compact' => 1, 'large' => 3]" gap="space200">
@foreach ($levels as $level => $title) @foreach ($levels as $level => $title)
{{-- data-scheme beside data-contrast: a profile's level blocks key on both attributes {{-- data-scheme beside data-contrast: a profile's level blocks key on both attributes
together, as they do on <html>. --}} together, as they do on <html>. --}}
<div <x-livewire-material::stack
@if ($profile !== null) data-scheme="{{ $profile }}" @endif :data-scheme="$profile"
@if ($level !== 'standard') data-contrast="{{ $level }}" @endif :data-contrast="$level !== 'standard' ? $level : null"
class="space-y-3" gap="space150"
> >
<h3 class="type-title-sm text-on-surface-variant">{{ $title }}</h3> <h3 class="md-type-title-sm md-ink-variant">{{ $title }}</h3>
@foreach (['light', 'dark'] as $theme) @foreach (['light', 'dark'] as $theme)
<div data-theme="{{ $theme }}" class="space-y-2 rounded-corner-lg bg-surface p-3 text-on-surface"> <x-livewire-material::surface data-theme="{{ $theme }}" level="surface" padding="space200" corner="lg">
<h4 class="type-label-md capitalize text-on-surface-variant">{{ $theme }}</h4> <x-livewire-material::stack gap="space100">
<h4 class="md-type-label-md md-ink-variant">{{ ucfirst($theme) }}</h4>
@foreach ($levelRoles as $role) @foreach ($levelRoles as $role)
<div class="flex items-center gap-2"> <x-livewire-material::row gap="space100" align="center">
<span @class(['size-6 shrink-0 rounded-corner-xs border border-outline-variant', $role])></span> <span data-md-showcase-swatch="{{ $role }}" style="background: var(--md-sys-color-{{ $role }})"></span>
<span class="min-w-0 truncate type-body-sm">{{ \Illuminate\Support\Str::after($role, 'bg-') }}</span> <span class="md-type-body-sm md-truncate" data-md-showcase-swatch-label>{{ $role }}</span>
</div> </x-livewire-material::row>
@endforeach
</div>
@endforeach
</div>
@endforeach
</div>
<div class="grid gap-4 expanded:grid-cols-2">
@foreach (['light', 'dark'] as $theme)
<div data-theme="{{ $theme }}" class="space-y-6 rounded-corner-lg bg-surface p-4 text-on-surface">
<h3 class="type-title-md capitalize">{{ $theme }}</h3>
@foreach ($groups as $group => $roles)
<div class="space-y-2">
<h4 class="type-label-lg text-on-surface-variant">{{ $group }}</h4>
<div class="grid grid-cols-2 gap-2 medium:grid-cols-3">
@foreach ($roles as $role)
<div class="flex items-center gap-2">
<span @class(['size-8 shrink-0 rounded-corner-sm border border-outline-variant', $role])></span>
<span class="min-w-0 truncate type-body-sm">{{ \Illuminate\Support\Str::after($role, 'bg-') }}</span>
</div>
@endforeach @endforeach
</div> </x-livewire-material::stack>
</div> </x-livewire-material::surface>
@endforeach @endforeach
</div> </x-livewire-material::stack>
@endforeach @endforeach
</div> </x-livewire-material::grid>
</section>
<x-livewire-material::grid :columns="['compact' => 1, 'expanded' => 2]" gap="space200">
@foreach (['light', 'dark'] as $theme)
<x-livewire-material::surface data-theme="{{ $theme }}" level="surface" padding="space200" corner="lg">
<x-livewire-material::stack gap="space300">
<h3 class="md-type-title-md">{{ ucfirst($theme) }}</h3>
@foreach ($groups as $group => $roles)
<x-livewire-material::stack gap="space100">
<h4 class="md-type-label-lg md-ink-variant">{{ $group }}</h4>
<x-livewire-material::grid :columns="['compact' => 2, 'medium' => 3]" gap="space100">
@foreach ($roles as $role)
<x-livewire-material::row gap="space100" align="center">
<span data-md-showcase-swatch="{{ $role }}" data-md-showcase-size="lg" style="background: var(--md-sys-color-{{ $role }})"></span>
<span class="md-type-body-sm md-truncate" data-md-showcase-swatch-label>{{ $role }}</span>
</x-livewire-material::row>
@endforeach
</x-livewire-material::grid>
</x-livewire-material::stack>
@endforeach
</x-livewire-material::stack>
</x-livewire-material::surface>
@endforeach
</x-livewire-material::grid>
</x-livewire-material::stack>
+1 -1
View File
@@ -54,7 +54,7 @@ it('draws each profile in light and dark, and the default without the attribute'
$page->script("document.documentElement.setAttribute('data-theme', 'dark')"); $page->script("document.documentElement.setAttribute('data-theme', 'dark')");
$page->assertScript(pagePrimary()." === '{$this->profiles['teal']['dark']['primary']}'") $page->assertScript(pagePrimary()." === '{$this->profiles['teal']['dark']['primary']}'")
// A panel that sets its own theme keeps the page's profile. // A panel that sets its own theme keeps the page's profile.
->assertScript("getComputedStyle(document.querySelector('#colour [data-theme=\"light\"] .bg-primary')).backgroundColor === '".rgb($this->profiles['teal']['light']['primary'])."'"); ->assertScript("getComputedStyle(document.querySelector('#colour [data-theme=\"light\"] [data-md-showcase-swatch=\"primary\"]')).backgroundColor === '".rgb($this->profiles['teal']['light']['primary'])."'");
$page->script("document.documentElement.removeAttribute('data-scheme')"); $page->script("document.documentElement.removeAttribute('data-scheme')");
$page->assertScript(pagePrimary()." === '{$this->profiles['baseline']['dark']['primary']}'") $page->assertScript(pagePrimary()." === '{$this->profiles['baseline']['dark']['primary']}'")
+1 -1
View File
@@ -56,7 +56,7 @@ it('adopts an earlier toggle\'s choice once', function () {
}); });
it('repaints a section that sets its own theme', function () { it('repaints a section that sets its own theme', function () {
$swatch = fn (int $index): string => "getComputedStyle(document.querySelectorAll('#colour [data-theme] .bg-primary')[{$index}]).backgroundColor"; $swatch = fn (int $index): string => "getComputedStyle(document.querySelectorAll('#colour [data-theme] [data-md-showcase-swatch=\"primary\"]')[{$index}]).backgroundColor";
visit('/material/colour')->inLightMode() visit('/material/colour')->inLightMode()
->assertScript(theme('data-theme', 'light')) ->assertScript(theme('data-theme', 'light'))