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
75 lines
2.9 KiB
PHP
75 lines
2.9 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
/**
|
|
* The layout components at M3's breakpoints (plan step 35): each on a page of its own, measured at
|
|
* the pixel either side of the breakpoint it changes at. The window is resized after the page has
|
|
* loaded, which is what a person does; a media query answers at once, and a script listening to
|
|
* one (resources/js/layout.js) hears a change event.
|
|
*/
|
|
function layoutPage(string $body, int $width, int $height = 900, string $dir = 'ltr'): mixed
|
|
{
|
|
// Until the Workbench leaves Tailwind, its build turns `:dir(rtl)` into a list of `:lang()`s, so a
|
|
// right-to-left page also says which language it is in.
|
|
$lang = $dir === 'rtl' ? 'ar' : 'en';
|
|
$path = '/layout-probe/'.md5($body.$dir);
|
|
|
|
Route::middleware('web')->get($path, fn () => Blade::render(<<<BLADE
|
|
<!DOCTYPE html>
|
|
<html dir="{$dir}" lang="{$lang}">
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<x-theme-script />
|
|
@vite(config('livewire-material.showcase.vite'))
|
|
@livewireStyles
|
|
</head>
|
|
<body>
|
|
{$body}
|
|
@livewireScripts
|
|
</body>
|
|
</html>
|
|
BLADE));
|
|
|
|
return layoutReady(visit($path)->resize($width, $height));
|
|
}
|
|
|
|
function layoutReady(mixed $page): mixed
|
|
{
|
|
return $page->waitForEvent('networkidle')
|
|
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
|
|
}
|
|
|
|
/** A computed style of the element matching the selector. */
|
|
function layoutStyle(string $selector, string $property): string
|
|
{
|
|
return "getComputedStyle(document.querySelector('{$selector}')).{$property}";
|
|
}
|
|
|
|
/** A side of the element's box, rounded to the pixel. */
|
|
function layoutRect(string $selector, string $side): string
|
|
{
|
|
return "Math.round(document.querySelector('{$selector}').getBoundingClientRect().{$side})";
|
|
}
|
|
|
|
/** How many column tracks a grid has. */
|
|
function layoutColumns(string $selector): string
|
|
{
|
|
return layoutStyle($selector, 'gridTemplateColumns').".split(' ').length";
|
|
}
|
|
|
|
it('spaces a stack with its token, never with its parent\'s, and hides it from a breakpoint over its own display', function () {
|
|
$body = '<x-stack id="outer" gap="space300"><x-stack id="inner"><p>One</p><p>Two</p></x-stack><p>Three</p></x-stack>'
|
|
.'<x-stack id="hidden-from" hide-from="medium"><p>Compact only</p></x-stack>';
|
|
|
|
layoutPage($body, 599)
|
|
->assertScript(layoutStyle('#outer', 'flexDirection')." === 'column'")
|
|
->assertScript(layoutStyle('#outer', 'rowGap')." === '24px'")
|
|
->assertScript(layoutStyle('#inner', 'rowGap')." === '0px'")
|
|
->assertScript(layoutStyle('#hidden-from', 'display')." === 'flex'");
|
|
|
|
layoutPage($body, 600)
|
|
->assertScript(layoutStyle('#hidden-from', 'display')." === 'none'");
|
|
});
|