Plan step 36 (containment group): <x-drawer>'s class lists move into resources/css/components/drawer.css, keyed on data-md-drawer (with data-md-open, data-md-standard) and data-md-drawer-scrim/-sheet (data-md-side)/-head/-head-row/-heading/-title/-subtitle/-close/-body/ -actions. The modal sheet slides in on the emphasized-decelerate easing and out on emphasized-accelerate (an asymmetric CSS transition expressed by giving the open-state selector its own transition-duration/-timing-function, which the "after-change style" rule picks up); the sheet stays x-show-driven, with @starting-style + transition-behavior: allow-discrete taking the place of x-transition's enter/leave classes, matching resources/css/components/search.css's already-rewritten scrim/view. A standard sheet drops the scrim and trap from 840px and only fades in place (an in-flow box does not travel); data-md-drawer-collapsed (from the view's `wide` Alpine state) takes it out of the layout while closed there. <x-drawer pane> is removed: step 35's <x-list-detail> is M3's form of that job (the plan's decision, docs/plans/material-3-alignment.md "Decisions"). Removed the prop, its CSS classes and its tests (OverlayTest's two pane-only tests deleted, the others' pane cases dropped); pointed each mention at <x-list-detail> in the material-3 guideline, the material-3-design and livewire-material-development skills, and added a line to UPGRADE.md's 2.0.0 notes. No other living file named `<x-drawer pane>` (grepped views, tests, README, boost, workbench excluding build output); docs/plans and docs/audits keep their history unchanged. Imported from the Containment block of components.css, importing button.css and divider.css for what the view renders. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
115 lines
5.7 KiB
PHP
115 lines
5.7 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
|
|
|
/**
|
|
* The containment components, rewritten without Tailwind (plan step 36): each view renders
|
|
* `data-md-*` attributes and no class list of its own beyond the interaction and text classes
|
|
* (tests/Support/ViewClasses.php), and each has a stylesheet in `material.components` that imports
|
|
* 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.
|
|
*/
|
|
dataset('containment components', [
|
|
'divider',
|
|
'collapse',
|
|
'card',
|
|
'list',
|
|
'list-item',
|
|
'modal',
|
|
'drawer',
|
|
]);
|
|
|
|
/**
|
|
* Fails unless every media query in the stylesheet writes its lengths in px, at one of M3's four
|
|
* breakpoints. `rem` and `em` would grow with the reader's text size, which a breakpoint must not.
|
|
*/
|
|
function assertContainmentBreakpointsInPx(ComponentStylesheet $css, string $label): void
|
|
{
|
|
foreach ($css->mediaQueries() 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', "{$label}: {$query}");
|
|
expect(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$label}: {$query} is not at an M3 breakpoint");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Colours are roles, shadows are elevation levels or a ring, type is a type style, and breakpoints
|
|
* are px, at one of M3's four.
|
|
*/
|
|
function assertContainmentTokensAndPxBreakpoints(ComponentStylesheet $css, string $label): void
|
|
{
|
|
$source = (string) preg_replace('~/\*.*?\*/~s', '', $css->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)/');
|
|
|
|
// A box-shadow is `none`, an elevation level, or a ring: `[inset] 0 0 0 <n>px` in a colour role.
|
|
$colour = 'var\(--md-sys-color-[a-z-]+\)|color-mix\(in srgb, var\(--md-sys-color-[a-z-]+\) [^;]+?, transparent\)';
|
|
|
|
preg_match_all('/\bbox-shadow:\s*([^;]+);/', $source, $shadows, PREG_SET_ORDER);
|
|
|
|
foreach ($shadows as [, $value]) {
|
|
expect(trim($value))->toMatch("/^(?:none|var\\(--md-sys-elevation-[0-5]\\)|(?:inset )?0 0 0 \\d+px (?:{$colour}))$/", "{$label}: box-shadow: {$value} is not none, an elevation or a ring in a colour role");
|
|
}
|
|
|
|
assertContainmentBreakpointsInPx($css, $label);
|
|
}
|
|
|
|
it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) {
|
|
$css = ComponentStylesheet::read($name);
|
|
|
|
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($name)).'/'.$import))->toBeTrue("{$name}.css imports {$import}, which does not exist");
|
|
}
|
|
})->with('containment components');
|
|
|
|
it('imports the stylesheet of every component its view renders', function (string $name) {
|
|
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php"), $tags);
|
|
|
|
$rendered = collect($tags[1])->unique()
|
|
// A component still written for Tailwind has no stylesheet of its own to import yet; its
|
|
// old stylesheet, if it has one, lives in tailwind.css without the layer statement.
|
|
->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($name)->imports())))->toBe([]);
|
|
})->with('containment components');
|
|
|
|
it('writes no class list into the view but the interaction and text classes', function (string $name) {
|
|
expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php")))->toBe([]);
|
|
})->with('containment components');
|
|
|
|
it('takes its values from the tokens and its breakpoints in px', function (string $name) {
|
|
assertContainmentTokensAndPxBreakpoints(ComponentStylesheet::read($name), "{$name}.css");
|
|
})->with('containment components');
|
|
|
|
it('is imported from the containment block of components.css', function (string $name) {
|
|
$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/{$name}.css';");
|
|
})->with('containment components');
|