Add the Material 3 Expressive foundation
tests / lint (push) Successful in 1m0s
tests / feature (8.4) (push) Successful in 1m0s
tests / feature (8.5) (push) Successful in 1m1s
tests / browser (safari, webkit) (push) Successful in 1m59s
tests / browser (chrome, chromium) (push) Successful in 1m49s
tests / browser (firefox, firefox) (push) Successful in 1m54s

Colour, shape, type, elevation and motion as tokens and Tailwind
utilities; `php artisan material:scheme`, which generates an app's colour
roles with Google's material-color-utilities (spec 2025); the theme head
script with light, dark and system and its Alpine store; Google Sans Flex;
every Material Symbol (4,135, outlined and filled) drawn by <x-icon>
without blade-icons; all 35 M3 Expressive shapes, ported from androidx, as
<x-shape>; the x-figure directive; the Toasts concern; DesignGuard for
applications' tests; and a showcase with every token, both themes side by
side and an icon search.

Colour utilities are `@theme inline`, so a section with its own
data-theme repaints; without it they resolve once on :root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 05:24:11 +02:00
co-authored by Claude Opus 5
parent 9b53891a8e
commit b48e879254
8364 changed files with 12720 additions and 28 deletions
@@ -0,0 +1,45 @@
@php
$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'],
'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'],
'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'],
'Error' => ['bg-error', 'bg-on-error', 'bg-error-container', 'bg-on-error-container', 'bg-inverse-error'],
'Success' => ['bg-success', 'bg-on-success', 'bg-success-container', 'bg-on-success-container', 'bg-inverse-success'],
'Warning' => ['bg-warning', 'bg-on-warning', 'bg-warning-container', 'bg-on-warning-container', 'bg-inverse-warning'],
'Info' => ['bg-info', 'bg-on-info', 'bg-info-container', 'bg-on-info-container', 'bg-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'],
'Ink and lines' => ['bg-body', 'bg-meta', 'bg-quiet', 'bg-structure', 'bg-chrome', 'bg-divider'],
];
@endphp
<section id="colour" class="scroll-mt-24 space-y-6">
<h2 class="type-headline-md">Colour</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
Every role as <code>bg-*</code>, <code>text-*</code> and <code>border-*</code>, generated by
<code>php artisan material:scheme</code>. Both themes side by side, whatever the page is showing.
</p>
<div class="grid gap-4 lg: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 sm: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
</div>
</div>
@endforeach
</div>
@endforeach
</div>
</section>
@@ -0,0 +1,15 @@
<section id="elevation" class="scroll-mt-24 space-y-6">
<h2 class="type-headline-md">Elevation</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
Five shadow levels as <code>shadow-elevation-*</code>, for what floats over content.
</p>
<div class="grid grid-cols-2 gap-6 rounded-corner-lg bg-surface-container-low p-6 sm:grid-cols-5">
@foreach (['shadow-elevation-1', 'shadow-elevation-2', 'shadow-elevation-3', 'shadow-elevation-4', 'shadow-elevation-5'] as $shadow)
<div @class(['flex h-24 items-center justify-center rounded-corner-md bg-surface-container-high', $shadow])>
<code class="type-label-md">{{ $shadow }}</code>
</div>
@endforeach
</div>
</section>
@@ -0,0 +1,47 @@
<section
id="icons"
class="scroll-mt-24 space-y-6"
x-data="{
names: {{ json_encode(\NoNameWeb\LivewireMaterial\Support\SvgFile::symbolNames()) }},
base: '{{ rtrim(route('livewire-material.showcase', [], false), '/') }}/symbols',
query: '',
filled: false,
get matches() {
const query = this.query.trim().toLowerCase().replaceAll(' ', '_');
return (query === '' ? this.names : this.names.filter((name) => name.includes(query))).slice(0, 120);
},
}"
>
<h2 class="type-headline-md">Icons</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
Every Material Symbol (Rounded, 400, grade 0, 24px) as <code>&lt;x-icon name="" /&gt;</code>, outlined or <code>filled</code>.
</p>
<div class="flex flex-wrap items-center gap-4">
<label class="flex min-w-72 flex-1 items-center gap-2 rounded-corner-xs border border-outline px-3 py-2 focus-within:outline-3 focus-within:outline-secondary">
<x-icon name="search" class="size-5 text-on-surface-variant" />
<input type="search" x-model.debounce.150ms="query" placeholder="Search {{ number_format(count(\NoNameWeb\LivewireMaterial\Support\SvgFile::symbolNames())) }} symbols" class="w-full bg-transparent type-body-lg outline-none" />
</label>
<label class="flex items-center gap-2 type-label-lg">
<input type="checkbox" x-model="filled" class="size-4 accent-primary" />
Filled
</label>
</div>
<ul class="grid grid-cols-[repeat(auto-fill,minmax(7.5rem,1fr))] gap-2">
<template x-for="name in matches" x-bind:key="name">
<li class="flex flex-col items-center gap-2 rounded-corner-md bg-surface-container p-3 text-center">
<span
class="size-6 bg-current"
x-bind:style="{
mask: `url('${base}/${filled ? 'filled' : 'outlined'}/${name}.svg') center / contain no-repeat`,
}"
></span>
<code class="w-full break-all type-label-sm text-on-surface-variant" x-text="name"></code>
</li>
</template>
</ul>
</section>
@@ -0,0 +1,35 @@
<section id="motion" class="scroll-mt-24 space-y-6" x-data="{ moved: false, figure: 1204 }">
<h2 class="type-headline-md">Motion</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
M3 Expressive's springs as <code>ease-spatial-*</code> and <code>ease-effects-*</code>, each with its
<code>--md-sys-motion-*-duration</code>. Reduced motion makes all of them instant.
</p>
<button type="button" class="state-layer focus-ring rounded-corner-full bg-primary px-6 py-2.5 type-label-lg text-on-primary" x-on:click="moved = ! moved">
Move
</button>
<div class="space-y-3 rounded-corner-lg bg-surface-container p-4">
@foreach (['spatial-fast', 'spatial-default', 'spatial-slow', 'effects-fast', 'effects-default', 'effects-slow'] as $spring)
<div class="grid grid-cols-[9rem_1fr] items-center gap-4">
<code class="type-label-md text-on-surface-variant">ease-{{ $spring }}</code>
<div class="h-8">
<div
class="size-8 rounded-corner-full bg-tertiary"
style="transition: translate var(--md-sys-motion-{{ $spring }}-duration) var(--md-sys-motion-{{ $spring }})"
x-bind:style="moved && { translate: '16rem' }"
></div>
</div>
</div>
@endforeach
</div>
<div class="flex flex-wrap items-center gap-4 rounded-corner-lg bg-surface-container p-4">
<p class="type-emphasized-display-sm tabular-nums" x-figure x-text="figure.toLocaleString('en') + ' files'">1,204 files</p>
<button type="button" class="state-layer focus-ring rounded-corner-full px-4 py-2 type-label-lg text-primary" x-on:click="figure = Math.round(Math.random() * 5000)">
Change the figure
</button>
<code class="type-label-md text-on-surface-variant">x-figure</code>
</div>
</section>
@@ -0,0 +1,25 @@
<section id="shape" class="scroll-mt-24 space-y-6">
<h2 class="type-headline-md">Shape</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
The corner scale as <code>rounded-corner-*</code>, and M3 Expressive's shapes as <code>&lt;x-shape name="…" /&gt;</code>.
</p>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-5">
@foreach (['rounded-corner-none', 'rounded-corner-xs', 'rounded-corner-sm', 'rounded-corner-md', 'rounded-corner-lg', 'rounded-corner-lg-increased', 'rounded-corner-xl', 'rounded-corner-xl-increased', 'rounded-corner-xxl', 'rounded-corner-full'] as $corner)
<div class="space-y-2">
<div @class(['h-20 bg-primary-container', $corner])></div>
<code class="type-label-md text-on-surface-variant">{{ $corner }}</code>
</div>
@endforeach
</div>
<div class="grid grid-cols-3 gap-4 sm:grid-cols-5 lg:grid-cols-7">
@foreach (\NoNameWeb\LivewireMaterial\Support\SvgFile::shapeNames() as $shape)
<div class="space-y-2 text-center">
<x-shape :name="$shape" class="mx-auto size-20 text-secondary-container" />
<code class="type-label-md text-on-surface-variant">{{ $shape }}</code>
</div>
@endforeach
</div>
</section>
@@ -0,0 +1,37 @@
@php
$styles = [
['type-display-lg', 'type-emphasized-display-lg'],
['type-display-md', 'type-emphasized-display-md'],
['type-display-sm', 'type-emphasized-display-sm'],
['type-headline-lg', 'type-emphasized-headline-lg'],
['type-headline-md', 'type-emphasized-headline-md'],
['type-headline-sm', 'type-emphasized-headline-sm'],
['type-title-lg', 'type-emphasized-title-lg'],
['type-title-md', 'type-emphasized-title-md'],
['type-title-sm', 'type-emphasized-title-sm'],
['type-body-lg', 'type-emphasized-body-lg'],
['type-body-md', 'type-emphasized-body-md'],
['type-body-sm', 'type-emphasized-body-sm'],
['type-label-lg', 'type-emphasized-label-lg'],
['type-label-md', 'type-emphasized-label-md'],
['type-label-sm', 'type-emphasized-label-sm'],
];
@endphp
<section id="type" class="scroll-mt-24 space-y-6">
<h2 class="type-headline-md">Type</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
M3's typescale in Google Sans Flex, each style regular and emphasized: <code>type-title-lg</code>, <code>type-emphasized-title-lg</code>.
</p>
<div class="divide-y divide-divider rounded-corner-lg bg-surface-container">
@foreach ($styles as [$regular, $emphasized])
<div class="grid gap-2 p-4 md:grid-cols-[12rem_1fr_1fr] md:items-baseline">
<code class="type-label-md text-on-surface-variant">{{ $regular }}</code>
<p @class([$regular, 'truncate'])>Share files, safely</p>
<p @class([$emphasized, 'truncate'])>Share files, safely</p>
</div>
@endforeach
</div>
</section>