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; } }'); +});