Smooth the page's text in the foundation, not per surface

The showcase and the error pages set grayscale font smoothing on
themselves, but the foundation did not, so an application that dropped
Tailwind's antialiased class rendered heavier text on macOS than the
package's own pages, silently. base.css now sets it on the page; the
showcase's and error page's copies go, and the guard tells a leftover
antialiased class to go too. Decided with the user (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 12:41:20 +02:00
co-authored by Claude Opus 5
parent 6614b32fd3
commit f5f040516e
10 changed files with 25 additions and 11 deletions
+2 -1
View File
@@ -138,7 +138,8 @@ component the views render, opening with the layer statement every package style
either: layout components (`<x-scaffold>`, `<x-pane>`, `<x-stack>`, `<x-row>`, `<x-grid>`, the
canonical layouts) take M3's spacing tokens and breakpoints as props, a small set of `md-type-*`
and `md-ink-*` classes covers text on plain elements, and `--md-sys-*` custom properties serve the
rest of an application's own stylesheet.
rest of an application's own stylesheet. The foundation smooths text in grayscale, as Tailwind's
`antialiased` class did: drop the class.
### 7. New in 2.0.0
@@ -90,7 +90,7 @@ Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile'));
## Tokens
2.0.0's vocabulary is plain CSS, without Tailwind. `resources/css/foundation.css` is the one required import, first in the entry, followed by the stylesheets of the components the views render (see Setup; 1.x's `material.css` shortcut is gone). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, every `--md-sys-*` token (spacing included: `--md-sys-measurement-space25``space900`, 272px), and `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link`. Text on plain elements takes the fixed text classes of `resources/css/text.css` and nothing else: `md-type-{display|headline|title|body|label}-{lg|md|sm}` and `md-type-emphasized-…`; `md-ink` (on-surface), `md-ink-variant`, `md-ink-quiet`, `md-ink-primary`, `md-ink-error`, `md-ink-success`, `md-ink-warning`, `md-ink-info`, `md-ink-inverse`; `md-text-start|center|end`, `md-truncate`, `md-line-clamp-2|3`, `md-nowrap`, `md-tabular`, `md-visually-hidden`. Every component is plain CSS now; the Tailwind names below stay for the showcase and an application's own views, until Tailwind itself leaves the package.
2.0.0's vocabulary is plain CSS, without Tailwind. `resources/css/foundation.css` is the one required import, first in the entry, followed by the stylesheets of the components the views render (see Setup; 1.x's `material.css` shortcut is gone). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, the page's `surface` and `on-surface` in the brand typeface smoothed in grayscale (drop Tailwind's `antialiased`), every `--md-sys-*` token (spacing included: `--md-sys-measurement-space25``space900`, 272px), and `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link`. Text on plain elements takes the fixed text classes of `resources/css/text.css` and nothing else: `md-type-{display|headline|title|body|label}-{lg|md|sm}` and `md-type-emphasized-…`; `md-ink` (on-surface), `md-ink-variant`, `md-ink-quiet`, `md-ink-primary`, `md-ink-error`, `md-ink-success`, `md-ink-warning`, `md-ink-info`, `md-ink-inverse`; `md-text-start|center|end`, `md-truncate`, `md-line-clamp-2|3`, `md-nowrap`, `md-tabular`, `md-visually-hidden`. Every component is plain CSS now; the Tailwind names below stay for the showcase and an application's own views, until Tailwind itself leaves the package.
Tailwind's default palette is cleared: every colour class names an M3 role. `text-red-600`, `bg-base-200` or `text-gray-500` compile to nothing. The rules behind the names below — which role, surface container, corner, type style, elevation level, motion spring, state and window size class to use, and M3's don'ts — are the `material-3` guideline (always on) and the `material-3-design` skill (the tables and Google's source pages); activate that skill before designing a screen.
-1
View File
@@ -48,7 +48,6 @@
min-height: 100dvh;
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
-webkit-font-smoothing: antialiased;
}
[data-md-error-page] {
+7
View File
@@ -8,6 +8,11 @@
* `--md-ref-typeface-brand` (tokens/type.css), Google Sans Flex from tokens/font.css, whose
* `@font-face` this file imports into the same layer.
*
* Text is smoothed in grayscale where a platform offers the choice (macOS browsers): Google Sans
* Flex is drawn for it, and subpixel smoothing thickens its strokes on light text over a dark
* surface. Every surface the package draws the showcase, the error pages is set this way,
* and so is an application, which drops Tailwind's `antialiased` class with nothing to replace.
*
* In `material.base`, above the reset and below every component and text class.
*/
@@ -20,5 +25,7 @@
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
font-family: var(--md-ref-typeface-brand);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
-5
View File
@@ -28,11 +28,6 @@
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@layer material.components {
[data-md-showcase] {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* The rail's wordmark: `md-type-title-lg md-truncate md-focus-ring` cover the type, the
ellipsis and the ring; `display: block` gives the truncated text a width to ellipsize
within, and the corner rounds the ring to match the state layer every other rail control
+2 -1
View File
@@ -161,8 +161,9 @@ class DesignGuard
['/^(?:cursor-[\w-]+|pointer-events-(?:none|auto)|select-(?:none|text|all|auto)|touch-[\w-]+|resize(?:-[xy]|-none)?|appearance-(?:none|auto)|scroll-(?:smooth|auto)|-?scroll-[mp][trblxyse]?-[\w.]+|snap-[\w-]+)$/', 'Tailwind interactivity utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:aspect-(?:auto|square|video|\d+\/\d+)|object-(?:contain|cover|fill|none|scale-down|top|bottom|center|left|right|left-top|left-bottom|right-top|right-bottom))$/', 'Tailwind media utility `%s` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS'],
['/^font-(?:sans|serif)$/', 'Tailwind\'s `%s` compiles to nothing — the foundation already sets the brand typeface; any other `font-family` is your own CSS'],
['/^antialiased$/', 'Tailwind\'s `%s` compiles to nothing — the foundation already smooths text in grayscale (base.css); drop it'],
['/^(?:underline|no-underline)$/', 'Tailwind\'s `%s` compiles to nothing — `md-link` draws a link (interaction.css); any other decoration is your own CSS'],
['/^(?:uppercase|lowercase|capitalize|normal-case|italic|not-italic|overline|line-through|underline-offset-\w+|decoration-(?:\d+|solid|double|dotted|dashed|wavy|auto|from-font|clone|slice)|font-mono|antialiased|subpixel-antialiased|whitespace-(?:normal|pre|pre-line|pre-wrap|break-spaces)|break-(?:words|all|keep|normal)|wrap-(?:break-word|anywhere|normal)|text-(?:wrap|balance|pretty|ellipsis|clip|justify)|line-clamp-(?:\d+|none)|not-sr-only|list-(?:disc|decimal|none|inside|outside)|align-(?:baseline|top|middle|bottom|text-top|text-bottom|sub|super)|indent-[\w.]+|hyphens-(?:none|manual|auto)|(?:normal|lining|oldstyle|proportional)-nums|ordinal|slashed-zero|(?:diagonal|stacked)-fractions)$/', 'Tailwind text utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^(?:uppercase|lowercase|capitalize|normal-case|italic|not-italic|overline|line-through|underline-offset-\w+|decoration-(?:\d+|solid|double|dotted|dashed|wavy|auto|from-font|clone|slice)|font-mono|subpixel-antialiased|whitespace-(?:normal|pre|pre-line|pre-wrap|break-spaces)|break-(?:words|all|keep|normal)|wrap-(?:break-word|anywhere|normal)|text-(?:wrap|balance|pretty|ellipsis|clip|justify)|line-clamp-(?:\d+|none)|not-sr-only|list-(?:disc|decimal|none|inside|outside)|align-(?:baseline|top|middle|bottom|text-top|text-bottom|sub|super)|indent-[\w.]+|hyphens-(?:none|manual|auto)|(?:normal|lining|oldstyle|proportional)-nums|ordinal|slashed-zero|(?:diagonal|stacked)-fractions)$/', 'Tailwind text utility `%s` compiles to nothing — write the rule in your own CSS'],
['/^bg-(?:(?:gradient-to|linear-to)-[a-z]+|radial|conic|cover|contain|auto|center|top|bottom|left|right|no-repeat|repeat(?:-[xy])?|fixed|local|scroll|none|clip-[a-z]+|origin-[a-z]+)$/', 'Tailwind background utility `%s` compiles to nothing — write the rule in your own CSS'],
];
+1
View File
@@ -372,6 +372,7 @@ it('reports position, border, effect, interactivity and text utilities with the
'tailwind/utilities.blade.php:3 Tailwind text utility `font-mono` compiles to nothing — write the rule in your own CSS',
'tailwind/utilities.blade.php:3 Tailwind text utility `list-disc` compiles to nothing — write the rule in your own CSS',
'tailwind/utilities.blade.php:3 Tailwind text utility `uppercase` compiles to nothing — write the rule in your own CSS',
"tailwind/utilities.blade.php:3 Tailwind's `antialiased` compiles to nothing — the foundation already smooths text in grayscale (base.css); drop it",
"tailwind/utilities.blade.php:3 Tailwind's `font-sans` compiles to nothing — the foundation already sets the brand typeface; any other `font-family` is your own CSS",
"tailwind/utilities.blade.php:3 Tailwind's `text-end` compiles to nothing — use `md-text-end` (text.css)",
"tailwind/utilities.blade.php:3 Tailwind's `underline` compiles to nothing — `md-link` draws a link (interaction.css); any other decoration is your own CSS",
+1 -1
View File
@@ -21,7 +21,7 @@ it('serves the bundle as long-cached CSS at the hash of its own content', functi
->assertHeader('Cache-Control', 'immutable, max-age=31536000, public');
expect($response->getContent())
->toContain('[data-md-showcase]')
->toContain('[data-md-showcase-brand]')
->toContain('[data-md-pane]')
->toContain('@layer material.reset, material.tokens');
});
+10
View File
@@ -212,6 +212,16 @@ it('brings every foundation file and text.css into the foundation, and nothing e
->and(array_values(preg_grep('/^components\//', $foundation)))->toBe([]);
});
it('paints the page in surface and on-surface, in the brand typeface, smoothed in grayscale', function () {
// An application drops Tailwind's `antialiased` with nothing to replace it: the showcase and the
// error pages, which set it themselves before, read it from here too.
$base = (string) preg_replace('~/\*.*?\*/~s', '', (string) file_get_contents(stylesheetPath('foundation/base.css')));
expect($base)->toMatch('/html \{\s*background-color: var\(--md-sys-color-surface\);\s*color: var\(--md-sys-color-on-surface\);\s*font-family: var\(--md-ref-typeface-brand\);\s*-webkit-font-smoothing: antialiased;\s*-moz-osx-font-smoothing: grayscale;\s*\}/')
->and((string) file_get_contents(stylesheetPath('showcase.css')).file_get_contents(stylesheetPath('components/error-page.css')))
->not->toContain('font-smoothing');
});
it('leaves no stylesheet under resources/css/ outside all.css or showcase.css', function () {
// The full tree now that Tailwind is gone (plan step 39): every .css file on disk, not just the
// ones a bundler happens to reach — a stray or orphaned file would show up here even though
@@ -1,4 +1,4 @@
<div class="relative inset-0 -top-2 z-10 overflow-hidden shrink-0 col-start-2 justify-self-end container">
<span class="border border-t-2 rounded shadow outline-none ring-2 transition opacity-50 -rotate-45 cursor-pointer select-none"></span>
<span class="aspect-video object-cover font-mono font-sans underline uppercase text-end list-disc table bg-clip-text"></span>
<span class="aspect-video object-cover font-mono font-sans antialiased underline uppercase text-end list-disc table bg-clip-text"></span>
</div>