Add the stack layout component and the layout foundation
Plan step 35: <x-stack gap align>, the first of M3's layout components, with what they all share - src/Support/Layout.php reading the props into data-md-* attributes, the spacing-token gap and padding rules, the hide-below/hide-from rules in material.visibility, the stylesheet checks for resources/css/layout, the browser test file and the skill's Layout group. The Workbench puts the material layers above Tailwind's preflight, which would otherwise zero every layout padding and margin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
6be9f35c98
commit
b677e60876
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use NoNameWeb\LivewireMaterial\Support\Layout;
|
||||
|
||||
/**
|
||||
* The package's stylesheets are plain CSS in `material.*` layers, with no Tailwind in them. Every
|
||||
@@ -327,6 +328,148 @@ it('declares M3\'s spacing scale as the reference gives it', function () {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The layout components' stylesheets: layout.css and everything it imports.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function layoutStylesheets(): array
|
||||
{
|
||||
return stylesheetTree(stylesheetPath('layout.css'));
|
||||
}
|
||||
|
||||
it('imports a stylesheet for every layout component from layout.css, each beside its view', function () {
|
||||
$imports = stylesheetImports(stylesheetPath('layout.css'));
|
||||
$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.
|
||||
$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 ($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");
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps every layout rule in material.layout, and the visibility props in material.visibility', function () {
|
||||
foreach (layoutStylesheets() as $file) {
|
||||
$name = stylesheetName($file);
|
||||
$css = File::get($file);
|
||||
$layer = match ($name) {
|
||||
'layout/visibility.css' => 'material.visibility',
|
||||
default => 'material.layout',
|
||||
};
|
||||
|
||||
expect(stylesheetWithoutComments($css))->not->toContain('!important', $name);
|
||||
|
||||
foreach (stylesheetItems($css) as $item) {
|
||||
if (! isset($item['prelude'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
expect($item['prelude'])->toBe("@layer {$layer}", "{$name} writes outside `@layer {$layer}`")
|
||||
->and($item['body'])->not->toContain('@layer');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('writes no Tailwind in the layout stylesheets, and media queries only at M3\'s breakpoints, in px', function () {
|
||||
$queries = 0;
|
||||
|
||||
foreach (layoutStylesheets() as $file) {
|
||||
$name = stylesheetName($file);
|
||||
$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-')),
|
||||
array_keys(stylesheetDeclarations(stylesheetBlock(File::get(stylesheetPath('tokens/spacing.css')), ':root'))),
|
||||
);
|
||||
|
||||
expect(Layout::SPACING)->toBe($tokens);
|
||||
|
||||
$spacing = File::get(stylesheetPath('layout/spacing.css'));
|
||||
|
||||
expect(stylesheetDeclarations(stylesheetBlock($spacing, "[data-md-gap='none']")))->toBe(['--md-gap' => '0px']);
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
expect(stylesheetDeclarations(stylesheetBlock($spacing, "[data-md-gap='{$token}']")))->toBe(['--md-gap' => "var(--md-sys-measurement-{$token})"])
|
||||
->and(stylesheetDeclarations(stylesheetBlock($spacing, "[data-md-padding='{$token}']")))->toBe(['padding' => "var(--md-sys-measurement-{$token})"]);
|
||||
}
|
||||
|
||||
expect(collect(stylesheetItems(stylesheetBlock($spacing, '@layer material.layout')))->pluck('prelude')->all())
|
||||
->toHaveCount(1 + 2 * count($tokens));
|
||||
});
|
||||
|
||||
it('hides an element below and from every breakpoint but compact, and nothing else', function () {
|
||||
$visibility = stylesheetBlock(File::get(stylesheetPath('layout/visibility.css')), '@layer material.visibility');
|
||||
|
||||
$expected = [];
|
||||
|
||||
foreach (['medium' => 600, 'expanded' => 840, 'large' => 1200, 'extra-large' => 1600] as $breakpoint => $width) {
|
||||
$expected["@media (width < {$width}px)"] = "[data-md-hide-below='{$breakpoint}']";
|
||||
}
|
||||
|
||||
foreach (['medium' => 600, 'expanded' => 840, 'large' => 1200, 'extra-large' => 1600] as $breakpoint => $width) {
|
||||
$expected["@media (width >= {$width}px)"] = "[data-md-hide-from='{$breakpoint}']";
|
||||
}
|
||||
|
||||
$actual = collect(stylesheetItems($visibility))->mapWithKeys(function (array $item): array {
|
||||
$rule = stylesheetItems($item['body'])[0];
|
||||
|
||||
expect(stylesheetDeclarations($rule['body']))->toBe(['display' => 'none']);
|
||||
|
||||
return [$item['prelude'] => $rule['prelude']];
|
||||
})->all();
|
||||
|
||||
expect($actual)->toBe($expected);
|
||||
});
|
||||
|
||||
it('gives every md-type class the declarations of its type utility', function () {
|
||||
preg_match_all('/@utility (type-[\w-]+) \{\n(.*?)\n\}/s', File::get(stylesheetPath('tokens/utilities.css')), $matches, PREG_SET_ORDER);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user