Rewrite the error pages without Tailwind
Plan step 36 (containment group, error pages): errors::minimal's class lists move into resources/css/components/error-page.css, keyed on data-md-error-page/-art/-shape/-code/-headline/-message/-actions; body carries no hook of its own and is styled by a bare `body` selector, matching how ErrorPage::fallbackStyles() already styles it (no other rewritten view is a full HTML document). The section contract (title, code, headline, message, shape, actions) and the framework's 401/402 compatibility are untouched, since neither depends on a class or a hook. The shape's <x-shape> (a component outside this batch, and one with no "fill the parent" prop) is coloured and sized to its container by a plain `svg` descendant selector rather than a class or a new prop. The page's font stays a literal system stack, not var(--md-ref-typeface-brand): Tailwind's own `font-sans` utility this replaces was never the brand font either (resources/css/tokens/theme.css's --font-sans is the only utility that is), and an error page must read before any webfont has loaded. ErrorPage::fallbackStyles() draws onto the same hooks, renamed the same way (data-error-* -> data-md-error-*, data-error-fallback -> data-md-error-fallback) so the no-build path keeps working; how it is built (a hand-written heredoc, not Stylesheets::bundle()) is unchanged, per step 40. Updated in the same commit: tests/Feature/ErrorPagesTest.php and tests/Browser/ErrorPagesTest.php. The error pages' views live outside resources/views/components/, so they are not in ContainmentStylesheetsTest's dataset (its "imports what its view renders" check reads a fixed resources/views/components/<name>.blade.php path); adapted tests/Feature/ErrorPagesTest.php instead, with the same stylesheet- shape, import, ViewClasses, token/px-breakpoint and containment-block checks the dataset gives every other component. Imported from the Containment block of components.css. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a149970dd0
commit
a8c2d88fbb
@@ -66,5 +66,6 @@
|
||||
@import './components/bottom-sheet.css';
|
||||
@import './components/carousel-item.css';
|
||||
@import './components/carousel.css';
|
||||
@import './components/error-page.css';
|
||||
|
||||
/* Navigation */
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* `errors::minimal`: the layout the package's 403/404/419/429/500/503 pages and the framework's
|
||||
* own 401/402 (which extend this layout, `src/Support/ErrorPage.php`'s docblock) render through.
|
||||
* A page's own `@section`s fill `title`, `code`, `headline`, `message`, `shape` and `actions`
|
||||
* (`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
|
||||
* [data-md-error-page]
|
||||
* [data-md-error-art]
|
||||
* [data-md-error-shape] an <x-shape>, turning once a minute unless reduced motion asks
|
||||
* otherwise (its `svg` fills the art box and is coloured here,
|
||||
* since <x-shape size> only takes a fixed px, not "fill parent")
|
||||
* [data-md-error-code] the status, in display type over the shape
|
||||
* [data-md-error-headline] what happened
|
||||
* [data-md-error-message] one sentence on what to do, only when `headline` is its own
|
||||
* 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 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).
|
||||
*
|
||||
* 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
|
||||
* pairing `docs/audits/m3-alignment/containment.md`'s "Aligned" section confirms; 24px page
|
||||
* gutters and the 600px growth of the art and headline are the same numbers the audit found
|
||||
* clean. `--md-sys-typescale-emphasized-display-lg` is the code's weight and roundedness; the
|
||||
* shape's 60s linear turn has no motion token (`--md-sys-motion-*` pairs a duration with a spring
|
||||
* for a state change, not an ambient loop) and stays a literal, gated the way the audit's
|
||||
* "Aligned" section already found it: behind `prefers-reduced-motion: no-preference`.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './button.css';
|
||||
@import './shape.css';
|
||||
|
||||
@layer material.components {
|
||||
body {
|
||||
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;
|
||||
}
|
||||
|
||||
[data-md-error-page] {
|
||||
display: flex;
|
||||
min-height: 100dvh;
|
||||
max-width: 576px;
|
||||
margin-inline: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline: var(--md-sys-measurement-space300);
|
||||
padding-block: var(--md-sys-measurement-space600);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-md-error-art] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-error-art] {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-error-shape] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
[data-md-error-shape] svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--md-sys-color-primary-container);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-md-error-shape] {
|
||||
animation: material-error-turn 60s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes material-error-turn {
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-error-code] {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
font: var(--md-sys-typescale-emphasized-display-lg);
|
||||
letter-spacing: var(--md-sys-typescale-emphasized-display-lg-tracking);
|
||||
font-variation-settings: 'ROND' 100;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-md-error-headline] {
|
||||
margin: var(--md-sys-measurement-space500) 0 0;
|
||||
font: var(--md-sys-typescale-headline-md);
|
||||
letter-spacing: var(--md-sys-typescale-headline-md-tracking);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@media (width >= 600px) {
|
||||
[data-md-error-headline] {
|
||||
font: var(--md-sys-typescale-headline-lg);
|
||||
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-error-message] {
|
||||
margin: 12px 0 0;
|
||||
max-width: 448px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-body-lg);
|
||||
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
[data-md-error-actions] {
|
||||
display: flex;
|
||||
margin-top: var(--md-sys-measurement-space500);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,13 @@
|
||||
the way here, a text Back.
|
||||
|
||||
The app's own Vite entries (`livewire-material.showcase.vite`) bring its scheme, font and
|
||||
utilities. But an error page is also what shows while a deploy has no build yet, so when
|
||||
those tags cannot be made the page brings a small stylesheet of its own: the app's scheme
|
||||
from its scheme data, drawn onto the `data-error-*` hooks. Keep the hooks when changing
|
||||
the markup. The shape turns once a minute, unless the visitor asks for reduced motion. --}}
|
||||
the package's stylesheets, including this layout's own,
|
||||
resources/css/components/error-page.css. But an error page is also what shows while a deploy
|
||||
has no build yet, so when those tags cannot be made the page brings a small stylesheet of its
|
||||
own instead: the app's scheme from its scheme data, drawn onto the same `data-md-error-*`
|
||||
hooks (`src/Support/ErrorPage.php::fallbackStyles()`). Keep the hooks in step across both
|
||||
when changing the markup. The shape turns once a minute, unless the visitor asks for reduced
|
||||
motion. --}}
|
||||
|
||||
@php
|
||||
$assets = \NoNameWeb\LivewireMaterial\Support\ErrorPage::assets();
|
||||
@@ -37,25 +40,20 @@
|
||||
@if ($assets !== null)
|
||||
{{ $assets }}
|
||||
@else
|
||||
<style data-error-fallback>{{ \NoNameWeb\LivewireMaterial\Support\ErrorPage::fallbackStyles() }}</style>
|
||||
<style data-md-error-fallback>{{ \NoNameWeb\LivewireMaterial\Support\ErrorPage::fallbackStyles() }}</style>
|
||||
@endif
|
||||
|
||||
<style>
|
||||
@keyframes material-error-turn { to { transform: rotate(1turn); } }
|
||||
@media (prefers-reduced-motion: no-preference) { [data-error-shape] { animation: material-error-turn 60s linear infinite; } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-dvh bg-surface font-sans text-on-surface antialiased">
|
||||
<main data-error-page class="mx-auto flex min-h-dvh max-w-xl flex-col items-center justify-center px-6 py-12 text-center">
|
||||
<div data-error-art class="relative grid size-48 shrink-0 place-items-center medium:size-60">
|
||||
<div data-error-shape class="absolute inset-0">
|
||||
<x-livewire-material::shape :name="trim($__env->yieldContent('shape', 'cookie-7'))" class="size-full text-primary-container" />
|
||||
<body>
|
||||
<main data-md-error-page>
|
||||
<div data-md-error-art>
|
||||
<div data-md-error-shape>
|
||||
<x-livewire-material::shape :name="trim($__env->yieldContent('shape', 'cookie-7'))" />
|
||||
</div>
|
||||
|
||||
<p data-error-code class="relative type-emphasized-display-lg text-on-primary-container tabular-nums">@yield('code')</p>
|
||||
<p data-md-error-code>@yield('code')</p>
|
||||
</div>
|
||||
|
||||
<h1 data-error-headline class="mt-10 type-headline-md text-balance medium:type-headline-lg">
|
||||
<h1 data-md-error-headline>
|
||||
@hasSection('headline')
|
||||
@yield('headline')
|
||||
@else
|
||||
@@ -64,10 +62,10 @@
|
||||
</h1>
|
||||
|
||||
@hasSection('headline')
|
||||
<p data-error-message class="mt-3 max-w-md type-body-lg text-balance text-on-surface-variant">@yield('message')</p>
|
||||
<p data-md-error-message>@yield('message')</p>
|
||||
@endif
|
||||
|
||||
<div data-error-actions class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
||||
<div data-md-error-actions>
|
||||
@hasSection('actions')
|
||||
@yield('actions')
|
||||
@else
|
||||
|
||||
+15
-12
@@ -60,7 +60,10 @@ 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-error-*` hooks drawn to match the Tailwind version.
|
||||
* 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()` —
|
||||
* plan step 40 replaces this method's own body, once the design guard exists to catch a hook
|
||||
* the two drift apart on.
|
||||
*/
|
||||
public static function fallbackStyles(): HtmlString
|
||||
{
|
||||
@@ -76,17 +79,17 @@ class ErrorPage
|
||||
[data-theme='dark'] { color-scheme: dark; {$roles($scheme['dark'])}}
|
||||
*, ::before, ::after { box-sizing: border-box; }
|
||||
body { margin: 0; background-color: var(--md-sys-color-surface); color: var(--md-sys-color-on-surface); font: 400 1rem/1.5rem ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
[data-error-page] { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100dvh; max-width: 36rem; margin: 0 auto; padding: 3rem 1.5rem; text-align: center; }
|
||||
[data-error-art] { position: relative; display: grid; place-items: center; width: 12rem; height: 12rem; }
|
||||
[data-error-shape] { position: absolute; inset: 0; color: var(--md-sys-color-primary-container); }
|
||||
[data-error-shape] svg { display: block; width: 100%; height: 100%; }
|
||||
[data-error-code] { position: relative; margin: 0; font-size: 3.5625rem; font-weight: 500; line-height: 4rem; color: var(--md-sys-color-on-primary-container); font-variant-numeric: tabular-nums; }
|
||||
[data-error-headline] { margin: 2.5rem 0 0; font-size: 1.75rem; font-weight: 400; line-height: 2.25rem; text-wrap: balance; }
|
||||
[data-error-message] { margin: 0.75rem 0 0; color: var(--md-sys-color-on-surface-variant); text-wrap: balance; }
|
||||
[data-error-actions] { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.75rem; margin-top: 2.5rem; }
|
||||
[data-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-error-actions] > :first-child { background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
|
||||
[data-error-actions] :is(a, button):focus-visible { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; }
|
||||
[data-md-error-page] { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100dvh; max-width: 36rem; margin: 0 auto; padding: 3rem 1.5rem; text-align: center; }
|
||||
[data-md-error-art] { position: relative; display: grid; place-items: center; width: 12rem; height: 12rem; }
|
||||
[data-md-error-shape] { position: absolute; inset: 0; color: var(--md-sys-color-primary-container); }
|
||||
[data-md-error-shape] svg { display: block; width: 100%; height: 100%; }
|
||||
[data-md-error-code] { position: relative; margin: 0; font-size: 3.5625rem; font-weight: 500; line-height: 4rem; color: var(--md-sys-color-on-primary-container); font-variant-numeric: tabular-nums; }
|
||||
[data-md-error-headline] { margin: 2.5rem 0 0; font-size: 1.75rem; font-weight: 400; line-height: 2.25rem; text-wrap: balance; }
|
||||
[data-md-error-message] { margin: 0.75rem 0 0; color: var(--md-sys-color-on-surface-variant); text-wrap: balance; }
|
||||
[data-md-error-actions] { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.75rem; margin-top: 2.5rem; }
|
||||
[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; }
|
||||
CSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function missingPage(string $colorScheme = 'light')
|
||||
$page = $colorScheme === 'dark' ? $page->inDarkMode() : $page->inLightMode();
|
||||
|
||||
return $page->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && document.querySelector('[data-error-page]') !== null");
|
||||
->assertScript("document.readyState === 'complete' && document.querySelector('[data-md-error-page]') !== null");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,10 +33,10 @@ function paintedIn(string $selector, string $role): string
|
||||
it('draws the 404 page in the scheme, in light and dark', function () {
|
||||
missingPage('light')
|
||||
->assertSee('Page not found')
|
||||
->assertVisible('[data-error-headline]')
|
||||
->assertVisible('[data-md-error-headline]')
|
||||
->assertScript("document.documentElement.getAttribute('data-theme') === 'light'")
|
||||
->assertScript(paintedIn('body', 'surface'))
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-error-shape] svg')).color !== getComputedStyle(document.body).color")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-md-error-shape] svg')).color !== getComputedStyle(document.body).color")
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
missingPage('dark')
|
||||
@@ -48,7 +48,7 @@ it('draws the 404 page in the scheme, in light and dark', function () {
|
||||
it('fits a 400px screen without scrolling sideways', function () {
|
||||
missingPage()
|
||||
->resize(400, 800)
|
||||
->assertVisible('[data-error-headline]')
|
||||
->assertVisible('[data-md-error-headline]')
|
||||
->assertScript('document.documentElement.scrollWidth <= window.innerWidth')
|
||||
->assertScript("document.querySelector('[data-error-actions] a').getBoundingClientRect().right <= 400");
|
||||
->assertScript("document.querySelector('[data-md-error-actions] a').getBoundingClientRect().right <= 400");
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\Str;
|
||||
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
|
||||
use NoNameWeb\LivewireMaterial\Support\Scheme;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->temporary = sys_get_temp_dir().'/livewire-material-errors-'.Str::random(8);
|
||||
@@ -39,7 +41,7 @@ it('renders the package page for each status', function (int $code, string $head
|
||||
$this->withoutVite()
|
||||
->get("/abort/{$code}")
|
||||
->assertStatus($code)
|
||||
->assertSee('data-error-page', false)
|
||||
->assertSee('data-md-error-page', false)
|
||||
->assertSee("{$code}</p>", false)
|
||||
->assertSee($headline)
|
||||
->assertSee($sentence);
|
||||
@@ -56,7 +58,7 @@ it('draws the framework\'s own error pages in the package layout', function () {
|
||||
$this->withoutVite()
|
||||
->get('/abort/401')
|
||||
->assertStatus(401)
|
||||
->assertSee('data-error-page', false)
|
||||
->assertSee('data-md-error-page', false)
|
||||
->assertSee('Unauthorized')
|
||||
->assertSee('Go home');
|
||||
});
|
||||
@@ -86,9 +88,9 @@ it('lets the application\'s own error view win', function () {
|
||||
->get('/abort/404')
|
||||
->assertNotFound()
|
||||
->assertSee('The application view')
|
||||
->assertDontSee('data-error-page', false);
|
||||
->assertDontSee('data-md-error-page', false);
|
||||
|
||||
$this->get('/abort/403')->assertSee('data-error-page', false);
|
||||
$this->get('/abort/403')->assertSee('data-md-error-page', false);
|
||||
});
|
||||
|
||||
it('loads the application\'s Vite entries', function () {
|
||||
@@ -105,7 +107,7 @@ it('loads the application\'s Vite entries', function () {
|
||||
->assertNotFound()
|
||||
->assertSee('build/assets/app-probe.css', false)
|
||||
->assertSee('build/assets/app-probe.js', false)
|
||||
->assertDontSee('data-error-fallback', false);
|
||||
->assertDontSee('data-md-error-fallback', false);
|
||||
});
|
||||
|
||||
it('still renders, in the application\'s scheme, when the Vite manifest is missing', function () {
|
||||
@@ -121,7 +123,7 @@ it('still renders, in the application\'s scheme, when the Vite manifest is missi
|
||||
$this->get('/abort/500')
|
||||
->assertStatus(500)
|
||||
->assertSee('Something went wrong')
|
||||
->assertSee('<style data-error-fallback>', false)
|
||||
->assertSee('<style data-md-error-fallback>', false)
|
||||
->assertSee('--md-sys-color-surface: #fafaf0;', false)
|
||||
->assertSee('--md-sys-color-primary: #123456;', false)
|
||||
->assertSee('--md-sys-color-surface: #101010;', false)
|
||||
@@ -185,7 +187,7 @@ it('prerenders the 503 page for maintenance mode, without an exception', functio
|
||||
(new RegisterErrorViewPaths)();
|
||||
|
||||
expect(view('errors::503', ['retryAfter' => 60])->render())
|
||||
->toContain('data-error-page')
|
||||
->toContain('data-md-error-page')
|
||||
->toContain('We’ll be right back')
|
||||
->toContain('location.reload()');
|
||||
});
|
||||
@@ -209,3 +211,71 @@ it('sends an expired form back to its page to refresh', function () {
|
||||
->assertSee('href="'.url('/settings').'"', false)
|
||||
->assertSee('Refresh the page');
|
||||
});
|
||||
|
||||
/**
|
||||
* Plan step 36: `errors::minimal`'s class lists moved into
|
||||
* resources/css/components/error-page.css, keyed on `data-md-error-*`. Its view lives outside
|
||||
* resources/views/components/, so it is not in ContainmentStylesheetsTest's dataset (whose
|
||||
* "imports what its view renders" check reads a fixed resources/views/components/<name>.blade.php
|
||||
* path); this is its own small stylesheet-shape test instead, checking the same things.
|
||||
*/
|
||||
it('draws the error layout from a stylesheet shaped like every package stylesheet', function () {
|
||||
$css = ComponentStylesheet::read('error-page');
|
||||
|
||||
expect($css->css)->toStartWith('/*')
|
||||
->and($css->statements()[0] ?? null)->toBe('@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;')
|
||||
->and(array_slice($css->statements(), 1))->each->toMatch('/^@import \'\.\/[a-z-]+\.css\';$/')
|
||||
->and($css->blocks())->each->toBe('@layer material.components')
|
||||
->and($css->css)->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b|--(?:theme|spacing|alpha)\(|\btheme\(/');
|
||||
|
||||
foreach ($css->imports() as $import) {
|
||||
expect(is_file(dirname(ComponentStylesheet::path('error-page')).'/'.$import))->toBeTrue("error-page.css imports {$import}, which does not exist");
|
||||
}
|
||||
});
|
||||
|
||||
it('imports the stylesheet of every component the error layout renders', function () {
|
||||
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__.'/../../resources/views/error-pages/errors/minimal.blade.php'), $tags);
|
||||
|
||||
$rendered = collect($tags[1])->unique()
|
||||
->filter(fn (string $tag): bool => is_file(ComponentStylesheet::path($tag)) && str_contains(File::get(ComponentStylesheet::path($tag)), '@layer material.components'))
|
||||
->map(fn (string $tag): string => "./{$tag}.css")
|
||||
->values()
|
||||
->all();
|
||||
|
||||
expect(array_values(array_diff($rendered, ComponentStylesheet::read('error-page')->imports())))->toBe([]);
|
||||
});
|
||||
|
||||
it('writes no class list into the error layout but the interaction and text classes', function () {
|
||||
expect(ViewClasses::violations(File::get(__DIR__.'/../../resources/views/error-pages/errors/minimal.blade.php')))->toBe([]);
|
||||
});
|
||||
|
||||
it('takes its values from the tokens and its breakpoints in px', function () {
|
||||
$source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read('error-page')->css);
|
||||
|
||||
expect($source)->not->toMatch('/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/i')
|
||||
->not->toMatch('/\bfont:(?!\s*var\(--md-sys-typescale-)/')
|
||||
->not->toMatch('/\btransition[a-z-]*:[^;]*(?:\d+m?s\b|\bease\b|ease-in|ease-out|cubic-bezier)/');
|
||||
|
||||
preg_match_all('/@media\s*([^{]+)\{/', $source, $queries);
|
||||
|
||||
foreach ($queries[1] as $query) {
|
||||
preg_match_all('/\(([^()]*)\)/', $query, $features);
|
||||
|
||||
foreach ($features[1] as $feature) {
|
||||
preg_match_all('/(\d*\.?\d+)(px|rem|em)\b/', $feature, $lengths, PREG_SET_ORDER);
|
||||
|
||||
foreach ($lengths as [, $number, $unit]) {
|
||||
expect($unit)->toBe('px', $query);
|
||||
expect(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$query} is not at an M3 breakpoint");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('is imported from the containment block of components.css', function () {
|
||||
$components = File::get(__DIR__.'/../../resources/css/components.css');
|
||||
$block = substr($components, (int) strpos($components, '/* Containment */'));
|
||||
$block = substr($block, 0, (int) strpos($block, '/* Navigation */'));
|
||||
|
||||
expect($block)->toContain("@import './components/error-page.css';");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user