Delete Tailwind's half and build the Workbench without it

Plan step 39 (parts 1-3, folded into one commit: PHP tests read the
deleted files directly, so they cannot land apart from it). Tailwind
leaves the whole stack:

- Delete resources/css/tailwind.css, tokens/theme.css and
  tokens/utilities.css. Neither token file declared an --md-sys-*
  custom property of its own (both only referenced tokens declared
  elsewhere), so nothing loses a value; the md-* interaction and text
  classes already mirror utilities.css's declarations exactly
  (foundation/interaction.css, text.css).
- npm uninstall tailwindcss @tailwindcss/vite; vite.config.js drops the
  plugin and its import; composer.json drops the tailwindcss keyword
  (no lock change — keywords are outside Composer's content hash).
- The Workbench now builds one CSS entry, workbench/resources/css/app.css
  (all.css, showcase.css and the scheme; package.css is folded in and
  removed) instead of two, used by ErrorPage::assets() and every
  browser-test probe page's raw @vite() call; the showcase's own pages
  still take their CSS from the bundle route.
- TokensTest and StylesheetsTest: the two facts theme.css and
  utilities.css carried (every scheme role becomes a colour, resolved
  on the element; md-type-* matches the type-* utilities) are asserted
  directly against the scheme and text.css now that there is no second
  copy to cross-check; StylesheetsTest gained a full-tree scan (every
  .css file under resources/css/ is reached from all.css or
  showcase.css, no exclusions left for Tailwind); the Workbench-entry
  test and every "moved out of tailwind.css" assertion updated for the
  single entry and its removal.
- DesignGuard.php's comments and the development skill's setup section
  no longer name the deleted files or a second Tailwind entry.

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 06:49:03 +02:00
co-authored by Claude Sonnet 5
parent cfc57a9867
commit f21943746e
24 changed files with 109 additions and 1310 deletions
@@ -24,8 +24,6 @@ Composer packages must be installed before the Vite build (in Dockerfiles and CI
`foundation.css` is required and first; then one stylesheet per component the views render, under `resources/css/components/` or `resources/css/layout/`, named like the component (`<x-tab>` is drawn by `tabs.css`, and `<x-theme-script>` needs none). Each imports the stylesheets of the components it draws (a split button's button and menu), and Vite keeps a file several of them import once. `resources/css/all.css` stands for the foundation and every component at once. A component rendered without its stylesheet is unstyled, so add the import with the tag.
While the application's own views still write Tailwind classes, Tailwind goes in a second entry, never beside these imports (`@tailwindcss/vite` would repeat every shared stylesheet): `@import 'tailwindcss'`, `resources/css/tailwind.css` and the two `@source` lines for `resources/views` and `src`, with `@layer properties, theme, base, material, components, utilities;` at the top of both entries.
```js
// resources/js/app.js
import '../../vendor/nonameweb/livewire-material/resources/js/material.js'
-22
View File
@@ -1,22 +0,0 @@
/*
* Livewire Material's Tailwind half: the theme that turns the tokens into utilities and the token
* utilities themselves (`state-layer`, `focus-ring`, `touch-target`, `link`, `type-*`), for a view
* that still writes Tailwind classes the showcase until step 38, and an application's own views
* until it drops Tailwind. Every component stylesheet is plain CSS (plan step 36) and needs
* nothing from here. This file goes when the last Tailwind class leaves (step 39).
*
* It carries no tokens, so it builds beside the package's CSS without declaring them twice, in an
* entry of its own `@tailwindcss/vite` inlines the imports of an entry that uses Tailwind without
* deduplicating them, so the package's stylesheets stay in another (the Workbench's package.css).
* The Workbench's Tailwind entry, like an application's:
*
* @layer properties, theme, base, material, components, utilities;
* @import 'tailwindcss' source(none);
* @import '../../../resources/css/tailwind.css';
*
* with the same layer statement at the top of the package's entry. It replaces nothing of the 1.x
* `material.css` shortcut, which is gone: the components come from `all.css` or their own files.
*/
@import './tokens/theme.css';
@import './tokens/utilities.css';
-179
View File
@@ -1,179 +0,0 @@
/*
* The Tailwind theme: every token as a utility.
*
* `--color-*: initial` clears Tailwind's default palette, so every colour a template can name
* is an M3 role (bg-primary, text-on-surface-variant, border-outline-variant) and a
* `text-red-600` compiles to nothing. White and black come back as the two absolutes. An
* application that wants more colours adds them in its own @theme after importing this file.
*
* The colour blocks are `@theme inline`: `bg-primary` compiles to
* `background-color: var(--md-sys-color-primary)`, resolved on the element itself. Without
* `inline` it compiles to `var(--color-primary)`, a variable computed once on :root the
* page-wide theme still switches, but a `data-theme="dark"` section inside a light page keeps
* the light colours. Only blocks whose values are variables may be inline; a literal value in
* an inline block would be copied into every utility and lose its variable.
*/
@theme {
--color-*: initial;
--color-white: #ffffff;
--color-black: #000000;
--font-sans: var(--md-ref-typeface-brand);
}
/*
* Breakpoints are M3's window size classes, and only those: compact is below `medium`, then
* medium 600, expanded 840, large 1200 and extra-large 1600 (`medium:`, `expanded:`, `large:`,
* `extra-large:`, and `max-medium:` for "below"). Tailwind's sm/md/lg/xl/2xl are cleared
* a `sm:` compiles to nothing because a layout decision keyed on 640px means nothing in M3.
* resources/js/breakpoints.js carries the same four numbers for scripts.
*/
@theme {
--breakpoint-*: initial;
--breakpoint-medium: 600px;
--breakpoint-expanded: 840px;
--breakpoint-large: 1200px;
--breakpoint-extra-large: 1600px;
}
/*
* Only M3's scales compile. Tailwind's default radius, shadow, text-size, weight, leading,
* tracking and easing utilities are cleared like its palette, so `rounded-lg`, `text-sm`,
* `font-medium`, `shadow-md` or `ease-in-out` compile to nothing; the M3 sets are declared
* below (`rounded-corner-*`, `shadow-elevation-*`, `type-*`, `ease-spatial-*`). The 4px
* spacing scale stays: it is M3's spacing grid.
*/
@theme {
--radius-*: initial;
--shadow-*: initial;
--inset-shadow-*: initial;
--drop-shadow-*: initial;
--text-*: initial;
--font-weight-*: initial;
--leading-*: initial;
--tracking-*: initial;
--ease-*: initial;
}
/* The colour roles, as generated by `php artisan material:scheme`. */
@theme inline {
--color-background: var(--md-sys-color-background);
--color-on-background: var(--md-sys-color-on-background);
--color-surface: var(--md-sys-color-surface);
--color-surface-dim: var(--md-sys-color-surface-dim);
--color-surface-bright: var(--md-sys-color-surface-bright);
--color-surface-container-lowest: var(--md-sys-color-surface-container-lowest);
--color-surface-container-low: var(--md-sys-color-surface-container-low);
--color-surface-container: var(--md-sys-color-surface-container);
--color-surface-container-high: var(--md-sys-color-surface-container-high);
--color-surface-container-highest: var(--md-sys-color-surface-container-highest);
--color-on-surface: var(--md-sys-color-on-surface);
--color-on-surface-variant: var(--md-sys-color-on-surface-variant);
--color-outline: var(--md-sys-color-outline);
--color-outline-variant: var(--md-sys-color-outline-variant);
--color-inverse-surface: var(--md-sys-color-inverse-surface);
--color-inverse-on-surface: var(--md-sys-color-inverse-on-surface);
--color-primary: var(--md-sys-color-primary);
--color-primary-dim: var(--md-sys-color-primary-dim);
--color-on-primary: var(--md-sys-color-on-primary);
--color-primary-container: var(--md-sys-color-primary-container);
--color-on-primary-container: var(--md-sys-color-on-primary-container);
--color-primary-fixed: var(--md-sys-color-primary-fixed);
--color-primary-fixed-dim: var(--md-sys-color-primary-fixed-dim);
--color-on-primary-fixed: var(--md-sys-color-on-primary-fixed);
--color-on-primary-fixed-variant: var(--md-sys-color-on-primary-fixed-variant);
--color-inverse-primary: var(--md-sys-color-inverse-primary);
--color-secondary: var(--md-sys-color-secondary);
--color-secondary-dim: var(--md-sys-color-secondary-dim);
--color-on-secondary: var(--md-sys-color-on-secondary);
--color-secondary-container: var(--md-sys-color-secondary-container);
--color-on-secondary-container: var(--md-sys-color-on-secondary-container);
--color-secondary-fixed: var(--md-sys-color-secondary-fixed);
--color-secondary-fixed-dim: var(--md-sys-color-secondary-fixed-dim);
--color-on-secondary-fixed: var(--md-sys-color-on-secondary-fixed);
--color-on-secondary-fixed-variant: var(--md-sys-color-on-secondary-fixed-variant);
--color-tertiary: var(--md-sys-color-tertiary);
--color-tertiary-dim: var(--md-sys-color-tertiary-dim);
--color-on-tertiary: var(--md-sys-color-on-tertiary);
--color-tertiary-container: var(--md-sys-color-tertiary-container);
--color-on-tertiary-container: var(--md-sys-color-on-tertiary-container);
--color-tertiary-fixed: var(--md-sys-color-tertiary-fixed);
--color-tertiary-fixed-dim: var(--md-sys-color-tertiary-fixed-dim);
--color-on-tertiary-fixed: var(--md-sys-color-on-tertiary-fixed);
--color-on-tertiary-fixed-variant: var(--md-sys-color-on-tertiary-fixed-variant);
--color-error: var(--md-sys-color-error);
--color-error-dim: var(--md-sys-color-error-dim);
--color-on-error: var(--md-sys-color-on-error);
--color-error-container: var(--md-sys-color-error-container);
--color-on-error-container: var(--md-sys-color-on-error-container);
--color-success: var(--md-sys-color-success);
--color-on-success: var(--md-sys-color-on-success);
--color-success-container: var(--md-sys-color-success-container);
--color-on-success-container: var(--md-sys-color-on-success-container);
--color-warning: var(--md-sys-color-warning);
--color-on-warning: var(--md-sys-color-on-warning);
--color-warning-container: var(--md-sys-color-warning-container);
--color-on-warning-container: var(--md-sys-color-on-warning-container);
--color-info: var(--md-sys-color-info);
--color-on-info: var(--md-sys-color-on-info);
--color-info-container: var(--md-sys-color-info-container);
--color-on-info-container: var(--md-sys-color-on-info-container);
--color-inverse-error: var(--md-sys-color-inverse-error);
--color-inverse-success: var(--md-sys-color-inverse-success);
--color-inverse-warning: var(--md-sys-color-inverse-warning);
--color-inverse-info: var(--md-sys-color-inverse-info);
--color-shadow: var(--md-sys-color-shadow);
--color-scrim: var(--md-sys-color-scrim);
}
/*
* Ink and lines by what they are: body copy, metadata, decoration; a structural line, a line
* around chrome, a divider. Each is an M3 role, never an opacity: M3 marks emphasis with
* on-surface-variant and outline, and reserves 38% for disabled content, so an ink dimmed by
* opacity reads as disabled and loses its contrast guarantee.
*/
@theme inline {
--color-body: var(--md-sys-color-on-surface-variant);
--color-meta: var(--md-sys-color-on-surface-variant);
--color-quiet: var(--md-sys-color-outline);
--color-structure: var(--md-sys-color-outline-variant);
--color-chrome: var(--md-sys-color-outline-variant);
--color-divider: var(--md-sys-color-outline-variant);
}
@theme {
--radius-corner-none: var(--md-sys-shape-corner-none);
--radius-corner-xs: var(--md-sys-shape-corner-xs);
--radius-corner-sm: var(--md-sys-shape-corner-sm);
--radius-corner-md: var(--md-sys-shape-corner-md);
--radius-corner-lg: var(--md-sys-shape-corner-lg);
--radius-corner-lg-increased: var(--md-sys-shape-corner-lg-increased);
--radius-corner-xl: var(--md-sys-shape-corner-xl);
--radius-corner-xl-increased: var(--md-sys-shape-corner-xl-increased);
--radius-corner-xxl: var(--md-sys-shape-corner-xxl);
--radius-corner-full: var(--md-sys-shape-corner-full);
--shadow-elevation-1: var(--md-sys-elevation-1);
--shadow-elevation-2: var(--md-sys-elevation-2);
--shadow-elevation-3: var(--md-sys-elevation-3);
--shadow-elevation-4: var(--md-sys-elevation-4);
--shadow-elevation-5: var(--md-sys-elevation-5);
--ease-spatial-fast: var(--md-sys-motion-spatial-fast);
--ease-spatial-default: var(--md-sys-motion-spatial-default);
--ease-spatial-slow: var(--md-sys-motion-spatial-slow);
--ease-effects-fast: var(--md-sys-motion-effects-fast);
--ease-effects-default: var(--md-sys-motion-effects-default);
--ease-effects-slow: var(--md-sys-motion-effects-slow);
--ease-standard: var(--md-sys-motion-easing-standard);
--ease-emphasized: var(--md-sys-motion-easing-emphasized);
--ease-emphasized-decelerate: var(--md-sys-motion-easing-emphasized-decelerate);
--ease-emphasized-accelerate: var(--md-sys-motion-easing-emphasized-accelerate);
}
/*
* `dark:` follows the page's theme, not the operating system: <x-theme-script> has already
* resolved `system` into data-theme, and the OS was never meant to be a second voice.
*/
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
-286
View File
@@ -1,286 +0,0 @@
/*
* The token utilities, for the components still written in Tailwind.
*
* Tailwind's side of tokens/state.css and tokens/type.css, kept apart so that the token files the
* foundation imports carry no Tailwind directive. tailwind.css imports this file after Tailwind;
* it goes when the last component leaves Tailwind. New markup uses the plain-CSS classes instead,
* which carry the same declarations: `md-state-layer`, `md-focus-ring`, `md-touch-target` and
* `md-link` (foundation/interaction.css), and `md-type-*` (text.css).
*
* `state-layer` puts M3's state layer on an element. Hover only where the pointer can hover,
* because a touch screen keeps the last hover after a tap; `data-dragged` on the element draws
* the dragged layer. The layer is a ::before, so the element becomes `position: relative` and
* `isolation: isolate`; never pass `static` or a z-index to it.
*
* `touch-target` extends an element's pointer target to M3's 48×48px minimum without changing
* what is drawn: a ::after centred on it, at least 48px each way. It is `::after`-based, so a
* component that already draws with ::after wraps its control in an element carrying it.
*
* `focus-ring` is M3's focus indicator 3px in secondary, 2px out, drawn only for keyboard
* focus. The site gives no thickness or offset; the values are @material/web's focus ring
* tokens (Apache-2.0, © Google LLC). `link` is a link in running text: underlined in the
* colour of the words around it.
*
* `type-*` is one of M3's 30 type styles (tokens/type.css): size, line height, weight and
* tracking together. M3 asks for emphasis one element at a time, and `font-variation-settings`
* inherits, so every regular utility resets it: body copy inside an emphasized heading reads as
* itself again.
*/
@utility state-layer {
position: relative;
isolation: isolate;
&::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background-color: currentColor;
opacity: 0;
pointer-events: none;
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
&:hover::before {
opacity: var(--md-sys-state-hover-state-layer-opacity);
}
}
&:focus-visible::before {
opacity: var(--md-sys-state-focus-state-layer-opacity);
}
&:active::before {
opacity: var(--md-sys-state-pressed-state-layer-opacity);
}
&[data-dragged]::before {
opacity: var(--md-sys-state-dragged-state-layer-opacity);
}
&:disabled::before,
&[aria-disabled='true']::before {
display: none;
}
}
@utility touch-target {
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
min-width: 3rem;
min-height: 3rem;
transform: translate(-50%, -50%);
}
}
@utility focus-ring {
outline: none;
&:focus-visible {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
}
@utility link {
cursor: pointer;
text-decoration-line: underline;
text-decoration-thickness: from-font;
text-underline-offset: 0.15em;
border-radius: var(--md-sys-shape-corner-xs);
&:focus-visible {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
}
@utility type-display-lg {
font: var(--md-sys-typescale-display-lg);
letter-spacing: var(--md-sys-typescale-display-lg-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-display-lg {
font: var(--md-sys-typescale-emphasized-display-lg);
letter-spacing: var(--md-sys-typescale-emphasized-display-lg-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-display-md {
font: var(--md-sys-typescale-display-md);
letter-spacing: var(--md-sys-typescale-display-md-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-display-md {
font: var(--md-sys-typescale-emphasized-display-md);
letter-spacing: var(--md-sys-typescale-emphasized-display-md-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-display-sm {
font: var(--md-sys-typescale-display-sm);
letter-spacing: var(--md-sys-typescale-display-sm-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-display-sm {
font: var(--md-sys-typescale-emphasized-display-sm);
letter-spacing: var(--md-sys-typescale-emphasized-display-sm-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-headline-lg {
font: var(--md-sys-typescale-headline-lg);
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-headline-lg {
font: var(--md-sys-typescale-emphasized-headline-lg);
letter-spacing: var(--md-sys-typescale-emphasized-headline-lg-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-headline-md {
font: var(--md-sys-typescale-headline-md);
letter-spacing: var(--md-sys-typescale-headline-md-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-headline-md {
font: var(--md-sys-typescale-emphasized-headline-md);
letter-spacing: var(--md-sys-typescale-emphasized-headline-md-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-headline-sm {
font: var(--md-sys-typescale-headline-sm);
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-headline-sm {
font: var(--md-sys-typescale-emphasized-headline-sm);
letter-spacing: var(--md-sys-typescale-emphasized-headline-sm-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-title-lg {
font: var(--md-sys-typescale-title-lg);
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-title-lg {
font: var(--md-sys-typescale-emphasized-title-lg);
letter-spacing: var(--md-sys-typescale-emphasized-title-lg-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-title-md {
font: var(--md-sys-typescale-title-md);
letter-spacing: var(--md-sys-typescale-title-md-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-title-md {
font: var(--md-sys-typescale-emphasized-title-md);
letter-spacing: var(--md-sys-typescale-emphasized-title-md-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-title-sm {
font: var(--md-sys-typescale-title-sm);
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-title-sm {
font: var(--md-sys-typescale-emphasized-title-sm);
letter-spacing: var(--md-sys-typescale-emphasized-title-sm-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-body-lg {
font: var(--md-sys-typescale-body-lg);
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-body-lg {
font: var(--md-sys-typescale-emphasized-body-lg);
letter-spacing: var(--md-sys-typescale-emphasized-body-lg-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-body-md {
font: var(--md-sys-typescale-body-md);
letter-spacing: var(--md-sys-typescale-body-md-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-body-md {
font: var(--md-sys-typescale-emphasized-body-md);
letter-spacing: var(--md-sys-typescale-emphasized-body-md-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-body-sm {
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-body-sm {
font: var(--md-sys-typescale-emphasized-body-sm);
letter-spacing: var(--md-sys-typescale-emphasized-body-sm-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-label-lg {
font: var(--md-sys-typescale-label-lg);
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-label-lg {
font: var(--md-sys-typescale-emphasized-label-lg);
letter-spacing: var(--md-sys-typescale-emphasized-label-lg-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-label-md {
font: var(--md-sys-typescale-label-md);
letter-spacing: var(--md-sys-typescale-label-md-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-label-md {
font: var(--md-sys-typescale-emphasized-label-md);
letter-spacing: var(--md-sys-typescale-emphasized-label-md-tracking);
font-variation-settings: "ROND" 100;
}
@utility type-label-sm {
font: var(--md-sys-typescale-label-sm);
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
font-variation-settings: normal;
}
@utility type-emphasized-label-sm {
font: var(--md-sys-typescale-emphasized-label-sm);
letter-spacing: var(--md-sys-typescale-emphasized-label-sm-tracking);
font-variation-settings: "ROND" 100;
}