From a764fddf27a1dae0e14ebac61b903366ebf5b4bd Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 22:06:56 +0200 Subject: [PATCH] Style the body only on the error page Plan step 36 review of a8c2d88f. error-page.css drew a bare `body`, which components.css already bundles into every Workbench page and step 37's all.css would put in every application page: each would take the error page's background, margin and system font. The rule now matches body:has(> [data-md-error-page]); the fallback stylesheet, inlined into the error page alone, keeps its own. A test in the containment harness and in ErrorPagesTest rejects an unscoped html, body, dialog or :root. The body's system font stack is gone: the old `font-sans` was the brand typeface (tokens/theme.css), which the page inherits from the foundation's html rule, and the header's claim otherwise was wrong. The shape's slow turn, which moved from the view's inline style into the stylesheet, is back in the fallback so the no-build page still turns. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components/error-page.css | 23 ++++++++----------- src/Support/ErrorPage.php | 5 +++- .../Components/ContainmentStylesheetsTest.php | 14 ++++++++--- tests/Feature/ErrorPagesTest.php | 12 ++++++++++ 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/resources/css/components/error-page.css b/resources/css/components/error-page.css index e6767592..5b4d982f 100644 --- a/resources/css/components/error-page.css +++ b/resources/css/components/error-page.css @@ -5,7 +5,7 @@ * (`resources/views/error-pages/errors/minimal.blade.php`'s header) — none of that is this file's * concern, only the geometry and colour of the parts that show them: * - * body + * body:has(> [data-md-error-page]) * [data-md-error-page] * [data-md-error-art] * [data-md-error-shape] an , turning once a minute unless reduced motion asks @@ -17,17 +17,15 @@ * section (otherwise `message` doubles as the headline) * [data-md-error-actions] * - * `body` carries no hook of its own — `src/Support/ErrorPage.php`'s `fallbackStyles()`, the - * stylesheet an application without a Vite build gets instead, styles the bare element the same - * way, so this file matches it rather than inventing a hook the fallback has no equivalent of. - * That fallback still needs updating by hand when this file changes, until step 40 replaces how - * it is built. + * The body is matched only when it holds the layout (`body:has(> [data-md-error-page])`): this + * file sits in the application's bundle beside every other page, where a bare `body` rule would + * restyle them all. `src/Support/ErrorPage.php`'s `fallbackStyles()`, the stylesheet an + * application without a Vite build gets instead, is inlined into the error page alone, so it keeps + * a bare `body`; it still needs updating by hand when this file changes, until step 40 replaces + * how it is built. * - * The page's own font is a literal system stack, not `var(--md-ref-typeface-brand)`: an error - * page must read correctly before any webfont has had a chance to load (`@font-face` is what the - * fallback path has none of, and this file matches it even on the path that does), and Tailwind's - * `font-sans` this replaces never carried the brand font either - * (`resources/css/tokens/theme.css`'s `--font-sans` is the only utility that does). + * The page's font is the foundation's, `--md-ref-typeface-brand` on `html` (foundation/base.css), + * as the old `font-sans` was; only the fallback, which has no `@font-face`, uses a system stack. * * Colours from the roles (`docs/reference/m3/styles.md` § Typography, "Accessibility * requirements": surface/on-surface); the display code over its primary-container shape is the @@ -45,12 +43,11 @@ @import './shape.css'; @layer material.components { - body { + body:has(> [data-md-error-page]) { margin: 0; min-height: 100dvh; background-color: var(--md-sys-color-surface); color: var(--md-sys-color-on-surface); - font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing: antialiased; } diff --git a/src/Support/ErrorPage.php b/src/Support/ErrorPage.php index ce03b2f0..f0694466 100644 --- a/src/Support/ErrorPage.php +++ b/src/Support/ErrorPage.php @@ -61,7 +61,8 @@ class ErrorPage /** * A stylesheet for a page without its build: the application's scheme in both themes, a * system font, and the layout's `data-md-error-*` hooks drawn to match the built version - * (resources/css/components/error-page.css). Still hand-built, not `Stylesheets::bundle()` — + * (resources/css/components/error-page.css), the shape's slow turn included. Inlined into the + * error page alone, so its `body` rule needs no scope. Still hand-built, not `Stylesheets::bundle()` — * plan step 40 replaces this method's own body, once the design guard exists to catch a hook * the two drift apart on. */ @@ -90,6 +91,8 @@ class ErrorPage [data-md-error-actions] :is(a, button) { display: inline-flex; align-items: center; height: 3.5rem; padding: 0 1.5rem; border: 0; border-radius: 9999px; background: none; color: var(--md-sys-color-primary); font-family: inherit; font-size: 1rem; font-weight: 500; line-height: 1.5rem; text-decoration: none; cursor: pointer; } [data-md-error-actions] > :first-child { background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); } [data-md-error-actions] :is(a, button):focus-visible { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; } + @keyframes material-error-turn { to { transform: rotate(1turn); } } + @media (prefers-reduced-motion: no-preference) { [data-md-error-shape] { animation: material-error-turn 60s linear infinite; } } CSS); } } diff --git a/tests/Feature/Components/ContainmentStylesheetsTest.php b/tests/Feature/Components/ContainmentStylesheetsTest.php index 2be71f64..837c8d62 100644 --- a/tests/Feature/Components/ContainmentStylesheetsTest.php +++ b/tests/Feature/Components/ContainmentStylesheetsTest.php @@ -11,9 +11,9 @@ use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses; * the stylesheets of the components its view renders, writes its values from the tokens and its * breakpoints as px range queries, and is imported from the "Containment" block of components.css. * - * The dataset grows by one name per component commit (`divider`, `collapse`, `card`, then `list` - * and `list-item` together), the same rule InputStylesheetsTest.php and ActionStylesheetsTest.php - * follow. It starts empty: this file lands before any containment component is rewritten. + * The dataset grew by one name per component commit, the same rule InputStylesheetsTest.php and + * ActionStylesheetsTest.php follow. The error layout, whose view is not a component, has the same + * checks in tests/Feature/ErrorPagesTest.php. */ dataset('containment components', [ 'divider', @@ -115,3 +115,11 @@ it('is imported from the containment block of components.css', function (string expect($block)->toContain("@import './components/{$name}.css';"); })->with('containment components'); + +it('scopes every element-wide selector to a data-md hook', function (string $name) { + // `all.css` bundles every component stylesheet into every page, so a bare `html`, `body`, + // `dialog` or `:root` rule would restyle the application's own pages. + $source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read($name)->css); + + expect($source)->not->toMatch('/(?:^|[,{};]\s*)(?:html|body|dialog|:root)(?![\w-])(?!\[data-md-|:has\(> \[data-md-)/m'); +})->with('containment components'); diff --git a/tests/Feature/ErrorPagesTest.php b/tests/Feature/ErrorPagesTest.php index 308652ed..5ab47386 100644 --- a/tests/Feature/ErrorPagesTest.php +++ b/tests/Feature/ErrorPagesTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Vite; use Illuminate\Support\Str; use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider; +use NoNameWeb\LivewireMaterial\Support\ErrorPage; use NoNameWeb\LivewireMaterial\Support\Scheme; use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet; use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses; @@ -279,3 +280,14 @@ it('is imported from the containment block of components.css', function () { expect($block)->toContain("@import './components/error-page.css';"); }); + +it('styles the body only when it holds the error layout, and turns the shape on both paths', function () { + $source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read('error-page')->css); + + // The stylesheet travels in the application's bundle, so a bare `body` would restyle every page. + expect($source)->not->toMatch('/(?:^|[,{};]\s*)(?:html|body|dialog|:root)(?![\w-])(?!\[data-md-|:has\(> \[data-md-)/m') + ->and(ComponentStylesheet::read('error-page')->declarations('body:has(> [data-md-error-page])'))->not->toHaveKey('font-family') + ->and((string) ErrorPage::fallbackStyles()) + ->toContain('@keyframes material-error-turn') + ->toContain('@media (prefers-reduced-motion: no-preference) { [data-md-error-shape] { animation: material-error-turn 60s linear infinite; } }'); +});