Fold layout.css and components.css into all.css
Plan step 37: all.css replaces the two interim import lists with one entry for an application that wants everything — the foundation, every layout stylesheet and every component stylesheet, grouped under the same block comments components.css used, plus a Layout block. It also directly imports the three files nothing imported by name before (layout/spacing.css, layout/visibility.css, components/selection.css), so every file under components/ and layout/ is now one @import away. Every test that read a block of components.css or layout.css now reads the matching block of all.css through one shared helper (allCssBlock(), in tests/Pest.php so it loads for any test run) instead of repeating the same substr() search in each file. StylesheetsTest.php's shape, Tailwind-free and breakpoint checks, previously run twice (once for the foundation, once for the layout tree), now run once over the whole tree all.css reaches, since every component and layout stylesheet is plain CSS after step 36; a new test asserts all.css imports everything under components/ and layout/ exactly once. The Workbench imports all.css in place of layout.css and 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
04271bf227
commit
925d9a4439
@@ -10,8 +10,14 @@ use NoNameWeb\LivewireMaterial\Support\Layout;
|
||||
* would nest the layer twice); then its own `@layer material.<x>` blocks. The only rules outside
|
||||
* a layer are foundation/hidden.css's two `!important` ones.
|
||||
*
|
||||
* Until the components leave Tailwind, the checks cover the foundation — foundation.css and every
|
||||
* file it imports, text.css included — and material.css and tailwind.css carry the rest.
|
||||
* Every package component and layout stylesheet is plain CSS now (plan step 36), so the shape,
|
||||
* Tailwind-free and breakpoint checks below run over the whole tree `all.css` reaches (foundation,
|
||||
* layout and components together) rather than the foundation alone — everything but
|
||||
* `tailwind.css` and `tokens/theme.css`/`tokens/utilities.css`, which stay Tailwind until plan
|
||||
* step 39 and are simply never reached from `all.css`. A component's own values (colours as
|
||||
* roles, shadows as elevation levels, and so on) are each group's own concern, in
|
||||
* tests/Feature/Components/*StylesheetsTest.php; `bundle()`, the PHP bundler `all.css` is built
|
||||
* for, is tests/Feature/StylesheetsBundleTest.php's.
|
||||
*/
|
||||
const MATERIAL_LAYER_STATEMENT = '@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;';
|
||||
|
||||
@@ -170,6 +176,19 @@ function foundationStylesheets(): array
|
||||
return stylesheetTree(stylesheetPath('foundation.css'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything all.css reaches: the foundation, every layout stylesheet and every component
|
||||
* stylesheet, each once. What a check runs over now that every one of them is plain CSS (plan
|
||||
* step 36) — `tailwind.css` and its two token files are never in this tree, since all.css does
|
||||
* not import them.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function allStylesheets(): array
|
||||
{
|
||||
return stylesheetTree(stylesheetPath('all.css'));
|
||||
}
|
||||
|
||||
function stylesheetName(string $file): string
|
||||
{
|
||||
return substr($file, strlen(stylesheetPath()) + 1);
|
||||
@@ -189,12 +208,12 @@ it('brings every foundation file and text.css into the foundation, and nothing o
|
||||
->and($foundation)
|
||||
->toContain('tokens/scheme.css', 'tokens/shape.css', 'tokens/elevation.css', 'tokens/motion.css', 'tokens/type.css', 'tokens/state.css', 'tokens/spacing.css', 'tokens/font.css')
|
||||
// The Tailwind half: the theme, the token utilities and the components still written for it.
|
||||
->and(array_values(array_intersect($foundation, ['tokens/theme.css', 'tokens/utilities.css', 'tailwind.css', 'material.css'])))->toBe([])
|
||||
->and(array_values(array_intersect($foundation, ['tokens/theme.css', 'tokens/utilities.css', 'tailwind.css'])))->toBe([])
|
||||
->and(array_values(preg_grep('/^components\//', $foundation)))->toBe([]);
|
||||
});
|
||||
|
||||
it('opens every foundation stylesheet with a header, the layer statement and plain imports', function () {
|
||||
foreach (foundationStylesheets() as $file) {
|
||||
it('opens every stylesheet all.css reaches with a header, the layer statement and plain imports', function () {
|
||||
foreach (allStylesheets() as $file) {
|
||||
$name = stylesheetName($file);
|
||||
$items = stylesheetItems(File::get($file));
|
||||
|
||||
@@ -261,7 +280,7 @@ it('keeps every foundation rule inside its material layer, but the two that hide
|
||||
])->and($important)->toBe(2);
|
||||
});
|
||||
|
||||
it('defines x-cloak once, where both the foundation and material.css reach it', function () {
|
||||
it('defines x-cloak once, reached from both the foundation and all.css', function () {
|
||||
$definitions = collect(File::allFiles(stylesheetPath()))
|
||||
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'css')
|
||||
->filter(fn (SplFileInfo $file): bool => str_contains(stylesheetWithoutComments($file->getContents()), '[x-cloak]'))
|
||||
@@ -269,40 +288,50 @@ it('defines x-cloak once, where both the foundation and material.css reach it',
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$material = array_map(stylesheetName(...), stylesheetTree(stylesheetPath('material.css')));
|
||||
|
||||
expect($definitions)->toBe(['foundation/hidden.css'])
|
||||
->and($material)->toContain('foundation/hidden.css', 'tailwind.css')
|
||||
// Imported after Tailwind, the reset would sit in a layer above Tailwind's utilities.
|
||||
->and(array_values(array_intersect($material, ['foundation.css', 'foundation/reset.css', 'foundation/base.css', 'foundation/interaction.css', 'text.css'])))->toBe([]);
|
||||
->and(foundationStylesheets())->toContain(stylesheetPath('foundation/hidden.css'))
|
||||
->and(allStylesheets())->toContain(stylesheetPath('foundation/hidden.css'));
|
||||
});
|
||||
|
||||
it('writes no Tailwind directive in the foundation', function () {
|
||||
foreach (foundationStylesheets() as $file) {
|
||||
it('writes no Tailwind directive anywhere all.css reaches', function () {
|
||||
foreach (allStylesheets() as $file) {
|
||||
expect(stylesheetWithoutComments(File::get($file)))
|
||||
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/', stylesheetName($file))
|
||||
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/', stylesheetName($file));
|
||||
}
|
||||
});
|
||||
|
||||
it('writes a media query in the foundation only at M3\'s breakpoints, in px', function () {
|
||||
$queries = collect(foundationStylesheets())
|
||||
it('writes a media query, anywhere all.css reaches, only at M3\'s breakpoints, in px', function () {
|
||||
$queries = collect(allStylesheets())
|
||||
->flatMap(function (string $file): array {
|
||||
preg_match_all('/@media\b([^{]*)\{/', stylesheetWithoutComments(File::get($file)), $matches);
|
||||
|
||||
return array_map(fn (string $query): string => stylesheetName($file).': '.trim($query), $matches[1]);
|
||||
});
|
||||
|
||||
// The foundation's own queries (hover, reduced motion) hold no width; the check is for what
|
||||
// lands here later.
|
||||
// Some queries (hover, reduced motion) hold no width at all; the check below is only for the
|
||||
// ones that do.
|
||||
expect($queries)->not->toBeEmpty();
|
||||
|
||||
foreach ($queries as $query) {
|
||||
preg_match_all('/(\d*\.?\d+)([a-z%]*)/i', substr($query, strpos($query, ': ') + 2), $lengths, PREG_SET_ORDER);
|
||||
$feature = substr($query, strpos($query, ': ') + 2);
|
||||
|
||||
// The one named exception (docs/reference/m3/components-navigation-selection-inputs.md §
|
||||
// Time pickers/Behaviour, and tests/Feature/Components/InputStylesheetsTest.php's
|
||||
// assertBreakpointsInPx()): a viewport *height* in an `orientation` query is not a
|
||||
// breakpoint, since M3 does not make one of it. A width, in any query, still is.
|
||||
$orientationHeight = preg_match('/\(\s*orientation\s*:/', $feature) === 1
|
||||
&& preg_match('/(?:^|[\s:<>=(])(?:min-|max-)?height\b/', $feature) === 1
|
||||
&& ! str_contains($feature, 'width');
|
||||
|
||||
preg_match_all('/(\d*\.?\d+)([a-z%]*)/i', $feature, $lengths, PREG_SET_ORDER);
|
||||
|
||||
foreach ($lengths as [, $number, $unit]) {
|
||||
expect($unit)->toBe('px', $query)
|
||||
->and(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$query} is not at an M3 breakpoint");
|
||||
expect($unit)->toBe('px', $query);
|
||||
|
||||
if (! $orientationHeight) {
|
||||
expect(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$query} is not at an M3 breakpoint");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -329,31 +358,77 @@ it('declares M3\'s spacing scale as the reference gives it', function () {
|
||||
});
|
||||
|
||||
/**
|
||||
* The layout components' stylesheets: layout.css and everything it imports.
|
||||
* The layout components' stylesheets: all.css's own `./layout/*.css` imports, each with
|
||||
* everything it in turn imports (which reaches into `components/`, for the layout files that draw
|
||||
* with a component, e.g. pane.css's app bar and button).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function layoutStylesheets(): array
|
||||
{
|
||||
return stylesheetTree(stylesheetPath('layout.css'));
|
||||
$files = [];
|
||||
|
||||
foreach (stylesheetImports(stylesheetPath('all.css')) as $import) {
|
||||
if (! str_starts_with($import, './layout/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (stylesheetTree(stylesheetPath($import)) as $file) {
|
||||
if (! in_array($file, $files, true)) {
|
||||
$files[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
it('imports a stylesheet for every layout component from layout.css, each beside its view', function () {
|
||||
$imports = stylesheetImports(stylesheetPath('layout.css'));
|
||||
it('imports every stylesheet under components/ and layout/ directly, each exactly once', function () {
|
||||
$imports = stylesheetImports(stylesheetPath('all.css'));
|
||||
|
||||
expect(count($imports))->toBe(count(array_unique($imports)), 'all.css imports something more than once');
|
||||
|
||||
$expected = collect(File::files(stylesheetPath('components')))
|
||||
->map(fn (SplFileInfo $file): string => "./components/{$file->getFilename()}")
|
||||
->merge(collect(File::files(stylesheetPath('layout')))->map(fn (SplFileInfo $file): string => "./layout/{$file->getFilename()}"))
|
||||
->sort()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
// Every file under both directories is imported directly — a shared one (navigation-item.css,
|
||||
// selection.css, menu.css included) as much as a component's own — and nothing stale remains.
|
||||
$actual = collect($imports)
|
||||
->filter(fn (string $import): bool => str_starts_with($import, './components/') || str_starts_with($import, './layout/'))
|
||||
->sort()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
expect($actual)->toBe($expected);
|
||||
});
|
||||
|
||||
it('imports a stylesheet for every layout component from all.css, each beside its view', function () {
|
||||
$imports = array_values(array_filter(
|
||||
stylesheetImports(stylesheetPath('all.css')),
|
||||
fn (string $import): bool => str_starts_with($import, './layout/'),
|
||||
));
|
||||
$reached = array_map(stylesheetName(...), layoutStylesheets());
|
||||
|
||||
$files = collect(File::files(stylesheetPath('layout')))->map(fn (SplFileInfo $file): string => $file->getBasename('.css'));
|
||||
|
||||
// Two files are shared by the components rather than being one: the spacing props and the
|
||||
// visibility props.
|
||||
// visibility props. Both are still imported directly by all.css, like every other layout file
|
||||
// — only the "has its own component" half of the check below skips them.
|
||||
$components = $files->diff(['spacing', 'visibility'])->values();
|
||||
|
||||
expect($components)->not->toBeEmpty()
|
||||
->and($files->map(fn (string $name): string => "layout/{$name}.css")->diff($reached)->values()->all())->toBe([]);
|
||||
|
||||
foreach ($files as $name) {
|
||||
expect(in_array("./layout/{$name}.css", $imports, true))->toBeTrue("layout/{$name}.css is not imported directly by all.css");
|
||||
}
|
||||
|
||||
foreach ($components as $name) {
|
||||
expect(File::exists(__DIR__."/../../resources/views/components/{$name}.blade.php"))->toBeTrue("layout/{$name}.css has no component")
|
||||
->and($imports)->toContain("./layout/{$name}.css");
|
||||
expect(File::exists(__DIR__."/../../resources/views/components/{$name}.blade.php"))->toBeTrue("layout/{$name}.css has no component");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -389,28 +464,10 @@ it('imports, from every layout stylesheet, the stylesheet of each component its
|
||||
expect($checked)->toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('opens every layout stylesheet with a header, the layer statement and plain imports', function () {
|
||||
foreach (layoutStylesheets() as $file) {
|
||||
$name = stylesheetName($file);
|
||||
$items = stylesheetItems(File::get($file));
|
||||
|
||||
expect(File::get($file))->toStartWith('/*', "{$name} has no header comment")
|
||||
->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, "{$name} does not open with the layer statement");
|
||||
|
||||
$blocks = false;
|
||||
|
||||
foreach (array_slice($items, 1) as $item) {
|
||||
if (isset($item['prelude'])) {
|
||||
$blocks = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
expect($blocks)->toBeFalse("{$name}: `{$item['statement']}` comes after a block")
|
||||
->and($item['statement'])->toMatch('/^@import ([\'"])\.{1,2}\/[\w.\/-]+\.css\1;$/', "{$name}: `{$item['statement']}` is not a plain import");
|
||||
}
|
||||
}
|
||||
});
|
||||
// Every layout stylesheet's header, layer statement and plain-imports shape, and its freedom from
|
||||
// Tailwind and from a media query off an M3 breakpoint, are covered by the two all.css-wide tests
|
||||
// above (allStylesheets() reaches every layout file too) — a dedicated layout-only version of
|
||||
// either would only repeat that same check on a subset already checked.
|
||||
|
||||
it('keeps every layout rule in material.layout, and the visibility props in material.visibility', function () {
|
||||
// layoutStylesheets() follows every @import, so it now reaches component stylesheets a layout
|
||||
@@ -442,36 +499,6 @@ it('keeps every layout rule in material.layout, and the visibility props in mate
|
||||
}
|
||||
});
|
||||
|
||||
it('writes no Tailwind in the layout stylesheets, and media queries only at M3\'s breakpoints, in px', function () {
|
||||
$queries = 0;
|
||||
|
||||
// Component stylesheets reached through a layout file (see the test above) are not layout
|
||||
// stylesheets themselves, and keep their own group's rules (a hover query included).
|
||||
foreach (layoutStylesheets() as $file) {
|
||||
$name = stylesheetName($file);
|
||||
|
||||
if (! str_starts_with($name, 'layout/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$css = stylesheetWithoutComments(File::get($file));
|
||||
|
||||
expect($css)
|
||||
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/', $name)
|
||||
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/', $name);
|
||||
|
||||
preg_match_all('/@media\b([^{]*)\{/', $css, $matches);
|
||||
|
||||
foreach ($matches[1] as $query) {
|
||||
$queries++;
|
||||
|
||||
expect(trim($query))->toMatch('/^\(width (?:<|>=) (?:600|840|1200|1600)px\)$/', "{$name}: `@media {$query}` is not an M3 breakpoint in px");
|
||||
}
|
||||
}
|
||||
|
||||
expect($queries)->toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('gives every spacing token a gap and a padding, and the layout components read only those', function () {
|
||||
$tokens = array_map(
|
||||
fn (string $property): string => substr($property, strlen('--md-sys-measurement-')),
|
||||
@@ -610,13 +637,16 @@ it('declares the material layers in the Workbench between Tailwind\'s preflight
|
||||
// otherwise beat a rewritten component's padding, and below the utilities.
|
||||
expect(trim($app))->toStartWith('@layer theme, base, material, components, utilities;');
|
||||
|
||||
$foundation = strpos($app, "@import '../../../resources/css/foundation.css';");
|
||||
$all = strpos($app, "@import '../../../resources/css/all.css';");
|
||||
$tailwind = strpos($app, "@import 'tailwindcss'");
|
||||
$components = strpos($app, "@import '../../../resources/css/tailwind.css';");
|
||||
|
||||
expect($foundation)->toBeInt()
|
||||
->and($tailwind)->toBeGreaterThan($foundation)
|
||||
expect($all)->toBeInt()
|
||||
->and($tailwind)->toBeGreaterThan($all)
|
||||
->and($components)->toBeGreaterThan($tailwind)
|
||||
// material.css would declare the tokens a second time.
|
||||
->and($app)->not->toContain('material.css');
|
||||
// material.css declared the tokens a second time; components.css and layout.css are the
|
||||
// interim lists all.css replaced.
|
||||
->and($app)->not->toContain('material.css')
|
||||
->not->toContain("resources/css/components.css'")
|
||||
->not->toContain("resources/css/layout.css'");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user