Take Tailwind out of the package, and its detection out of the guard
tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
Tailwind left the stack in 2.0.0, but the package still carried about 330 mentions of it. What the guard's Tailwind detection protected — a class that compiles to nothing — is now protected by a check that does not care where a dead class came from. DesignGuard: about 500 lines of Tailwind tables, scales, palettes and "2.0.0 replacement" hints give way to one check — a class a view or PHP file writes that neither the application's stylesheets nor the package's own declare. It catches a utility of any framework, a typo and a class whose rules were deleted alike, so it also found two classes ReStride draws nothing with. A stylesheet has to be in reach for it: the `.css` files among the scanned paths, or what the `missingStylesheets()` entry imports. The class reader no longer mistakes an array index for a class list (`$block['base']`), and it reads the array a class helper is given, where it read nothing before. The package's own three Tailwind self-guards go with it. Only their one unique check stays, as a test of its own: every `matchMedia` width in resources/js is an M3 breakpoint. The pagination views are `material.blade.php` and `simple-material.blade.php`; only Laravel's and Livewire's default theme names ever made them `tailwind`. The provider sets `Paginator`'s default views and switches `livewire.pagination_theme` to `material` when it is still Livewire's own default, so no application can forget the config; a theme an application chose, and a component's own `$paginationTheme` or `paginationView()`, still win. The rest is prose: the layer-order guidance for an application that still builds Tailwind, the Tailwind wording in the README, the Boost guidelines and the development skill, and about 25 "this used to be a Tailwind utility" comments, along with every "plan step NN" pointer into a gitignored folder. The reset keeps its credit, and NOTICE now carries it too. Feature suite 1159 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
247c596c3a
commit
fb7007c976
@@ -687,11 +687,9 @@ it('toggles each choice of a multi selection group, down to none', function () {
|
||||
});
|
||||
|
||||
/**
|
||||
* Browser tests owed from docs/plans/material-3-browser-tests.md § Actions (Phase E), written on
|
||||
* the data-md-* hooks of plan step 36's actions stream: submenu keyboard and hover-open, the
|
||||
* filtering menu, the sheet-at-compact list, a long menu's scroll, and the FAB menu's own scroll
|
||||
* behind its fixed close button. Written but not run — the actions group's Chromium run happens
|
||||
* once every stream in the group has landed.
|
||||
* The tests below: a submenu opened and closed by keyboard or hover, a menu's filter field, its
|
||||
* sheet-at-compact form below 600px, a long menu's scroll and the FAB menu's own scroll behind
|
||||
* its fixed close button. `LongMenuProbe` and `SheetMenuProbe` back the last three of those.
|
||||
*/
|
||||
|
||||
/** A Livewire component whose menu has more items than the popover's 288px cap can show. */
|
||||
@@ -872,10 +870,10 @@ it('filters a menu\'s items as its field is typed into, moves the highlight with
|
||||
|
||||
it('opens a sheet-at-compact menu below 600px, focused on its first item, and the popover from 600px', function () {
|
||||
$trigger = '#menus button:has-text("Photo")';
|
||||
// The sheet is <x-bottom-sheet>, rewritten (plan step 36) but still x-show-driven — its CSS
|
||||
// now animates open/close through @starting-style and transition-behavior: allow-discrete
|
||||
// rather than Alpine's x-transition classes, but x-show still owns the element's display, so
|
||||
// its openness shows as visibility, not the Popover API's :popover-open.
|
||||
// The sheet is <x-bottom-sheet>, still x-show-driven — its CSS now animates open/close
|
||||
// through @starting-style and transition-behavior: allow-discrete rather than Alpine's
|
||||
// x-transition classes, but x-show still owns the element's display, so its openness shows
|
||||
// as visibility, not the Popover API's :popover-open.
|
||||
$dialog = '[role="dialog"][aria-label="Photo actions"]';
|
||||
$addToAlbum = '#menus [role="menuitem"]:has-text("Add to album")';
|
||||
|
||||
|
||||
@@ -187,9 +187,9 @@ it('scrolls instantly and leaves every item unmasked under reduced motion', func
|
||||
});
|
||||
|
||||
it('brings an item cut off at the row\'s edge into view when it is pressed under reduced motion', function () {
|
||||
// Owed by the Chromium baseline (step 32, docs/plans/material-3-browser-tests.md): reduced
|
||||
// motion writes a zero inset for every item, so a mask cannot say an item is only partly
|
||||
// shown; carousel.js reads the item's box against the row's instead, as for multi-aspect.
|
||||
// Under reduced motion, the row writes a zero inset for every item, so a mask cannot say an
|
||||
// item is only partly shown; carousel.js reads the item's box against the row's instead, as
|
||||
// for multi-aspect.
|
||||
$page = carouselShowcase(['reducedMotion' => 'reduce'])
|
||||
->assertScript(onCarousel(0, <<<'JS'
|
||||
const row = scroller.getBoundingClientRect()
|
||||
|
||||
@@ -7,13 +7,13 @@ use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* The central claims of the `@layer material` design (plan step 42, docs/plans/material-3-alignment.md
|
||||
* Decisions § "Components carry their styles as `data-md-*` attributes inside `@layer material`"),
|
||||
* each on its own small probe page: an application's unlayered rule beats a component default;
|
||||
* `[hidden]` beats a component's `display`; the layer order holds even when a component file is
|
||||
* imported before the foundation; a `data-md-*` attribute survives a Livewire `$refresh`; an icon
|
||||
* mirrors in a right-to-left document. Every other stylesheet claim (layers, imports, breakpoints,
|
||||
* no Tailwind) is StylesheetsTest.php's, asserted on the source rather than a rendered page.
|
||||
* The central claims of the `@layer material` design — components carry their styles as
|
||||
* `data-md-*` attributes inside `@layer material` — each on its own small probe page: an
|
||||
* application's unlayered rule beats a component default; `[hidden]` beats a component's
|
||||
* `display`; the layer order holds even when a component file is imported before the foundation;
|
||||
* a `data-md-*` attribute survives a Livewire `$refresh`; an icon mirrors in a right-to-left
|
||||
* document. Every other stylesheet claim (layers, imports, breakpoints) is StylesheetsTest.php's,
|
||||
* asserted on the source rather than a rendered page.
|
||||
*/
|
||||
function cascadeReady(mixed $page): mixed
|
||||
{
|
||||
@@ -71,8 +71,8 @@ it('lets [hidden] beat a component\'s display', function () {
|
||||
});
|
||||
|
||||
it('keeps the layer order even when a component file is imported before the foundation', function () {
|
||||
// Every package stylesheet opens with the same full `@layer` statement (plan step 33), so the
|
||||
// order it establishes holds whichever file a page reaches first — the error layout's copy of
|
||||
// Every package stylesheet opens with the same full `@layer` statement, so the order it
|
||||
// establishes holds whichever file a page reaches first — the error layout's copy of
|
||||
// it, reached here before the foundation's. The two prebuilt error-page stylesheets, in that
|
||||
// deliberate order: the layout's bundle, whose button.css rules sit in `material.components`,
|
||||
// physically precedes the fallback's foundation reset, in `material.reset` — and still loses
|
||||
|
||||
@@ -1077,9 +1077,8 @@ it('slides the side sheet in from its own edge in a right-to-left page', functio
|
||||
|
||||
// `side="end"` (the default) is the trailing edge — the *left* of a right-to-left page — so it
|
||||
// slides in from off the left, not the right (the CSS mirror reads `[dir='rtl']` rather than
|
||||
// `:dir(rtl)`, plan step 39 — Vite's own minifier rewrote that to a `:lang()` list, same as
|
||||
// Tailwind's build did): caught well before the 500ms spatial transition ends, it is still
|
||||
// off-screen on the left.
|
||||
// `:dir(rtl)`, because Vite's own minifier rewrites that to a `:lang()` list): caught well
|
||||
// before the 500ms spatial transition ends, it is still off-screen on the left.
|
||||
$page->wait(0.05);
|
||||
|
||||
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
||||
|
||||
@@ -6,10 +6,10 @@ use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The layout components at M3's breakpoints: 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
|
||||
{
|
||||
|
||||
@@ -350,7 +350,7 @@ it('lifts the snackbar and the page above something docked on the phone\'s bar',
|
||||
* A page for the navigation bar and rail props the shell's own adaptive rail never exercises:
|
||||
* `tall`, `hide-on-scroll` (with a pinned snackbar), a narrow centred rail, and a rail that hides
|
||||
* when collapsed. A wide, fixed viewport throughout, so none of M3's breakpoints interfere with
|
||||
* what each example's own props already decide (docs/plans/material-3-browser-tests.md § Navigation).
|
||||
* what each example's own props already decide.
|
||||
*/
|
||||
function navigationExtrasProbe(): mixed
|
||||
{
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||
|
||||
/**
|
||||
* The texts Boost copies into an application: the two guidelines every session reads, and the two
|
||||
* skills. Each teaches the vocabulary an agent then writes, so none may name a class or variant
|
||||
* the package's stylesheets do not define.
|
||||
* skills. A fenced Blade, HTML, PHP or JS block goes through the design guard whole, so none may
|
||||
* write a class no stylesheet declares; in prose, only an inline span that reads as an `md-*`
|
||||
* class list is checked (`boostSpanIsClassList()`), so a name from another vocabulary — a prop, a
|
||||
* role, a config key — is never held to that standard.
|
||||
*/
|
||||
const BOOST_TEXTS = [
|
||||
'resources/boost/guidelines/core.blade.php',
|
||||
@@ -16,16 +17,10 @@ const BOOST_TEXTS = [
|
||||
'resources/boost/skills/material-3-design/SKILL.md',
|
||||
];
|
||||
|
||||
/**
|
||||
* The one section that names Tailwind's utilities on purpose: the design guard's table of what it
|
||||
* reports, each beside its replacement.
|
||||
*/
|
||||
const BOOST_SECTIONS_NAMING_WHAT_FAILS = ['Testing the design'];
|
||||
|
||||
/**
|
||||
* A text's code, line for line, as the design guard reads an application: fenced Blade, HTML, PHP
|
||||
* and JS into a view, fenced CSS into a stylesheet, and every inline span that reads as a class
|
||||
* list (`bg-primary`, `medium:hidden`, `md-ink-variant`) as a `class` attribute on its own line.
|
||||
* and JS into a view, fenced CSS and an inline rule into a stylesheet, and every inline span that
|
||||
* reads as a class list (`md-ink-variant md-truncate`) as a `class` attribute on its own line.
|
||||
* Prose, props, attributes and custom properties are left out.
|
||||
*
|
||||
* @return array{view: string, css: string}
|
||||
@@ -35,7 +30,6 @@ function boostTextAsCode(string $text): array
|
||||
$view = [];
|
||||
$css = [];
|
||||
$fence = null;
|
||||
$skipping = false;
|
||||
|
||||
foreach (explode("\n", $text) as $line) {
|
||||
$viewLine = '';
|
||||
@@ -44,28 +38,28 @@ function boostTextAsCode(string $text): array
|
||||
if (preg_match('/^```\s*([a-z]*)/', $line, $open) === 1) {
|
||||
$fence = $fence === null ? ($open[1] ?: 'text') : null;
|
||||
} elseif ($fence !== null) {
|
||||
if (! $skipping && $fence === 'css') {
|
||||
if ($fence === 'css') {
|
||||
$cssLine = $line;
|
||||
} elseif (! $skipping && in_array($fence, ['blade', 'html', 'php', 'js'], true)) {
|
||||
} elseif (in_array($fence, ['blade', 'html', 'php', 'js'], true)) {
|
||||
$viewLine = $line;
|
||||
}
|
||||
} else {
|
||||
if (preg_match('/^## (.+)$/', $line, $heading) === 1) {
|
||||
$skipping = in_array($heading[1], BOOST_SECTIONS_NAMING_WHAT_FAILS, true);
|
||||
}
|
||||
preg_match_all('/`([^`]+)`/', $line, $spans);
|
||||
|
||||
if (! $skipping) {
|
||||
preg_match_all('/`([^`]+)`/', $line, $spans);
|
||||
// A span that is a rule of its own (`.sport-run-label { … }`) declares the class
|
||||
// the example beside it writes, so it goes on the stylesheet side of the pair.
|
||||
[$rules, $spans] = collect($spans[1])->partition(fn (string $span): bool => preg_match('/^\s*\.[\w-]+[^{]*\{[^}]*\}\s*$/', $span) === 1);
|
||||
|
||||
$viewLine = collect($spans[1])
|
||||
->map(fn (string $span): ?string => match (true) {
|
||||
str_contains($span, '<') => $span,
|
||||
boostSpanIsClassList($span) => '<div class="'.$span.'"></div>',
|
||||
default => null,
|
||||
})
|
||||
->filter()
|
||||
->implode(' ');
|
||||
}
|
||||
$cssLine = $rules->implode(' ');
|
||||
|
||||
$viewLine = $spans
|
||||
->map(fn (string $span): ?string => match (true) {
|
||||
str_contains($span, '<') => $span,
|
||||
boostSpanIsClassList($span) => '<div class="'.$span.'"></div>',
|
||||
default => null,
|
||||
})
|
||||
->filter()
|
||||
->implode(' ');
|
||||
}
|
||||
|
||||
$view[] = $viewLine;
|
||||
@@ -76,70 +70,22 @@ function boostTextAsCode(string $text): array
|
||||
}
|
||||
|
||||
/**
|
||||
* The hyphenated names the texts also write alone in a span for something that is not a class:
|
||||
* every property the package's stylesheets declare (`border-color`), every component prop
|
||||
* (`placeholder-value`) and every colour role (`outline-variant`). Each is shaped like a colour
|
||||
* utility on a name the guard does not know, which is what an application's own theme colour
|
||||
* (`bg-brand`) looks like.
|
||||
*
|
||||
* @return array<string, true>
|
||||
*/
|
||||
function boostNamesThatAreNotClasses(): array
|
||||
{
|
||||
static $names = null;
|
||||
|
||||
if ($names !== null) {
|
||||
return $names;
|
||||
}
|
||||
|
||||
$names = [];
|
||||
$root = __DIR__.'/../../resources';
|
||||
|
||||
foreach (File::allFiles("{$root}/css") as $file) {
|
||||
preg_match_all('/(?<![\w-])([a-z][a-z-]*-[a-z-]+)\s*:\s*[^;{}]*[;}]|--md-sys-color-([a-z-]+)/', $file->getContents(), $matches);
|
||||
|
||||
foreach ([...$matches[1], ...$matches[2]] as $name) {
|
||||
$names[$name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (File::files("{$root}/views/components") as $file) {
|
||||
if (preg_match('/@props\(\[(.*?)\]\)/s', $file->getContents(), $props) === 1) {
|
||||
preg_match_all("/^\s*'(\w+)'/m", $props[1], $keys);
|
||||
|
||||
foreach ($keys[1] as $key) {
|
||||
$names[Str::kebab($key)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an inline span reads as a class list. Not: a CSS declaration (`font: var(…)`), a
|
||||
* command (`php artisan …`), a single bare word, which is as often a role, prop or value
|
||||
* (`outline`, `link`, `hidden`) as a utility, or a single property, prop or role name
|
||||
* (`border-color`, `placeholder-value`, `outline-variant`).
|
||||
* Whether an inline span reads as a class list: every token in it, and there has to be at least
|
||||
* one, has to be an `md-*` class — the only vocabulary this check holds prose to. A single bare
|
||||
* word (`outline`, `link`, `hidden`), a property or prop name (`border-color`,
|
||||
* `placeholder-value`), a colour role (`outline-variant`), a CSS function (`var(…)`) and a shell
|
||||
* command (`php artisan …`) all fail that test on their own, with no list of names to keep here.
|
||||
*/
|
||||
function boostSpanIsClassList(string $span): bool
|
||||
{
|
||||
$tokens = preg_split('/\s+/', trim($span));
|
||||
|
||||
if (count($tokens) === 1 && (preg_match('/[-:]/', $tokens[0]) !== 1 || isset(boostNamesThatAreNotClasses()[$tokens[0]]))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:php|npm|npx|composer|git)\b|\b(?:var|calc|color-mix)\(|^[a-z-]+:\s/', $span) === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (preg_match('/^(?:--|data-|aria-|wire:|x-|@|\$)/', $token) === 1 || str_contains($token, '::')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/^[a-z0-9][a-z0-9:\/.\[\]()#%_-]*$/', $token) !== 1) {
|
||||
// Every class the package declares is `md-*`, so that prefix is what tells a class list
|
||||
// from the prop values, slot names and config keys the prose lists the same way
|
||||
// (`top-start`, `rail-header`, `livewire-material.profiles`). A class of an application's
|
||||
// own that an example names is nobody's to declare here.
|
||||
if (preg_match('/^(?:[a-z][\w-]*:)*md-[a-z\d-]+$/', $token) !== 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Breakpoints are M3's window size classes and only those: compact below `medium`, then medium 600,
|
||||
* expanded 840, large 1200 and extra-large 1600 (docs/reference/m3/foundations.md § Layout,
|
||||
* foundations-supplement.md § Breakpoints). Tailwind is gone from the whole stack (plan step 39),
|
||||
* so a leftover `sm:` class or a media query at one of its screen widths no longer compiles to
|
||||
* nothing — a browser just as silently ignores the invalid class or the wrong-width rule, so the
|
||||
* mistake is exactly as easy to miss. These two scans are the guard: one for a Tailwind-shaped
|
||||
* prefix (a habit, or an example pasted from elsewhere), one for the media queries a stylesheet or
|
||||
* a script writes by hand.
|
||||
*
|
||||
* @return list<SplFileInfo>
|
||||
*/
|
||||
function breakpointSources(): array
|
||||
{
|
||||
return collect(['views', 'css', 'js'])
|
||||
->flatMap(fn (string $directory): array => File::allFiles(__DIR__.'/../../resources/'.$directory))
|
||||
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'css', 'js'], true))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Every line of a file that matches, as `path:line: the line`.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function breakpointOffenders(SplFileInfo $file, string $pattern): array
|
||||
{
|
||||
$lines = preg_split('/\R/', $file->getContents());
|
||||
|
||||
return collect($lines)
|
||||
->filter(fn (string $line): bool => preg_match($pattern, $line) === 1)
|
||||
->map(fn (string $line, int $index): string => $file->getRelativePathname().':'.($index + 1).': '.trim($line))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
it('writes no Tailwind breakpoint prefix in any view, stylesheet or script', function () {
|
||||
// A prefix, not a name: `--radius-corner-sm:` and slider.js's `sm: { corner: 8 }` are excluded
|
||||
// by the character before and the one after, and `@md:` is a container query, a different
|
||||
// thing — a component sized by the room it has, not by the window.
|
||||
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
|
||||
|
||||
$offenders = collect(breakpointSources())
|
||||
->flatMap(fn (SplFileInfo $file): array => breakpointOffenders($file, $pattern))
|
||||
->values();
|
||||
|
||||
expect($offenders->all())->toBe([]);
|
||||
});
|
||||
|
||||
it('writes no media query at a Tailwind screen width', function () {
|
||||
// 40rem/640px, 48rem/768px, 64rem/1024px, 80rem/1280px and the 39.99rem that stood for "below
|
||||
// 640" — none of them is an M3 boundary. The M3 ones are 37.5, 52.5, 75 and 100rem, and a
|
||||
// script asks resources/js/breakpoints.js for them instead of writing its own string.
|
||||
$widths = '/(?<![\d.])(?:40|48|64|80|39\.99)rem|(?<![\d.])(?:640|768|1024|1280)px/';
|
||||
|
||||
$offenders = collect(breakpointSources())
|
||||
->flatMap(function (SplFileInfo $file) use ($widths): array {
|
||||
// Only the condition of a query, so `max-width: min(40rem, 70dvh)` — a real width, not
|
||||
// a breakpoint — and a 640px cap on a bottom sheet are left alone.
|
||||
preg_match_all('/@(?:media|container)\b[^{]*|matchMedia\(\s*[\'"`][^\'"`]*/', $file->getContents(), $matches);
|
||||
|
||||
return collect($matches[0])
|
||||
->filter(fn (string $condition): bool => preg_match($widths, $condition) === 1)
|
||||
->map(fn (string $condition): string => $file->getRelativePathname().': '.trim($condition))
|
||||
->values()
|
||||
->all();
|
||||
})
|
||||
->values();
|
||||
|
||||
expect($offenders->all())->toBe([]);
|
||||
});
|
||||
|
||||
it('scans the showcase example heredocs too, where the package teaches its own markup', function () {
|
||||
$sections = collect(breakpointSources())
|
||||
->filter(fn (SplFileInfo $file): bool => str_contains($file->getRelativePathname(), 'showcase'.DIRECTORY_SEPARATOR.'sections'))
|
||||
->filter(fn (SplFileInfo $file): bool => str_contains($file->getContents(), "<<<'BLADE'"));
|
||||
|
||||
// Every section still teaches its examples through a heredoc nested inside an @php block
|
||||
// (plan step 38 rewrote what is inside them, not the mechanism), so the two scans above —
|
||||
// which read a file's raw text rather than its parsed Blade or PHP — still reach them.
|
||||
expect($sections)->not->toBeEmpty();
|
||||
|
||||
// The premise itself, proven on a fixture now that plan step 38 has cleared every Tailwind
|
||||
// breakpoint prefix out of the showcase (its own goal, so production content can no longer
|
||||
// carry one): a real file, shaped exactly like a showcase section (an outer heredoc nesting an
|
||||
// inner one, `@php … <<<'BLADE' … BLADE, … @endphp`), still gives up a prefix hiding two
|
||||
// heredocs deep to the same pattern and helper the first test above uses.
|
||||
$directory = sys_get_temp_dir().'/breakpoint-heredoc-probe-'.uniqid();
|
||||
File::ensureDirectoryExists($directory);
|
||||
|
||||
// Built from parts, not written whole: the design guard's own "no Tailwind utility class in
|
||||
// resources/ or tests/" scan (StylesheetsTest.php) reads this file's raw source too, and a
|
||||
// real Tailwind breakpoint prefix sitting in a class attribute here would be exactly what
|
||||
// that scan looks for — the placeholder keeps this file's own source clean of one.
|
||||
File::put($directory.'/probe.blade.php', str_replace('__BREAKPOINT__', 'md:', <<<'OUTER'
|
||||
@php
|
||||
$examples = [
|
||||
'Probe' => <<<'BLADE'
|
||||
<div class="__BREAKPOINT__flex"></div>
|
||||
BLADE,
|
||||
];
|
||||
@endphp
|
||||
OUTER));
|
||||
|
||||
try {
|
||||
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
|
||||
$probe = File::allFiles($directory)[0];
|
||||
|
||||
expect(breakpointOffenders($probe, $pattern))->not->toBeEmpty();
|
||||
} finally {
|
||||
File::deleteDirectory($directory);
|
||||
}
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A caller's `class` and `style` land on a component's root, untouched and once (the Phase F rule
|
||||
* in docs/plans/material-3-alignment-brief.md): an application's width or margin sizes the whole
|
||||
* component, whichever element its other attributes reach. The field family, the pickers and the
|
||||
* components that split their attributes between a root and an inner control are the ones that
|
||||
* can get it wrong, so each is rendered here with both.
|
||||
* A caller's `class` and `style` land on a component's root, untouched and once: an application's
|
||||
* width or margin sizes the whole component, whichever element its other attributes reach. The
|
||||
* field family, the pickers and the components that split their attributes between a root and an
|
||||
* inner control are the ones that can get it wrong, so each is rendered here with both.
|
||||
*/
|
||||
it('lands a caller\'s class and style on the root, and only there', function (string $blade) {
|
||||
$html = (string) $this->blade($blade);
|
||||
|
||||
@@ -5,16 +5,16 @@ use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
||||
|
||||
/**
|
||||
* Every component and layout-shared stylesheet, 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), writes its values from the tokens and its breakpoints as
|
||||
* px range queries, and is imported from its own block of all.css. This used to be four near-copies
|
||||
* of the same checks, one per group (Action/Containment/Input/Navigation), plus a fifth in
|
||||
* ErrorPagesTest.php for the error layout — folded into this one file (plan cleanup) because the
|
||||
* checks never differed in substance, only in which names they ran over and, for the token check,
|
||||
* which box-shadow shape a group's own values may take (see $boxShadow below). `icon` and `shape`
|
||||
* (no group's view renders them exclusively) and `color` (its own ColorTest.php already covers it,
|
||||
* the shared colour-role table every hue-taking component imports) keep their separate treatment.
|
||||
* Every component and layout-shared stylesheet: each view renders `data-md-*` attributes and no
|
||||
* class list of its own beyond the interaction and text classes (tests/Support/ViewClasses.php),
|
||||
* writes its values from the tokens and its breakpoints as px range queries, and is imported from
|
||||
* its own block of all.css. This used to be four near-copies of the same checks, one per group
|
||||
* (Action/Containment/Input/Navigation), plus a fifth in ErrorPagesTest.php for the error layout —
|
||||
* folded into this one file (plan cleanup) because the checks never differed in substance, only in
|
||||
* which names they ran over and, for the token check, which box-shadow shape a group's own values
|
||||
* may take (see $boxShadow below). `icon` and `shape` (no group's view renders them exclusively)
|
||||
* and `color` (its own ColorTest.php already covers it, the shared colour-role table every
|
||||
* hue-taking component imports) keep their separate treatment.
|
||||
*
|
||||
* Four names in the dataset are not a component view of their own: `selection` (what checkbox,
|
||||
* radio and toggle share), `navigation-item` (what the bar and the rail's items share), `pagination`
|
||||
@@ -103,9 +103,9 @@ dataset('scoped selectors', function (): Generator {
|
||||
it('draws the component from a stylesheet shaped like every package stylesheet', function (string $name) {
|
||||
$css = ComponentStylesheet::read($name);
|
||||
|
||||
// The header, the layer statement, the plain imports and freedom from Tailwind are
|
||||
// StylesheetsTest's, checked over every file all.css reaches; only the block layer and the
|
||||
// imports' existence are this dataset's own to check.
|
||||
// The header, the layer statement and the plain imports are StylesheetsTest's, checked over
|
||||
// every file all.css reaches; only the block layer and the imports' existence are this
|
||||
// dataset's own to check.
|
||||
expect($css->blocks())->each->toBe('@layer material.components');
|
||||
|
||||
foreach ($css->imports() as $import) {
|
||||
@@ -173,10 +173,10 @@ it('scopes every element-wide selector to a data-md hook', function (string $nam
|
||||
// ---- pagination: its views live outside resources/views/components/ --------------------------
|
||||
|
||||
dataset('pagination views', [
|
||||
'pagination/laravel/tailwind',
|
||||
'pagination/laravel/simple-tailwind',
|
||||
'pagination/livewire/tailwind',
|
||||
'pagination/livewire/simple-tailwind',
|
||||
'pagination/laravel/material',
|
||||
'pagination/laravel/simple-material',
|
||||
'pagination/livewire/material',
|
||||
'pagination/livewire/simple-material',
|
||||
]);
|
||||
|
||||
it('imports the stylesheet of every component pagination\'s views render', function (string $view) {
|
||||
|
||||
@@ -103,7 +103,7 @@ it('hides on a scroll down and springs back on a scroll up', function () {
|
||||
->toBe(['translate' => '0 100%'])
|
||||
// Reads --material-bottom-bar back from <x-scaffold>: layout/scaffold.css publishes it in
|
||||
// `material.layout`, which this rule's own `material.components` always outranks, so both
|
||||
// sit in a layer now (the era of an unlayered Tailwind utility is gone).
|
||||
// sit in a layer.
|
||||
->and($css->declarations('[data-md-scaffold]:has([data-md-navigation-bar][data-md-hide-on-scroll][data-md-hidden])'))
|
||||
->toBe(['--material-bottom-bar' => 'calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px))']);
|
||||
|
||||
|
||||
@@ -143,14 +143,14 @@ it('hides a collapsible or adaptive rail entirely when told to, and only those',
|
||||
});
|
||||
|
||||
/**
|
||||
* The Tailwind-era `rail-collapsed:` custom variant had seven branches for "this rail is drawn
|
||||
* collapsed"; navigation-rail.css merges the three that carry no media query into one `:where()`
|
||||
* (provably the same match set: an element matches `X:where(A, B, C)` exactly when it matches
|
||||
* `X:where(A)` or `X:where(B)` or `X:where(C)`) and keeps the four media-gated ones separate, since
|
||||
* CSS cannot merge different `@media` queries into one rule. This pins all five physical branches
|
||||
* — and so every one of the seven original conditions — for the rail's own width, the property
|
||||
* every branch sets, and for `--md-navigation-rail-value`, which each branch turns from `expanded`
|
||||
* to `collapsed` so an application's style query answers exactly when the rail's own shape does.
|
||||
* "This rail is drawn collapsed" has seven conditions in all; navigation-rail.css merges the three
|
||||
* that carry no media query into one `:where()` (provably the same match set: an element matches
|
||||
* `X:where(A, B, C)` exactly when it matches `X:where(A)` or `X:where(B)` or `X:where(C)`) and keeps
|
||||
* the four media-gated ones separate, since CSS cannot merge different `@media` queries into one
|
||||
* rule. This pins all five physical branches — and so every one of the seven original conditions —
|
||||
* for the rail's own width, the property every branch sets, and for `--md-navigation-rail-value`,
|
||||
* which each branch turns from `expanded` to `collapsed` so an application's style query answers
|
||||
* exactly when the rail's own shape does.
|
||||
*/
|
||||
it('holds a closing rail on screen for its exit rather than transitioning display', function () {
|
||||
$css = ComponentStylesheet::read('navigation-rail');
|
||||
|
||||
@@ -24,7 +24,7 @@ it('draws a search bar whose combobox controls the view', function () {
|
||||
->toContain('materialSearch(false,')
|
||||
->not->toContain('data-md-search-results')
|
||||
// Split so the string never reads as a literal class="..." attribute to the design guard's
|
||||
// own scan of this file — Tailwind's `relative` utility, which the field used to carry.
|
||||
// own scan of this file — `relative`, the utility class the field used to carry.
|
||||
->not->toContain('class="'.'relative"');
|
||||
|
||||
// The state belongs to the combobox, never to the bare textbox ARIA gives it to.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||
@@ -16,6 +15,15 @@ function fixtureRelative(array $violations): array
|
||||
return array_map(fn (string $violation): string => str_replace(realpath(GUARD_FIXTURES).'/', '', $violation), $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check (i)'s message for one class: it names no replacement, since a class no stylesheet declares
|
||||
* is dead whatever it was once meant to do.
|
||||
*/
|
||||
function undeclared(string $class): string
|
||||
{
|
||||
return "class `{$class}` is declared in no stylesheet — the package declares `md-*`, the rest is your own CSS";
|
||||
}
|
||||
|
||||
/**
|
||||
* Every check that only scans a fixture, optionally names the CSS entry `missingStylesheets()`
|
||||
* should check against, and compares the result to an exact violations list — most of this file,
|
||||
@@ -25,20 +33,58 @@ function fixtureRelative(array $violations): array
|
||||
* once) stays its own test, further down.
|
||||
*/
|
||||
dataset('design guard scans', [
|
||||
// app/Status.php returns class names from a PHP `match`, and views/page.blade.php writes one in
|
||||
// running text and in a Blade comment: none of the three is a class list, so none is read.
|
||||
'finds what compiles to nothing' => [
|
||||
[realpath(GUARD_FIXTURES.'/app'), realpath(GUARD_FIXTURES.'/views')],
|
||||
null,
|
||||
[
|
||||
'app/Status.php:14 Tailwind palette colour `text-red-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
|
||||
'views/page.blade.php:2 Tailwind spacing utility `p-4` compiles to nothing — use `padding="space200"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS',
|
||||
'views/page.blade.php:2 '.undeclared('p-4'),
|
||||
'views/page.blade.php:3 unknown Material Symbol `o-home`',
|
||||
'views/page.blade.php:4 unknown Material Symbol `not_a_symbol`',
|
||||
'views/page.blade.php:5 Tailwind palette colour `text-red-600` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
|
||||
'views/page.blade.php:6 Tailwind colour utility `text-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
||||
'views/page.blade.php:6 '.undeclared('focus-ring'),
|
||||
'views/page.blade.php:6 '.undeclared('text-tertiary'),
|
||||
'views/page.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`',
|
||||
"views/page.blade.php:8 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
||||
'views/page.blade.php:8 '.undeclared('size-4'),
|
||||
],
|
||||
],
|
||||
'passes a class the application\'s own stylesheet declares, and reports the one it does not' => [
|
||||
[realpath(GUARD_FIXTURES.'/declared/view.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')],
|
||||
null,
|
||||
['declared/view.blade.php:1 '.undeclared('mt-2')],
|
||||
],
|
||||
'reads a class list in an attribute, an Alpine binding and a PHP one, but not a string a condition compares or a call takes' => [
|
||||
[realpath(GUARD_FIXTURES.'/declared/bindings.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')],
|
||||
null,
|
||||
[
|
||||
'declared/bindings.blade.php:1 '.undeclared('collapsed'),
|
||||
'declared/bindings.blade.php:1 '.undeclared('dashboard-busy'),
|
||||
'declared/bindings.blade.php:1 '.undeclared('is-open'),
|
||||
'declared/bindings.blade.php:2 '.undeclared('dashboard-enter-start'),
|
||||
'declared/bindings.blade.php:2 '.undeclared('dashboard-enter'),
|
||||
'declared/bindings.blade.php:3 '.undeclared('share-list'),
|
||||
'declared/bindings.blade.php:4 '.undeclared('dashboard-lead'),
|
||||
'declared/bindings.blade.php:6 '.undeclared('share-list'),
|
||||
],
|
||||
],
|
||||
'reads the keys of a class list, the array a class helper is given and every branch of a ternary, never a string that indexes an array' => [
|
||||
[realpath(GUARD_FIXTURES.'/declared/lookups.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')],
|
||||
null,
|
||||
[
|
||||
'declared/lookups.blade.php:4 '.undeclared('product-shot-base'),
|
||||
'declared/lookups.blade.php:8 '.undeclared('routes-show-run-chosen'),
|
||||
'declared/lookups.blade.php:9 '.undeclared('shot-helper-one'),
|
||||
'declared/lookups.blade.php:9 '.undeclared('shot-helper-two'),
|
||||
'declared/lookups.blade.php:10 '.undeclared('shot-tern-a'),
|
||||
'declared/lookups.blade.php:10 '.undeclared('shot-tern-b'),
|
||||
'declared/lookups.blade.php:10 '.undeclared('shot-tern-c'),
|
||||
],
|
||||
],
|
||||
'passes every class the package itself declares, showcase included' => [
|
||||
realpath(GUARD_FIXTURES.'/declared/package-classes.blade.php'),
|
||||
null,
|
||||
[],
|
||||
],
|
||||
'names what a component tag takes in place of each directive' => [
|
||||
realpath(GUARD_FIXTURES.'/directives'),
|
||||
null,
|
||||
@@ -46,309 +92,18 @@ dataset('design guard scans', [
|
||||
'directives/tags.blade.php:1 Blade directive `@js` inside a component tag, where it does not compile — use `{{ \\Illuminate\\Support\\Js::from(…) }}`',
|
||||
'directives/tags.blade.php:2 Blade directive `@json` inside a component tag, where it does not compile — use `{{ json_encode(…) }}`',
|
||||
'directives/tags.blade.php:3 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`',
|
||||
'directives/tags.blade.php:3 '.undeclared('is-open'),
|
||||
'directives/tags.blade.php:4 Blade directive `@style` inside a component tag, where it does not compile — use `:style="\\Illuminate\\Support\\Arr::toCssStyles([…])"`',
|
||||
"directives/tags.blade.php:5 Blade directive `@entangle` inside a component tag, where it does not compile — use `\$wire.entangle('…')` in the Alpine expression",
|
||||
'directives/tags.blade.php:6 Blade directive `@disabled` inside a component tag, where it does not compile — use `:disabled="…"`',
|
||||
'directives/tags.blade.php:7 Blade directive `@if` inside a component tag, where it does not compile — use a `:prop` binding or `{{ }}`, or move it to a plain element inside the slot',
|
||||
],
|
||||
],
|
||||
'names the M3 breakpoint for a Tailwind prefix, with its 2.0.0 replacement' => [
|
||||
realpath(GUARD_FIXTURES.'/breakpoints'),
|
||||
null,
|
||||
[
|
||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `2xl:` compiles to nothing — M3's extra-large (1600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 1600px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `lg:` compiles to nothing — M3's expanded (840px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 840px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `sm:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:1 Tailwind breakpoint `xl:` compiles to nothing — M3's large (1200px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 1200px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-2xl:` compiles to nothing — M3's extra-large (1600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 1600px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-lg:` compiles to nothing — M3's expanded (840px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 840px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 600px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-sm:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 600px)` in your own CSS",
|
||||
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-xl:` compiles to nothing — M3's large (1200px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 1200px)` in your own CSS",
|
||||
],
|
||||
],
|
||||
'names the 2.0.0 replacement for a pseudo-class variant, but leaves a breakpoint prefix to its own finding' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/variant.blade.php'),
|
||||
null,
|
||||
[
|
||||
"tailwind/variant.blade.php:1 Tailwind variant class `dark:opacity-50` compiles to nothing — the roles already switch with the theme; a dark-only rule is `[data-theme='dark'] …` in your own CSS",
|
||||
"tailwind/variant.blade.php:1 Tailwind variant class `hover:underline` compiles to nothing — M3's hover, focus and press states are `md-state-layer` and `md-focus-ring` (interaction.css); any other state rule is `:hover`/`:focus-visible` in your own CSS",
|
||||
"tailwind/variant.blade.php:2 Tailwind breakpoint `md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
||||
"tailwind/variant.blade.php:2 Tailwind variant class `focus:md:gap-2` compiles to nothing — M3's hover, focus and press states are `md-state-layer` and `md-focus-ring` (interaction.css); any other state rule is `:hover`/`:focus-visible` in your own CSS",
|
||||
'tailwind/variant.blade.php:2 Tailwind variant class `group-hover:flex` compiles to nothing — `:has()` or a descendant selector in your own CSS',
|
||||
],
|
||||
],
|
||||
'leaves a breakpoint name that is not a variant alone' => [
|
||||
__DIR__.'/../../resources/js/slider.js',
|
||||
null,
|
||||
[],
|
||||
],
|
||||
'names the M3 corner for a Tailwind radius' => [
|
||||
realpath(GUARD_FIXTURES.'/scale/corners.blade.php'),
|
||||
null,
|
||||
[
|
||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-lg` — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-md` — use `var(--md-sys-shape-corner-md)` in your own CSS, or `<x-surface corner="md">`',
|
||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-none` — use `var(--md-sys-shape-corner-none)` in your own CSS, or `<x-surface corner="none">`',
|
||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-sm` — use `var(--md-sys-shape-corner-sm)` in your own CSS, or `<x-surface corner="sm">`',
|
||||
'scale/corners.blade.php:1 value outside the M3 scale `rounded-xs` — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`',
|
||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-2xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-3xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-4xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-full` — use `var(--md-sys-shape-corner-full)` in your own CSS, or `<x-surface corner="full">`',
|
||||
'scale/corners.blade.php:2 value outside the M3 scale `rounded-xl` — use `var(--md-sys-shape-corner-xl)` in your own CSS, or `<x-surface corner="xl">`',
|
||||
'scale/corners.blade.php:3 value outside the M3 scale `rounded-se-2xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
||||
'scale/corners.blade.php:3 value outside the M3 scale `rounded-t-lg` — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
||||
],
|
||||
],
|
||||
'names the M3 elevation level for a Tailwind shadow' => [
|
||||
realpath(GUARD_FIXTURES.'/scale/elevation.blade.php'),
|
||||
null,
|
||||
[
|
||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-2xs` — use `var(--md-sys-elevation-1)` in your own CSS',
|
||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-md` — use `var(--md-sys-elevation-2)` in your own CSS',
|
||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-sm` — use `var(--md-sys-elevation-1)` in your own CSS',
|
||||
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-xs` — use `var(--md-sys-elevation-1)` in your own CSS',
|
||||
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-2xl` — use `var(--md-sys-elevation-5)` in your own CSS',
|
||||
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-lg` — use `var(--md-sys-elevation-3)` in your own CSS',
|
||||
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-xl` — use `var(--md-sys-elevation-4)` in your own CSS',
|
||||
],
|
||||
],
|
||||
'sends a size, a weight, a leading and a tracking to a type style' => [
|
||||
realpath(GUARD_FIXTURES.'/scale/type.blade.php'),
|
||||
null,
|
||||
[
|
||||
'scale/type.blade.php:1 value outside the M3 scale `text-2xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:1 value outside the M3 scale `text-9xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:1 value outside the M3 scale `text-base` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:1 value outside the M3 scale `text-lg` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:1 value outside the M3 scale `text-sm` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:1 value outside the M3 scale `text-xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:1 value outside the M3 scale `text-xs` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:2 value outside the M3 scale `font-extralight` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:2 value outside the M3 scale `font-light` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:2 value outside the M3 scale `font-medium` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:2 value outside the M3 scale `font-normal` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:2 value outside the M3 scale `font-thin` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:3 value outside the M3 scale `font-black` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:3 value outside the M3 scale `font-bold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:3 value outside the M3 scale `font-extrabold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:3 value outside the M3 scale `font-semibold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
||||
'scale/type.blade.php:4 value outside the M3 scale `leading-6` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:4 value outside the M3 scale `leading-loose` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:4 value outside the M3 scale `leading-none` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:4 value outside the M3 scale `leading-normal` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:4 value outside the M3 scale `leading-relaxed` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:4 value outside the M3 scale `leading-snug` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:4 value outside the M3 scale `leading-tight` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-normal` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-tight` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-tighter` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-wide` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-wider` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'scale/type.blade.php:5 value outside the M3 scale `tracking-widest` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
],
|
||||
],
|
||||
'sends an easing and a duration to the motion tokens' => [
|
||||
realpath(GUARD_FIXTURES.'/scale/motion.blade.php'),
|
||||
null,
|
||||
[
|
||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-in-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-in` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-linear` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
||||
'scale/motion.blade.php:1 value outside the M3 scale `ease-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
||||
'scale/motion.blade.php:2 value outside the M3 scale `duration-1000` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
||||
'scale/motion.blade.php:2 value outside the M3 scale `duration-300` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
||||
'scale/motion.blade.php:2 value outside the M3 scale `duration-75` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
||||
],
|
||||
],
|
||||
// scale/motion.js reads a real CSS keyword out of matchMedia() into a variable named `easing`
|
||||
// (`'linear'`/`'ease-out'`), the shape the plan calls out as a false positive: nothing here is
|
||||
// a class list, so the class-context match above never sees it, and the family is off
|
||||
// outsideTheScale()'s line scan since the fix (see DesignGuard::outsideTheScale()'s header).
|
||||
'leaves an easing or a duration word alone in a JavaScript string outside a class list' => [
|
||||
realpath(GUARD_FIXTURES.'/scale/motion.js'),
|
||||
null,
|
||||
[],
|
||||
],
|
||||
'finds a colour written as a value' => [
|
||||
realpath(GUARD_FIXTURES.'/colour'),
|
||||
null,
|
||||
[
|
||||
'colour/badge.blade.php:1 arbitrary colour `bg-[#1d7afc]`, use an M3 role',
|
||||
'colour/badge.blade.php:1 arbitrary colour `border-[hsl(210_80%_50%)]`, use an M3 role',
|
||||
'colour/badge.blade.php:1 arbitrary colour `text-[rgb(0_0_0)]`, use an M3 role',
|
||||
'colour/badge.blade.php:2 arbitrary colour `fill-[oklch(0.7_0.1_250)]`, use an M3 role',
|
||||
'colour/badge.blade.php:2 arbitrary colour `ring-[color-mix(in_oklab,var(--x)_50%,transparent)]`, use an M3 role',
|
||||
'colour/badge.blade.php:3 Tailwind colour utility `bg-white` compiles to nothing — M3 paints with roles, never white or black: `<x-surface level="surface-container-lowest">` or `var(--md-sys-color-surface-container-lowest)` for a white surface, an `on-` role (`md-ink`, `var(--md-sys-color-on-primary)`) for ink',
|
||||
'colour/badge.blade.php:3 Tailwind colour utility `border-white` compiles to nothing — M3 paints with roles, never white or black: `<x-surface level="surface-container-lowest">` or `var(--md-sys-color-surface-container-lowest)` for a white surface, an `on-` role (`md-ink`, `var(--md-sys-color-on-primary)`) for ink',
|
||||
'colour/badge.blade.php:3 Tailwind colour utility `text-black` compiles to nothing — M3 paints with roles, never white or black: `<x-surface level="surface-container-lowest">` or `var(--md-sys-color-surface-container-lowest)` for a white surface, an `on-` role (`md-ink`, `var(--md-sys-color-on-primary)`) for ink',
|
||||
"colour/badge.blade.php:4 Tailwind colour utility `bg-on-surface/12` compiles to nothing — M3's hover, focus and press overlays are `md-state-layer`; any other tint is `color-mix(in srgb, var(--md-sys-color-on-surface) <n>%, transparent)` in your own CSS",
|
||||
"colour/badge.blade.php:4 Tailwind colour utility `border-outline/38` compiles to nothing — M3's hover, focus and press overlays are `md-state-layer`; any other tint is `color-mix(in srgb, var(--md-sys-color-outline) <n>%, transparent)` in your own CSS",
|
||||
"colour/badge.blade.php:4 Tailwind colour utility `text-on-surface/60` compiles to nothing — M3's quieter text is a role, not a faded one: `md-ink-variant` or `md-ink-quiet` (text.css)",
|
||||
],
|
||||
],
|
||||
'sends the state-layer opacities and every other role to their 2.0.0 replacement' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/inks.blade.php'),
|
||||
null,
|
||||
[
|
||||
'tailwind/inks.blade.php:1 Tailwind colour utility `bg-background` compiles to nothing — use `<x-surface level="surface">`',
|
||||
'tailwind/inks.blade.php:1 Tailwind colour utility `text-current` compiles to nothing — write `currentColor` in your own CSS',
|
||||
'tailwind/inks.blade.php:1 Tailwind colour utility `text-on-primary-container` compiles to nothing — use `var(--md-sys-color-on-primary-container)` in your own CSS',
|
||||
],
|
||||
],
|
||||
'names the md-ink-*, divider/surface or token replacement for a dead M3 role utility' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/roles.blade.php'),
|
||||
null,
|
||||
[
|
||||
'tailwind/roles.blade.php:1 Tailwind colour utility `border-outline-variant` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`, not a border utility',
|
||||
'tailwind/roles.blade.php:1 Tailwind colour utility `text-on-surface-variant` compiles to nothing — use `md-ink-variant` (text.css)',
|
||||
'tailwind/roles.blade.php:2 Tailwind colour utility `bg-primary` compiles to nothing — use `var(--md-sys-color-primary)` in your own CSS',
|
||||
'tailwind/roles.blade.php:2 Tailwind colour utility `bg-surface-container` compiles to nothing — use `<x-surface level="surface-container">`',
|
||||
],
|
||||
],
|
||||
'sends flex, grid and gap utilities to the layout component and prop that replaces each' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/layout.blade.php'),
|
||||
null,
|
||||
[
|
||||
'tailwind/layout.blade.php:1 Tailwind layout utility `flex-col` compiles to nothing — use `<x-stack>` (`gap`, `align`)',
|
||||
'tailwind/layout.blade.php:1 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
||||
'tailwind/layout.blade.php:1 Tailwind layout utility `grid` compiles to nothing — use `<x-grid>` (`:columns` per breakpoint, `gap`, `min-item`), or `<x-feed>` for a grid of cards',
|
||||
'tailwind/layout.blade.php:1 Tailwind layout utility `inline-flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
||||
"tailwind/layout.blade.php:2 Tailwind flex/grid item utility `col-span-2` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
||||
"tailwind/layout.blade.php:2 Tailwind layout utility `grid-cols-3` compiles to nothing — use `<x-grid :columns=\"3\">`, or a per-breakpoint map (`:columns=\"['compact' => 1, 'medium' => 3]\"`)",
|
||||
'tailwind/layout.blade.php:2 Tailwind layout utility `items-center` compiles to nothing — use `align="center"` on `<x-row>` or `<x-stack>`',
|
||||
'tailwind/layout.blade.php:2 Tailwind layout utility `justify-between` compiles to nothing — use `justify="between"` on `<x-row>`',
|
||||
'tailwind/layout.blade.php:2 Tailwind spacing utility `gap-4` compiles to nothing — use `gap="space200"` (16px) on `<x-row>`, `<x-stack>`, `<x-grid>` or `<x-feed>`',
|
||||
],
|
||||
],
|
||||
'sends padding, margin and space-between utilities to the M3 spacing step they are (gap-4 is space200)' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/spacing.blade.php'),
|
||||
null,
|
||||
[
|
||||
"tailwind/spacing.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
||||
'tailwind/spacing.blade.php:1 Tailwind spacing utility `p-4` compiles to nothing — use `padding="space200"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS',
|
||||
'tailwind/spacing.blade.php:1 Tailwind spacing utility `px-2` compiles to nothing — use `padding="space100"` (8px) on `<x-surface>`, or `var(--md-sys-measurement-space100)` in your own CSS',
|
||||
'tailwind/spacing.blade.php:1 Tailwind spacing utility `space-y-4` compiles to nothing — use `<x-stack>` with `gap="space200"` (16px)',
|
||||
"tailwind/spacing.blade.php:2 Tailwind spacing utility `m-4` compiles to nothing — space between siblings is a layout component's `gap=\"space200\"` (16px); any other margin is `var(--md-sys-measurement-space200)` in your own CSS",
|
||||
'tailwind/spacing.blade.php:2 Tailwind spacing utility `space-x-2` compiles to nothing — use `<x-row>` with `gap="space100"` (8px)',
|
||||
],
|
||||
],
|
||||
'sends width and height utilities to a literal length, since M3 keeps no size scale' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/sizing.blade.php'),
|
||||
null,
|
||||
[
|
||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `h-screen` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `max-w-md` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `min-w-0` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
||||
"tailwind/sizing.blade.php:1 Tailwind sizing utility `w-full` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
||||
"tailwind/sizing.blade.php:2 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
||||
],
|
||||
],
|
||||
'sends hidden to hide-below/hide-from and every other display utility to a plain CSS rule' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/display.blade.php'),
|
||||
null,
|
||||
[
|
||||
'tailwind/display.blade.php:1 Tailwind display utility `block` compiles to nothing — write the `display` rule in your own CSS',
|
||||
"tailwind/display.blade.php:1 Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute (the reset keeps it hidden), or `x-show`",
|
||||
'tailwind/display.blade.php:2 Tailwind display utility `inline-block` compiles to nothing — write the `display` rule in your own CSS',
|
||||
'tailwind/display.blade.php:2 Tailwind display utility `invisible` compiles to nothing — write the `display` rule in your own CSS',
|
||||
],
|
||||
],
|
||||
'sends text-layout utilities to their md-* class' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/text.blade.php'),
|
||||
null,
|
||||
[
|
||||
"tailwind/text.blade.php:1 Tailwind's `text-center` compiles to nothing — use `md-text-center` (text.css)",
|
||||
"tailwind/text.blade.php:1 Tailwind's `truncate` compiles to nothing — use `md-truncate` (text.css)",
|
||||
"tailwind/text.blade.php:2 Tailwind's `line-clamp-2` compiles to nothing — use `md-line-clamp-2` (text.css)",
|
||||
"tailwind/text.blade.php:2 Tailwind's `sr-only` compiles to nothing — use `md-visually-hidden` (text.css)",
|
||||
"tailwind/text.blade.php:2 Tailwind's `tabular-nums` compiles to nothing — use `md-tabular` (text.css)",
|
||||
"tailwind/text.blade.php:2 Tailwind's `whitespace-nowrap` compiles to nothing — use `md-nowrap` (text.css)",
|
||||
],
|
||||
],
|
||||
'exempts a class the application\'s own CSS declares, even when it is Tailwind-shaped' => [
|
||||
realpath(GUARD_FIXTURES.'/exempt'),
|
||||
null,
|
||||
[
|
||||
"exempt/view.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
||||
],
|
||||
],
|
||||
'reports an arbitrary [] value once, whatever utility it modifies, and strips a trailing !' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/arbitrary.blade.php'),
|
||||
null,
|
||||
[
|
||||
'tailwind/arbitrary.blade.php:1 Tailwind arbitrary value `grid-cols-[1fr_2fr]` compiles to nothing — write the literal value in your own CSS, or use an M3 token',
|
||||
'tailwind/arbitrary.blade.php:1 Tailwind arbitrary value `top-[10px]` compiles to nothing — write the literal value in your own CSS, or use an M3 token',
|
||||
'tailwind/arbitrary.blade.php:2 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
||||
'tailwind/arbitrary.blade.php:4 Tailwind arbitrary property `[--material-bottom-bar:5rem]` compiles to nothing — write `--material-bottom-bar` in your own CSS',
|
||||
"tailwind/arbitrary.blade.php:4 Tailwind arbitrary property `[font-variation-settings:'ROND'_100]` compiles to nothing — write `font-variation-settings` in your own CSS",
|
||||
],
|
||||
],
|
||||
'leaves every role alone until an application forbids one' => [
|
||||
realpath(GUARD_FIXTURES.'/forbidden-colours'),
|
||||
null,
|
||||
[
|
||||
'forbidden-colours/page.blade.php:5 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
||||
],
|
||||
],
|
||||
'names the property a Tailwind custom-property utility stands for' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/custom-property.blade.php'),
|
||||
null,
|
||||
[
|
||||
'tailwind/custom-property.blade.php:1 Tailwind custom-property utility `duration-(--md-sys-motion-spatial-fast-duration)` compiles to nothing — write `var(--md-sys-motion-spatial-fast-duration)` in your own CSS',
|
||||
],
|
||||
],
|
||||
'reports position, border, effect, interactivity and text utilities with the rule to write instead' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/utilities.blade.php'),
|
||||
null,
|
||||
[
|
||||
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `col-start-2` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
||||
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `justify-self-end` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
||||
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `shrink-0` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
||||
'tailwind/utilities.blade.php:1 Tailwind inset utility `-top-2` compiles to nothing — write the offset in your own CSS, from `var(--md-sys-measurement-space*)` where it is a spacing step',
|
||||
'tailwind/utilities.blade.php:1 Tailwind inset utility `inset-0` compiles to nothing — write the offset in your own CSS, from `var(--md-sys-measurement-space*)` where it is a spacing step',
|
||||
'tailwind/utilities.blade.php:1 Tailwind overflow utility `overflow-hidden` compiles to nothing — write `overflow` in your own CSS',
|
||||
"tailwind/utilities.blade.php:1 Tailwind position utility `relative` compiles to nothing — write `position` in your own CSS (a FAB goes in `<x-scaffold>`'s `fab` slot)",
|
||||
'tailwind/utilities.blade.php:1 Tailwind z-index utility `z-10` compiles to nothing — write `z-index` in your own CSS',
|
||||
"tailwind/utilities.blade.php:1 Tailwind's `container` compiles to nothing — use `<x-pane width>`, which sets M3's margins and a measure",
|
||||
'tailwind/utilities.blade.php:2 Tailwind border utility `border-t-2` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`',
|
||||
'tailwind/utilities.blade.php:2 Tailwind border utility `border` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`',
|
||||
'tailwind/utilities.blade.php:2 Tailwind effect utility `-rotate-45` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/utilities.blade.php:2 Tailwind interactivity utility `cursor-pointer` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/utilities.blade.php:2 Tailwind interactivity utility `select-none` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/utilities.blade.php:2 Tailwind motion utility `transition` compiles to nothing — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition` or `animation`',
|
||||
"tailwind/utilities.blade.php:2 Tailwind opacity utility `opacity-50` compiles to nothing — write `opacity` in your own CSS (M3's disabled content is 38 %)",
|
||||
"tailwind/utilities.blade.php:2 Tailwind outline utility `outline-none` compiles to nothing — M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS",
|
||||
"tailwind/utilities.blade.php:2 Tailwind outline utility `ring-2` compiles to nothing — M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS",
|
||||
'tailwind/utilities.blade.php:2 Tailwind radius utility `rounded` compiles to nothing — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`',
|
||||
'tailwind/utilities.blade.php:2 Tailwind shadow utility `shadow` compiles to nothing — use `var(--md-sys-elevation-*)` in your own CSS',
|
||||
'tailwind/utilities.blade.php:3 Tailwind background utility `bg-clip-text` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/utilities.blade.php:3 Tailwind display utility `table` compiles to nothing — write the `display` rule in your own CSS',
|
||||
'tailwind/utilities.blade.php:3 Tailwind media utility `aspect-video` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS',
|
||||
'tailwind/utilities.blade.php:3 Tailwind media utility `object-cover` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS',
|
||||
'tailwind/utilities.blade.php:3 Tailwind text utility `list-disc` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/utilities.blade.php:3 Tailwind text utility `uppercase` compiles to nothing — write the rule in your own CSS',
|
||||
"tailwind/utilities.blade.php:3 Tailwind's `antialiased` compiles to nothing — the foundation already smooths text in grayscale (base.css); drop it",
|
||||
"tailwind/utilities.blade.php:3 Tailwind's `font-mono` compiles to nothing — put the value in `<code>`, `<kbd>` or `<samp>`, or use `md-mono` (text.css)",
|
||||
"tailwind/utilities.blade.php:3 Tailwind's `font-sans` compiles to nothing — the foundation already sets the brand typeface; any other `font-family` is your own CSS",
|
||||
"tailwind/utilities.blade.php:3 Tailwind's `text-end` compiles to nothing — use `md-text-end` (text.css)",
|
||||
"tailwind/utilities.blade.php:3 Tailwind's `underline` compiles to nothing — `md-link` draws a link (interaction.css); any other decoration is your own CSS",
|
||||
],
|
||||
],
|
||||
'reads class lists in Alpine, Livewire and PHP bindings, but not a string compared in a condition' => [
|
||||
realpath(GUARD_FIXTURES.'/tailwind/bindings.blade.php'),
|
||||
null,
|
||||
[
|
||||
"tailwind/bindings.blade.php:1 Tailwind opacity utility `opacity-50` compiles to nothing — write `opacity` in your own CSS (M3's disabled content is 38 %)",
|
||||
"tailwind/bindings.blade.php:1 Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute (the reset keeps it hidden), or `x-show`",
|
||||
'tailwind/bindings.blade.php:2 Tailwind effect utility `scale-95` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/bindings.blade.php:2 Tailwind motion utility `transition` compiles to nothing — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition` or `animation`',
|
||||
'tailwind/bindings.blade.php:2 value outside the M3 scale `ease-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
||||
'tailwind/bindings.blade.php:3 Tailwind spacing utility `gap-2` compiles to nothing — use `gap="space100"` (8px) on `<x-row>`, `<x-stack>`, `<x-grid>` or `<x-feed>`',
|
||||
"tailwind/bindings.blade.php:4 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
||||
'tailwind/bindings.blade.php:6 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
||||
'forbidden-colours/page.blade.php:5 '.undeclared('bg-tertiary'),
|
||||
],
|
||||
],
|
||||
'names the missing package stylesheet and the exact @import line to add' => [
|
||||
@@ -441,11 +196,6 @@ dataset('design guard scans', [
|
||||
'shadows/rings.css:20 literal shadow `box-shadow: inset 0 0 4px 1px #000` — use `var(--md-sys-elevation-*)`',
|
||||
],
|
||||
],
|
||||
'never reads a Tailwind-shaped word out of running text' => [
|
||||
realpath(GUARD_FIXTURES.'/running-text.blade.php'),
|
||||
null,
|
||||
[],
|
||||
],
|
||||
]);
|
||||
|
||||
it('scans a fixture and reports exactly the expected violations', function (string|array $fixture, ?string $entry, array $expected) {
|
||||
@@ -454,81 +204,32 @@ it('scans a fixture and reports exactly the expected violations', function (stri
|
||||
expect(fixtureRelative(($entry === null ? $guard : $guard->missingStylesheets($entry))->violations()))->toBe($expected);
|
||||
})->with('design guard scans');
|
||||
|
||||
it('reports a colour utility on the application\'s own theme colour, and a Tailwind utility of the same prefix as its own family', function () {
|
||||
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/theme-colours.blade.php'))->violations());
|
||||
$theme = fn (string $token, string $name, string $use = '`var(--…)`'): string => "Tailwind colour utility `{$token}` compiles to nothing — `{$name}` is neither an M3 role nor a Tailwind colour, so it named a colour of the application's own theme: use {$use} in your own CSS";
|
||||
$tint = '`color-mix(in srgb, var(--…) <n>%, transparent)`';
|
||||
$line = 'a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`';
|
||||
$outline = "M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS";
|
||||
|
||||
expect($violations)->toBe([
|
||||
'tailwind/theme-colours.blade.php:1 '.$theme('bg-off-plan', 'off-plan'),
|
||||
'tailwind/theme-colours.blade.php:1 '.$theme('from-brand', 'brand'),
|
||||
'tailwind/theme-colours.blade.php:1 '.$theme('text-brand-ink', 'brand-ink'),
|
||||
'tailwind/theme-colours.blade.php:1 '.$theme('text-sport-run', 'sport-run'),
|
||||
'tailwind/theme-colours.blade.php:1 '.$theme('to-brand-end', 'brand-end'),
|
||||
'tailwind/theme-colours.blade.php:2 '.$theme('bg-route-reference/8', 'route-reference', $tint),
|
||||
'tailwind/theme-colours.blade.php:2 '.$theme('border-l-zone-4', 'zone-4'),
|
||||
'tailwind/theme-colours.blade.php:2 '.$theme('border-route-reference', 'route-reference'),
|
||||
'tailwind/theme-colours.blade.php:2 '.$theme('ring-offset-brand/[0.4]', 'brand', $tint),
|
||||
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-balance` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-clip` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-ellipsis` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-pretty` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:3 Tailwind text utility `text-wrap` compiles to nothing — write the rule in your own CSS',
|
||||
"tailwind/theme-colours.blade.php:3 Tailwind's `text-nowrap` compiles to nothing — use `md-nowrap` (text.css)",
|
||||
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-dashed` compiles to nothing — {$line}",
|
||||
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-dotted` compiles to nothing — {$line}",
|
||||
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-none` compiles to nothing — {$line}",
|
||||
"tailwind/theme-colours.blade.php:4 Tailwind border utility `border-solid` compiles to nothing — {$line}",
|
||||
'tailwind/theme-colours.blade.php:4 Tailwind table utility `border-collapse` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:4 Tailwind table utility `border-separate` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:5 Tailwind gradient utility `from-0%` compiles to nothing — write the gradient in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:5 Tailwind gradient utility `via-none` compiles to nothing — write the gradient in your own CSS',
|
||||
"tailwind/theme-colours.blade.php:5 Tailwind outline utility `outline-dashed` compiles to nothing — {$outline}",
|
||||
"tailwind/theme-colours.blade.php:5 Tailwind outline utility `outline-none` compiles to nothing — {$outline}",
|
||||
"tailwind/theme-colours.blade.php:5 Tailwind outline utility `ring-inset` compiles to nothing — {$outline}",
|
||||
'tailwind/theme-colours.blade.php:5 Tailwind shadow utility `shadow-none` compiles to nothing — use `var(--md-sys-elevation-*)` in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:5 Tailwind text utility `decoration-wavy` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind SVG utility `fill-none` compiles to nothing — write `fill` or `stroke` in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-center` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-clip-text` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-cover` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-fixed` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-no-repeat` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind background utility `bg-top-left` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind colour utility `text-inherit` compiles to nothing — write `inherit` in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind interactivity utility `accent-auto` compiles to nothing — write the rule in your own CSS',
|
||||
'tailwind/theme-colours.blade.php:6 Tailwind text utility `text-shadow-sm` compiles to nothing — write the rule in your own CSS',
|
||||
"tailwind/theme-colours.blade.php:7 Tailwind border utility `border-bs-2` compiles to nothing — {$line}",
|
||||
'tailwind/theme-colours.blade.php:7 Tailwind palette colour `bg-mauve-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves a Markdown mail component\'s classes and its theme to the mail theme, but still reads its icons and directives', function () {
|
||||
$mail = realpath(GUARD_FIXTURES.'/mail/views');
|
||||
$table = 'Tailwind display utility `table` compiles to nothing — write the `display` rule in your own CSS';
|
||||
$icon = 'mail/views/vendor/mail/html/sessions.blade.php:7 unknown Material Symbol `not_a_symbol`';
|
||||
$directive = 'mail/views/vendor/mail/html/sessions.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`';
|
||||
|
||||
config(['mail.markdown.paths' => [$mail.'/vendor/mail']]);
|
||||
|
||||
// The theme's `.table` exempts nothing outside the mail components, and its literals are its own.
|
||||
// The theme's `.table` declares nothing outside the mail components, and its literals are its own.
|
||||
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
||||
"mail/views/page.blade.php:1 {$table}",
|
||||
'mail/views/page.blade.php:1 '.undeclared('table'),
|
||||
$icon,
|
||||
$directive,
|
||||
]);
|
||||
|
||||
// Outside a mail component path the same files read as the application's own: the theme's
|
||||
// classes as Tailwind, its literals as check (iii)'s, and its `.table` exempting `table`.
|
||||
// classes against its own rules, which declare `table` and nothing else, and its literals as
|
||||
// check (iii)'s.
|
||||
config(['mail.markdown.paths' => []]);
|
||||
|
||||
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:3 value outside the M3 scale `text-sm` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:4 Tailwind text utility `break-all` compiles to nothing — write the rule in your own CSS',
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:3 '.undeclared('sessions'),
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:3 '.undeclared('text-sm'),
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:4 '.undeclared('break-all'),
|
||||
$icon,
|
||||
$directive,
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:8 '.undeclared('wide'),
|
||||
'mail/views/vendor/mail/html/themes/default.css:2 literal colour `#3d4852` — use `var(--md-sys-color-*)`',
|
||||
'mail/views/vendor/mail/html/themes/default.css:3 literal font size `font-size: 14px` — set the whole style with `font: var(--md-sys-typescale-*)` and its `-tracking`, or an `md-type-*` class',
|
||||
]);
|
||||
@@ -547,7 +248,7 @@ it('bans the roles an application leaves out, wherever 2.0.0 writes one', functi
|
||||
"forbidden-colours/page.blade.php:3 `:color=\"\$failed ? 'error' : 'info'\"`: role `error` is not part of this application's palette",
|
||||
"forbidden-colours/page.blade.php:4 `--md-sys-color-on-tertiary-container`: role `tertiary` is not part of this application's palette",
|
||||
"forbidden-colours/page.blade.php:4 `md-ink-error`: role `error` is not part of this application's palette",
|
||||
'forbidden-colours/page.blade.php:5 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
||||
'forbidden-colours/page.blade.php:5 '.undeclared('bg-tertiary'),
|
||||
"forbidden-colours/page.blade.php:7 `--md-sys-color-primary-container`: role `primary-container` is not part of this application's palette",
|
||||
]);
|
||||
});
|
||||
@@ -576,7 +277,7 @@ it('names each stylesheet the entry imports that no scanned view needs, and leav
|
||||
|
||||
it('follows an entry that imports what only a Vite build resolves, reports each missing stylesheet once, and leaves the entry\'s literals to scan()', function () {
|
||||
$entry = realpath(GUARD_FIXTURES.'/stylesheets/foreign-imports.css');
|
||||
$mt2 = "stylesheets/views-foreign/page.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS";
|
||||
$mt2 = 'stylesheets/views-foreign/page.blade.php:1 '.undeclared('mt-2');
|
||||
$button = "stylesheets/views-foreign/page.blade.php:2 `<x-button>` needs `components/button.css`, missing from stylesheets/foreign-imports.css — add `@import '../../../../resources/css/components/button.css';`";
|
||||
|
||||
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views-foreign'))->missingStylesheets($entry)->violations()))
|
||||
@@ -682,8 +383,7 @@ it('sorts every finding by path, then line, then message, whichever check produc
|
||||
// check (iii)'s application-CSS pass always runs last, after every view's own violations —
|
||||
// an unsorted result would put a.css's finding after z.blade.php's, even though "a.css"
|
||||
// sorts before "z.blade.php" (Finder itself already walks views alphabetically, so this is
|
||||
// the one place natural order and sorted order actually diverge). One line also names a
|
||||
// palette colour and a spacing utility, from two checks that run in the opposite order.
|
||||
// the one place natural order and sorted order actually diverge).
|
||||
File::put($dir.'/a.css', "body {\n color: #ff0000;\n}\n");
|
||||
File::put($dir.'/z.blade.php', '<div class="bg-red-500 p-4"></div>');
|
||||
|
||||
@@ -695,8 +395,8 @@ it('sorts every finding by path, then line, then message, whichever check produc
|
||||
|
||||
expect($violations)->toBe([
|
||||
"{$dir}/a.css:2 literal colour `#ff0000` — use `var(--md-sys-color-*)`",
|
||||
"{$dir}/z.blade.php:1 Tailwind palette colour `bg-red-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS",
|
||||
"{$dir}/z.blade.php:1 Tailwind spacing utility `p-4` compiles to nothing — use `padding=\"space200\"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS",
|
||||
"{$dir}/z.blade.php:1 ".undeclared('bg-red-500'),
|
||||
"{$dir}/z.blade.php:1 ".undeclared('p-4'),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -704,33 +404,11 @@ it('passes the package\'s own views, showcase and Workbench, every stylesheet im
|
||||
$root = realpath(__DIR__.'/../..');
|
||||
$paths = [$root.'/resources/views', $root.'/resources/js', $root.'/src', $root.'/workbench/resources'];
|
||||
|
||||
// The package's own Markdown mail components are the shape `mail.markdown.paths` names: their
|
||||
// classes come from mail/theme.blade.php, a stylesheet Blade renders, not from a CSS entry.
|
||||
config(['mail.markdown.paths' => [$root.'/resources/views/mail']]);
|
||||
|
||||
expect(DesignGuard::scan($paths)->missingStylesheets($root.'/resources/css/all.css')->violations())->toBe([])
|
||||
// …and the stylesheet check is not vacuous there: the showcase's plain tags do need imports.
|
||||
->and(DesignGuard::scan($root.'/resources/views/showcase')->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))->violations())->not->toBe([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* No Tailwind-shaped class anywhere the package ships or tests itself: DesignGuard's own family
|
||||
* table (check (i)) is the standing definition of a Tailwind utility or variant, so this is that
|
||||
* guard turned on the package's own source. resources/views, resources/js and src are already
|
||||
* covered by the previous test's scan (workbench/resources is an application fixture and carries
|
||||
* Tailwind-shaped class names on purpose, so it is left out there too); this test adds what that
|
||||
* one doesn't reach: tests/Browser and tests/Feature. This file and tests/Fixtures/design-guard/
|
||||
* (the guard's own fixtures) carry Tailwind on purpose and are left out.
|
||||
*/
|
||||
it('writes no Tailwind-shaped class anywhere under tests/', function () {
|
||||
$root = realpath(__DIR__.'/../..');
|
||||
$designGuardTest = realpath(__DIR__.'/DesignGuardTest.php');
|
||||
|
||||
$paths = collect(['tests/Browser', 'tests/Feature'])
|
||||
->flatMap(fn (string $directory): Collection => collect(File::allFiles($root.'/'.$directory)))
|
||||
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'js', 'ts'], true))
|
||||
->reject(fn (SplFileInfo $file): bool => (string) $file->getRealPath() === $designGuardTest)
|
||||
->map(fn (SplFileInfo $file): string => (string) $file->getRealPath())
|
||||
->values()
|
||||
->all();
|
||||
|
||||
expect($paths)->not->toBeEmpty();
|
||||
|
||||
expect(DesignGuard::scan($paths)->violations())->toBe([]);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ it('refers to its own components through its namespace, which no prefix or app c
|
||||
// Comments document usage as an application writes it, and the showcase's examples
|
||||
// are that usage; neither is compiled as the package's own markup. An example heredoc
|
||||
// ends `BLADE,` as an array's value (most sections) or `BLADE;` assigned to its own
|
||||
// variable (a Layout page with a single example of its own, plan step 38).
|
||||
// variable (a Layout page with a single example of its own).
|
||||
$source = preg_replace(['/\{\{--.*?--\}\}/s', "/<<<'BLADE'.*?^\\s*BLADE[,;]/sm"], '', $file->getContents());
|
||||
|
||||
preg_match_all('/<\/?x-('.$names.')(?=[\s\/>])/', $source, $matches);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -61,3 +64,61 @@ it('publishes the mail components where Laravel looks for them', function () {
|
||||
expect(File::files(LivewireMaterialServiceProvider::mailComponentPath().'/html'))->not->toBeEmpty()
|
||||
->and(File::files(LivewireMaterialServiceProvider::mailComponentPath().'/text'))->not->toBeEmpty();
|
||||
});
|
||||
|
||||
/**
|
||||
* Pagination: the provider takes over Laravel's and Livewire's default theme when
|
||||
* `livewire-material.pagination` is on (the default), so `->links()` reaches the package's views
|
||||
* without either framework being told which view to render.
|
||||
*/
|
||||
it('sets Paginator::defaultView() to the package\'s renamed views, so a plain ->links() call renders them', function () {
|
||||
$pages = new LengthAwarePaginator(range(1, 10), 95, 10, 3, ['path' => '/shares']);
|
||||
|
||||
expect(Paginator::$defaultView)->toBe('pagination::material')
|
||||
->and(Paginator::$defaultSimpleView)->toBe('pagination::simple-material')
|
||||
->and((string) $pages->links())->toContain('data-md-pagination');
|
||||
});
|
||||
|
||||
it('takes over livewire.pagination_theme only while it still reads as Livewire\'s own default', function (?string $before, string $after) {
|
||||
if ($before === null) {
|
||||
// Livewire's own SupportPagination.php reads the key with a `'tailwind'` fallback, so a
|
||||
// missing key must be treated the same as an explicit `tailwind` — this is the "missing"
|
||||
// half of that. Config::offsetUnset() only sets the key to null (Repository::set()), so
|
||||
// the key is removed for real by rebuilding the array without it.
|
||||
config(['livewire' => Arr::except(config('livewire'), ['pagination_theme'])]);
|
||||
} else {
|
||||
config(['livewire.pagination_theme' => $before]);
|
||||
}
|
||||
|
||||
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
|
||||
(fn () => $this->registerPagination())->call($provider);
|
||||
|
||||
expect(config('livewire.pagination_theme'))->toBe($after);
|
||||
})->with([
|
||||
'Livewire\'s default, unset' => [null, 'material'],
|
||||
'Livewire\'s default, explicit' => ['tailwind', 'material'],
|
||||
'already material' => ['material', 'material'],
|
||||
'another theme, left alone' => ['bootstrap', 'bootstrap'],
|
||||
]);
|
||||
|
||||
it('changes neither the default views nor the pagination theme when the config turns pagination off', function () {
|
||||
$restoreView = Paginator::$defaultView;
|
||||
$restoreSimpleView = Paginator::$defaultSimpleView;
|
||||
|
||||
try {
|
||||
config(['livewire-material.pagination' => false, 'livewire.pagination_theme' => 'tailwind']);
|
||||
Paginator::defaultView('pagination::tailwind');
|
||||
Paginator::defaultSimpleView('pagination::simple-tailwind');
|
||||
|
||||
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
|
||||
(fn () => $this->registerPagination())->call($provider);
|
||||
|
||||
expect(config('livewire.pagination_theme'))->toBe('tailwind')
|
||||
->and(Paginator::$defaultView)->toBe('pagination::tailwind')
|
||||
->and(Paginator::$defaultSimpleView)->toBe('pagination::simple-tailwind');
|
||||
} finally {
|
||||
// Both are global statics on Laravel's own class, outliving this test's app instance —
|
||||
// left as 'tailwind' they would fail the file's other pagination tests that run after.
|
||||
Paginator::defaultView($restoreView);
|
||||
Paginator::defaultSimpleView($restoreSimpleView);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ use NoNameWeb\LivewireMaterial\Support\Scheme;
|
||||
use NoNameWeb\LivewireMaterial\Support\SchemeStylesheet;
|
||||
|
||||
/**
|
||||
* The showcase's own stylesheet route (plan step 38): `all.css` plus `showcase.css`, prebuilt into
|
||||
* The showcase's own stylesheet route: `all.css` plus `showcase.css`, prebuilt into
|
||||
* resources/dist/showcase.css and served without the application's Vite build, and the two package
|
||||
* folders its relative `url()`s point into.
|
||||
*/
|
||||
|
||||
@@ -34,27 +34,6 @@ it('resolves the files an entry imports, transitively, without bundling any cont
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves past what an application entry imports and only a Vite build resolves, without throwing', function () {
|
||||
$dir = sys_get_temp_dir().'/livewire-material-resolved-'.Str::random(8);
|
||||
File::makeDirectory($dir);
|
||||
File::put($dir.'/theme.css', '.theme {}');
|
||||
File::put($dir.'/app.css', implode("\n", [
|
||||
"@import 'tailwindcss';",
|
||||
"@import url('https://fonts.googleapis.com/css2?family=Roboto');",
|
||||
"@import './theme.css' layer(app);",
|
||||
"@import './missing.css';",
|
||||
"@import '".stylesheetsBundlePath('components/icon.css')."';",
|
||||
]));
|
||||
|
||||
try {
|
||||
$files = Stylesheets::resolvedFiles([$dir.'/app.css']);
|
||||
} finally {
|
||||
File::deleteDirectory($dir);
|
||||
}
|
||||
|
||||
expect($files)->toBe([realpath(sys_get_temp_dir()).'/'.basename($dir).'/app.css', realpath(sys_get_temp_dir()).'/'.basename($dir).'/theme.css']);
|
||||
});
|
||||
|
||||
/**
|
||||
* Deduplication keeps a file at its first position, so an override that ties a rule it imports on
|
||||
* specificity (the navigation rail's header FAB over fab.css was one) still wins only while every
|
||||
@@ -170,11 +149,12 @@ function stylesheetsBundleLeafRules(string $css): array
|
||||
*
|
||||
* The built CSS is minified, so this compares rules rather than files: no innermost rule may
|
||||
* appear twice under the same at-rules. A build that inlined every occurrence of a shared
|
||||
* stylesheet repeats hundreds of them (the Workbench's entry did while it shared one file with
|
||||
* `@tailwindcss/vite`: button.css's hover and disabled rules thirteen times), while a file's plain
|
||||
* root rule alone is no evidence — the minifier folds identical copies of that one together. In
|
||||
* the application entry, button.css must also keep its first position: before split-button.css,
|
||||
* the first file that imports it, not at the end where the application named it again.
|
||||
* stylesheet repeats hundreds of them (the Workbench's entry did while `@tailwindcss/vite` was
|
||||
* still in its graph, inlining imports itself ahead of Vite's own postcss-import: button.css's
|
||||
* hover and disabled rules thirteen times), while a file's plain root rule alone is no evidence —
|
||||
* the minifier folds identical copies of that one together. In the application entry, button.css
|
||||
* must also keep its first position: before split-button.css, the first file that imports it, not
|
||||
* at the end where the application named it again.
|
||||
*/
|
||||
it('lands every rule once, at its first position, in a real Vite build of all.css and of an application entry', function () {
|
||||
$node = config('livewire-material.node', 'node');
|
||||
|
||||
@@ -5,14 +5,13 @@ use NoNameWeb\LivewireMaterial\Support\Layout;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
|
||||
/**
|
||||
* The package's stylesheets are plain CSS in `material.*` layers, with no Tailwind in them. Every
|
||||
* one opens with the same layer statement, so the order holds whichever file a bundler reaches
|
||||
* first; then plain imports (an `@import` cannot sit inside a layer block, and `layer()` on it
|
||||
* 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.
|
||||
* The package's stylesheets are plain CSS in `material.*` layers. Every one opens with the same
|
||||
* layer statement, so the order holds whichever file a bundler reaches first; then plain imports
|
||||
* (an `@import` cannot sit inside a layer block, and `layer()` on it 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.
|
||||
*
|
||||
* Every package component and layout stylesheet is plain CSS now (plan step 36), and Tailwind
|
||||
* itself is gone from the package (plan step 39), so the shape, Tailwind-free and breakpoint checks
|
||||
* Every package component and layout stylesheet is plain CSS, so the shape and breakpoint checks
|
||||
* below run over the whole tree `all.css` reaches (foundation, layout and components together)
|
||||
* rather than the foundation alone, and — one file it does not reach — `showcase.css`, checked on
|
||||
* its own further down. A component's own values (colours as roles, shadows as elevation levels,
|
||||
@@ -64,7 +63,7 @@ function stylesheetImports(string $file): array
|
||||
|
||||
/**
|
||||
* A stylesheet and every stylesheet it reaches through `@import`, each once, in the order a
|
||||
* bundler meets them. `tailwindcss` itself is a package, not a file, and is left out.
|
||||
* bundler meets them. An import that is not relative (a package name, a URL) is left out.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
@@ -105,9 +104,7 @@ function foundationStylesheets(): array
|
||||
|
||||
/**
|
||||
* Everything all.css reaches: the foundation, every layout stylesheet and every component
|
||||
* stylesheet, each once. Every one of them is plain CSS (plan step 36), and Tailwind itself is
|
||||
* gone from the package (plan step 39) — `tailwindcss`, `tailwind.css` and its two token files no
|
||||
* longer exist, so there is nothing left for this tree to exclude.
|
||||
* stylesheet, each once. Every one of them is plain CSS.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
@@ -131,8 +128,7 @@ it('brings every foundation file and text.css into the foundation, and nothing e
|
||||
expect($parts->diff($foundation)->values()->all())->toBe([])
|
||||
->and(array_map(fn (string $import): string => basename($import), stylesheetImports(stylesheetPath('foundation.css'))))
|
||||
// In the order of their layers, the rules outside every layer beside the reset — the seven
|
||||
// token files imported directly (plan step 33: foundation/tokens.css held only these
|
||||
// imports, and only foundation.css reached it, so it was folded in here).
|
||||
// token files imported directly.
|
||||
->toBe(['reset.css', 'hidden.css', 'scheme.css', 'shape.css', 'elevation.css', 'motion.css', 'type.css', 'state.css', 'spacing.css', 'base.css', 'interaction.css', 'text.css'])
|
||||
->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')
|
||||
@@ -140,8 +136,8 @@ it('brings every foundation file and text.css into the foundation, and nothing e
|
||||
});
|
||||
|
||||
it('paints the page in surface and on-surface, in the brand typeface, smoothed in grayscale', function () {
|
||||
// An application drops Tailwind's `antialiased` with nothing to replace it: the showcase and the
|
||||
// error pages, which set it themselves before, read it from here too.
|
||||
// An application gets it with no class of its own needed: the showcase and the error pages,
|
||||
// which set it themselves before, read it from here too.
|
||||
$base = (string) preg_replace('~/\*.*?\*/~s', '', (string) file_get_contents(stylesheetPath('foundation/base.css')));
|
||||
|
||||
expect($base)->toMatch('/html \{\s*background-color: var\(--md-sys-color-surface\);\s*color: var\(--md-sys-color-on-surface\);\s*font-family: var\(--md-ref-typeface-brand\);\s*-webkit-font-smoothing: antialiased;\s*-moz-osx-font-smoothing: grayscale;\s*\}/')
|
||||
@@ -163,9 +159,9 @@ it('names the monospace faces once, as a typeface token the reset, the field and
|
||||
});
|
||||
|
||||
it('leaves no stylesheet under resources/css/ outside all.css or showcase.css', function () {
|
||||
// The full tree now that Tailwind is gone (plan step 39): every .css file on disk, not just the
|
||||
// ones a bundler happens to reach — a stray or orphaned file would show up here even though
|
||||
// nothing imports it, which the checks above (walking @import from all.css) cannot catch.
|
||||
// The full tree: every .css file on disk, not just the ones a bundler happens to reach — a
|
||||
// stray or orphaned file would show up here even though nothing imports it, which the checks
|
||||
// above (walking @import from all.css) cannot catch.
|
||||
$onDisk = collect(File::allFiles(stylesheetPath()))
|
||||
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'css')
|
||||
->map(fn (SplFileInfo $file): string => stylesheetName((string) $file->getRealPath()))
|
||||
@@ -257,15 +253,7 @@ it('defines x-cloak once, reached from both the foundation and all.css', functio
|
||||
->and(allStylesheets())->toContain(stylesheetPath('foundation/hidden.css'));
|
||||
});
|
||||
|
||||
it('writes no Tailwind directive anywhere all.css reaches', function () {
|
||||
foreach (allStylesheets() as $file) {
|
||||
expect(ComponentStylesheet::withoutComments(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));
|
||||
}
|
||||
});
|
||||
|
||||
// No Tailwind-shaped class anywhere the package ships or tests itself is
|
||||
// A class no stylesheet declares, anywhere the package ships or tests itself, is
|
||||
// tests/Feature/DesignGuardTest.php's: one of its tests scans resources/views, resources/js, src
|
||||
// and the Workbench, another scans tests/Browser and tests/Feature, so nothing here repeats
|
||||
// either.
|
||||
@@ -309,6 +297,39 @@ it('writes a media query, anywhere all.css reaches, only at M3\'s breakpoints, i
|
||||
}
|
||||
});
|
||||
|
||||
it('writes every matchMedia width in resources/js at an M3 breakpoint', function () {
|
||||
// The one check BreakpointsTest.php (deleted) covered that nothing else did: a script that
|
||||
// widens or narrows window.matchMedia() by hand, at a width M3 does not know, rather than
|
||||
// asking resources/js/breakpoints.js for the number. Isolated the same way that test isolated
|
||||
// a media query's own condition — only the text inside matchMedia()'s call, up to its first
|
||||
// closing quote or backtick — so a real width sitting elsewhere in the file, in a plain
|
||||
// declaration rather than a query condition (a `min(40rem, 70dvh)`-style cap; none exists in
|
||||
// resources/js today), is never mistaken for a breakpoint.
|
||||
$files = collect(File::allFiles(__DIR__.'/../../resources/js'))
|
||||
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'js');
|
||||
|
||||
expect($files)->not->toBeEmpty();
|
||||
|
||||
foreach ($files as $file) {
|
||||
preg_match_all('/matchMedia\(\s*[\'"`][^\'"`]*/', $file->getContents(), $conditions);
|
||||
|
||||
foreach ($conditions[0] as $condition) {
|
||||
preg_match_all('/(\d*\.?\d+)(rem|px)/i', $condition, $lengths, PREG_SET_ORDER);
|
||||
|
||||
foreach ($lengths as [, $number, $unit]) {
|
||||
// breakpoints.js itself never writes a literal number — from() and upTo() build
|
||||
// the string from the shared table (`${width(name)}px`) — so today's tree has none
|
||||
// to check; this only guards whichever file writes one by hand next.
|
||||
$allowed = strtolower($unit) === 'px' ? ['600', '840', '1200', '1600'] : ['37.5', '52.5', '75', '100'];
|
||||
|
||||
expect(in_array($number, $allowed, true))->toBeTrue(
|
||||
$file->getRelativePathname().': '.trim($condition).' is not at an M3 breakpoint'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('declares M3\'s spacing scale as the reference gives it', function () {
|
||||
preg_match_all(
|
||||
'/^\|\s*\**space(\d+)\**\s*\|\s*\**([\d.]+)×\**\s*\|\s*\**(\d+)dp/m',
|
||||
@@ -526,10 +547,10 @@ it('imports, from every layout stylesheet, the stylesheet of each component its
|
||||
expect($checked)->toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// 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.
|
||||
// Every layout stylesheet's header, layer statement and plain-imports shape, and its media
|
||||
// queries 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
|
||||
@@ -607,10 +628,6 @@ it('hides an element below and from every breakpoint but compact, and nothing el
|
||||
});
|
||||
|
||||
it('gives every md-type class exactly font, letter-spacing and font-variation-settings', function () {
|
||||
// Tailwind's tokens/utilities.css used to declare the same 30 styles as `type-*` utility
|
||||
// blocks, and this test proved text.css's `.md-type-*` classes matched them; deleted with
|
||||
// Tailwind (plan step 39), text.css is the only copy left, so the shape it gives each class is
|
||||
// asserted directly rather than cross-checked against a second copy.
|
||||
preg_match_all('/\.md-(type-[\w-]+) \{\n(.*?)\n {4}\}/s', File::get(stylesheetPath('text.css')), $matches, PREG_SET_ORDER);
|
||||
|
||||
$classes = collect($matches)->mapWithKeys(fn (array $match): array => ["md-{$match[1]}" => ComponentStylesheet::flatDeclarations($match[2])]);
|
||||
@@ -672,27 +689,16 @@ it('ships exactly the documented text classes, each ink an M3 role', function ()
|
||||
}
|
||||
});
|
||||
|
||||
// A test used to prove foundation/interaction.css's `.md-*` classes matched tokens/utilities.css's
|
||||
// utility blocks byte for byte apart from two deliberate differences, because the components
|
||||
// still written in Tailwind read the utilities while every other component read the classes.
|
||||
// Deleted with Tailwind (plan step 39): every component reads the classes now, there is no second
|
||||
// copy left to drift from, and interaction.css's own StylesheetsTest coverage above (the layer it
|
||||
// writes into, no Tailwind directive) already holds it to the same shape every other foundation
|
||||
// file keeps to.
|
||||
|
||||
it('builds the package in the Workbench\'s one CSS entry, with no Tailwind left to sit between', function () {
|
||||
it('opens the Workbench\'s one CSS entry with the layer statement, then all.css, showcase.css and the scheme', function () {
|
||||
$app = ComponentStylesheet::withoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css'));
|
||||
|
||||
// Opens by ordering the layers, so the order holds whichever the page links first — an
|
||||
// application with Tailwind of its own still opens its own entry the same way (its header).
|
||||
// Opens by ordering the layers, so the order holds whichever the page links first.
|
||||
expect(trim($app))->toStartWith(MATERIAL_LAYER_STATEMENT)
|
||||
->toContain("@import '../../../resources/css/all.css';")
|
||||
->toContain("@import '../../../resources/css/showcase.css';")
|
||||
->toContain("@import './material-scheme.css';")
|
||||
->not->toMatch('/@(?:tailwind|source|theme|utility|variant|custom-variant|apply|config|plugin|reference)\b/')
|
||||
->and(File::get(__DIR__.'/../../vite.config.js'))
|
||||
->toContain("input: ['workbench/resources/css/app.css', 'workbench/resources/js/app.js']")
|
||||
->not->toContain('tailwindcss')
|
||||
->and(config('livewire-material.showcase.vite'))->toBe([
|
||||
'workbench/resources/css/app.css',
|
||||
'workbench/resources/js/app.js',
|
||||
@@ -705,10 +711,7 @@ it('shapes showcase.css like a package stylesheet, with its documented unlayered
|
||||
$items = ComponentStylesheet::items(ComponentStylesheet::withoutComments($css));
|
||||
|
||||
expect($css)->toStartWith('/*', 'showcase.css has no header comment')
|
||||
->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, 'showcase.css does not open with the layer statement')
|
||||
->and(ComponentStylesheet::withoutComments($css))
|
||||
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/')
|
||||
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/');
|
||||
->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, 'showcase.css does not open with the layer statement');
|
||||
|
||||
$blocks = false;
|
||||
$unlayered = [];
|
||||
|
||||
@@ -39,8 +39,7 @@ function typeStyles(): array
|
||||
|
||||
/**
|
||||
* The body of every `.md-type-…` class in text.css, keyed by class name without the `md-` prefix
|
||||
* (`type-display-lg`, `type-emphasized-display-lg`, …). Plain CSS now (plan step 39 deleted the
|
||||
* Tailwind utilities they used to be); text.css is theirs alone.
|
||||
* (`type-display-lg`, `type-emphasized-display-lg`, …). Plain CSS; text.css is theirs alone.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -86,14 +85,9 @@ it('ships M3\'s medium and high contrast levels beside the standard one', functi
|
||||
}
|
||||
});
|
||||
|
||||
// Tailwind's theme.css used to turn every scheme role into a `--color-*` Tailwind colour, in an
|
||||
// inline theme block so it resolved on the element and a nested data-theme still repainted it
|
||||
// rather than reading a value computed once on :root. Both went with Tailwind (plan step 39): the
|
||||
// first fact is already the scheme test above (every role a `--md-sys-color-*` custom property,
|
||||
// light and dark); the second no longer has anything to hold, because no file replaces theme.css's
|
||||
// alias — every package rule reads `--md-sys-color-*` directly at its point of use, so there is no
|
||||
// second custom property that could be computed once on :root and go stale under a nested
|
||||
// data-theme in the first place.
|
||||
// Every package rule reads a role's `--md-sys-color-*` custom property directly at its point of
|
||||
// use: no second alias computed once on `:root` that a nested `data-theme` could leave stale. The
|
||||
// scheme test above already proves every role is `--md-sys-color-*`, light and dark.
|
||||
it('never aliases a colour role to a second custom property resolved once on :root', function () {
|
||||
foreach (File::allFiles(packageCss()) as $file) {
|
||||
expect($file->getContents())->not->toMatch('/--color-[\w-]+:\s*var\(--md-sys-color-/', $file->getRelativePathname());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* An application's own CSS entry, the way plan step 37's claim is about: outside the package, the
|
||||
* foundation first, then the stylesheet of each component its views render. Split button, app bar,
|
||||
* modal and pagination each import button.css (and icon.css, through it), and the application
|
||||
* names button.css itself once more at the end. Built by tests/Fixtures/dedup.vite.config.mjs for
|
||||
* StylesheetsBundleTest.php, which expects every shared rule once, at its first position.
|
||||
* An application's own CSS entry: outside the package, the foundation first, then the stylesheet
|
||||
* of each component its views render. Split button, app bar, modal and pagination each import
|
||||
* button.css (and icon.css, through it), and the application names button.css itself once more
|
||||
* at the end. Built by tests/Fixtures/dedup.vite.config.mjs for StylesheetsBundleTest.php, which
|
||||
* expects every shared rule once, at its first position.
|
||||
*/
|
||||
|
||||
@import '../../resources/css/foundation.css';
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// Builds two CSS entries with no plugin in the graph, isolating Vite's own bundled postcss-import,
|
||||
// whose `skipDuplicates` option is the mechanism docs/plans/material-3-alignment.md's "Tailwind's
|
||||
// footprint" paragraph measured and StylesheetsBundleTest.php pins: resources/css/all.css, and
|
||||
// tests/Fixtures/dedup-app.css, an application-shaped entry importing per-component stylesheets
|
||||
// that share button.css. A stylesheet several files import lands once per entry, at its first
|
||||
// position.
|
||||
// whose `skipDuplicates` option is the deduplication StylesheetsBundleTest.php pins:
|
||||
// resources/css/all.css, and tests/Fixtures/dedup-app.css, an application-shaped entry importing
|
||||
// per-component stylesheets that share button.css. A stylesheet several files import lands once
|
||||
// per entry, at its first position.
|
||||
//
|
||||
// Not the Workbench's config: `@tailwindcss/vite` inlines the imports of any entry that uses
|
||||
// Tailwind itself, without that deduplication, so a shared stylesheet repeats there. The Workbench
|
||||
// keeps the package's CSS in an entry of its own (workbench/resources/css/package.css) for that
|
||||
// reason, until plan step 39 removes Tailwind.
|
||||
// Not the Workbench's config: this one carries no plugin, which is what isolates Vite's own
|
||||
// postcss-import above. The Workbench's config carries laravel-vite-plugin, so its build is not
|
||||
// this dedup test's to run.
|
||||
//
|
||||
// DEDUP_OUT_DIR is required: the test builds into a temporary directory and deletes it, and nothing
|
||||
// should land in the package's own tree, least of all workbench/public/build.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="sm:flex md:grid lg:hidden xl:block 2xl:contents">
|
||||
<span class="max-sm:hidden max-md:flex max-lg:grid max-xl:block max-2xl:contents"></span>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
<div class="bg-[#1d7afc] text-[rgb(0_0_0)] border-[hsl(210_80%_50%)]">
|
||||
<span class="fill-[oklch(0.7_0.1_250)] ring-[color-mix(in_oklab,var(--x)_50%,transparent)]"></span>
|
||||
<span class="bg-white text-black border-white"></span>
|
||||
<span class="text-on-surface/60 bg-on-surface/12 border-outline/38"></span>
|
||||
<span class="md-ink md-type-body-md">M3's own are fine</span>
|
||||
</div>
|
||||
+4
@@ -5,3 +5,7 @@
|
||||
.bg-brand {
|
||||
background: var(--brand);
|
||||
}
|
||||
|
||||
.dashboard-hero {
|
||||
padding: var(--md-sys-measurement-space200);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div x-data="{ view: 'grid' }" :class="{ 'is-open': open, collapsed: !open, 'md-ink': view === 'grid' }" wire:loading.class="dashboard-busy"
|
||||
x-transition:enter="dashboard-enter" x-transition:enter-start="dashboard-enter-start">
|
||||
<ul @class(['dashboard-hero', 'share-list', 'md-ink-quiet' => $status === 'hidden', 'md-ink' => isActive('block')])
|
||||
{{ $attributes->merge(['class' => 'dashboard-lead']) }}>
|
||||
<li class="md-truncate
|
||||
share-list">Row</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
{{-- Real application lines: a quoted string that is an array index ($block['base'],
|
||||
$header['class'], $tab['iconClass']) reads a value, not a class list. The keys around it and
|
||||
both branches of a ternary are classes, and are read. --}}
|
||||
<div @class(['product-shot-base' => $block['base']])></div>
|
||||
<x-sort-header :sort-by="$sortBy" :column="$header['key']" :class="$header['class'] ?? null" />
|
||||
<x-icon :name="$tab['icon']" size="20" :class="$tab['iconClass']" />
|
||||
<x-row :class="$band['easy'] ? 'md-type-title-sm' : 'md-type-body-md'" />
|
||||
<div @class(['routes-show-run-chosen' => $referenceRunId === $run['id']])></div>
|
||||
<x-badge :class="\Illuminate\Support\Arr::toCssClasses(['shot-helper-one', 'shot-helper-two' => $on])" />
|
||||
<span :class="$on ? 'shot-tern-a' : ($off ? 'shot-tern-b' : 'shot-tern-c')"></span>
|
||||
@@ -0,0 +1 @@
|
||||
<p class="md-type-title-md md-ink-variant showcase-w-narrow">Every one of these is a package rule.</p>
|
||||
@@ -1,4 +0,0 @@
|
||||
{{-- Running text is not a class list: none of Tailwind's bare-word utilities should be read out
|
||||
of prose just because they share a spelling with one. --}}
|
||||
<p>This creates a grid of cards, in a flex layout that stays hidden below medium and stacks into a
|
||||
block once the pane narrows. Nothing here truncates or grows, and the sample is not a table.</p>
|
||||
@@ -1,5 +0,0 @@
|
||||
<div class="rounded-none rounded-xs rounded-sm rounded-md rounded-lg">
|
||||
<span class="rounded-xl rounded-2xl rounded-3xl rounded-4xl rounded-full"></span>
|
||||
<span class="rounded-t-lg rounded-se-2xl"></span>
|
||||
<span class="md-type-body-md" style="border-radius: var(--md-sys-shape-corner-lg)">M3's own are fine</span>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class="shadow-2xs shadow-xs shadow-sm shadow-md">
|
||||
<span class="shadow-lg shadow-xl shadow-2xl"></span>
|
||||
<span class="md-type-body-md" style="box-shadow: var(--md-sys-elevation-1)">M3's own are fine</span>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class="ease-linear ease-in ease-out ease-in-out">
|
||||
<span class="duration-75 duration-300 duration-1000"></span>
|
||||
<span class="md-type-body-md">M3's own are fine</span>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
// A plain CSS keyword read out of an API, not a Tailwind class: matches DesignGuard's easing and
|
||||
// duration families only in a class list, never a bare JS string like this one.
|
||||
const easing = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'linear' : 'ease-out';
|
||||
const duration = matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 300;
|
||||
@@ -1,7 +0,0 @@
|
||||
<div class="text-xs text-sm text-base text-lg text-xl text-2xl text-9xl">
|
||||
<span class="font-thin font-extralight font-light font-normal font-medium"></span>
|
||||
<span class="font-semibold font-bold font-extrabold font-black"></span>
|
||||
<span class="leading-none leading-tight leading-snug leading-normal leading-relaxed leading-loose leading-6"></span>
|
||||
<span class="tracking-tighter tracking-tight tracking-normal tracking-wide tracking-wider tracking-widest"></span>
|
||||
<span class="md-type-body-md md-type-title-lg md-type-emphasized-label-lg">M3's own are fine</span>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@fontsource/roboto';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto');
|
||||
@import './app-theme.css' layer(app);
|
||||
@import '../../../../resources/css/foundation.css';
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class="top-[10px] grid-cols-[1fr_2fr]">
|
||||
<span class="flex!"></span>
|
||||
</div>
|
||||
<div class="[--material-bottom-bar:5rem] [font-variation-settings:'ROND'_100]"></div>
|
||||
@@ -1,8 +0,0 @@
|
||||
<div x-data="{ view: 'grid' }" :class="{ 'is-open': open, hidden: !open, 'md-ink': view === 'grid' }" wire:loading.class="opacity-50"
|
||||
x-transition:enter="transition ease-out" x-transition:enter-start="scale-95">
|
||||
<ul @class(["gap-2", 'share-list', 'md-ink-quiet' => $status === 'hidden', 'md-ink' => isActive('block')])
|
||||
{{ $attributes->merge(['class' => 'mt-2']) }}>
|
||||
<li class="md-truncate
|
||||
flex">Row</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
<span class="duration-(--md-sys-motion-spatial-fast-duration)"></span>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="hidden block">
|
||||
<span class="inline-block invisible"></span>
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
<span class="text-on-primary-container bg-background text-current md-ink-variant"></span>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="flex grid inline-flex flex-col">
|
||||
<span class="grid-cols-3 col-span-2 items-center justify-between gap-4"></span>
|
||||
</div>
|
||||
@@ -1,2 +0,0 @@
|
||||
<div class="text-on-surface-variant border-outline-variant"></div>
|
||||
<span class="bg-surface-container bg-primary"></span>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="w-full h-screen min-w-0 max-w-md">
|
||||
<span class="size-4"></span>
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="p-4 px-2 mt-2 space-y-4">
|
||||
<span class="m-4 space-x-2"></span>
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
<p class="text-center truncate">
|
||||
<span class="sr-only whitespace-nowrap line-clamp-2 tabular-nums"></span>
|
||||
</p>
|
||||
@@ -1,8 +0,0 @@
|
||||
<div class="bg-off-plan text-sport-run from-brand to-brand-end text-brand-ink">
|
||||
<span class="border-route-reference bg-route-reference/8 border-l-zone-4 ring-offset-brand/[0.4]"></span>
|
||||
<p class="text-balance text-pretty text-wrap text-nowrap text-ellipsis text-clip"></p>
|
||||
<table class="border-collapse border-separate border-solid border-dashed border-dotted border-none"></table>
|
||||
<span class="shadow-none outline-none outline-dashed ring-inset decoration-wavy from-0% via-none"></span>
|
||||
<span class="bg-clip-text bg-fixed bg-cover bg-center bg-no-repeat bg-top-left text-inherit text-shadow-sm fill-none accent-auto"></span>
|
||||
<span class="bg-mauve-500 border-bs-2"></span>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class="relative inset-0 -top-2 z-10 overflow-hidden shrink-0 col-start-2 justify-self-end container">
|
||||
<span class="border border-t-2 rounded shadow outline-none ring-2 transition opacity-50 -rotate-45 cursor-pointer select-none"></span>
|
||||
<span class="aspect-video object-cover font-mono font-sans antialiased underline uppercase text-end list-disc table bg-clip-text"></span>
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="hover:underline dark:opacity-50">
|
||||
<span class="group-hover:flex focus:md:gap-2"></span>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
<x-icon name="o-home" />
|
||||
<x-button icon="calendar_month" icon-right="not_a_symbol" label="Fine" />
|
||||
<span>text-red-600</span>
|
||||
<p class="text-tertiary focus-ring">Tailwind's and ours</p>
|
||||
<p class="text-tertiary focus-ring">Classes no stylesheet declares</p>
|
||||
<x-icon :name="$dynamic" />
|
||||
<x-icon name="home" @class(['size-4']) />
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ if (($timeout = (int) getenv('BROWSER_TIMEOUT')) > 0) {
|
||||
/**
|
||||
* The named block of resources/css/all.css — the content between its `/* Heading *\/` comment and
|
||||
* the next one, or the end of the file for the last block ("Navigation"). components.css and
|
||||
* layout.css grouped their imports the same way before plan step 37 folded both into all.css; this
|
||||
* layout.css grouped their imports the same way before all.css folded both together; this
|
||||
* is the one place that block lookup lives now, so a stylesheet test asks "is this imported from
|
||||
* the Containment block" without repeating the search in every file that needs it. It is here,
|
||||
* rather than in tests/Feature/StylesheetsTest.php, because Pest.php is always loaded, whichever
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
namespace NoNameWeb\LivewireMaterial\Tests\Support;
|
||||
|
||||
/**
|
||||
* The classes a package view rewritten without Tailwind may write (plan step 36; the brief's
|
||||
* "Interaction is the shared classes"): the three interaction classes from
|
||||
* foundation/interaction.css, the fixed text classes resources/css/text.css defines (plan step 34,
|
||||
* "The component views use the same set"), and a caller's own class handed on whole. Everything
|
||||
* else a view draws comes from its `data-md-*` hooks.
|
||||
* The classes a package view may write: the three interaction classes from
|
||||
* foundation/interaction.css, the fixed text classes resources/css/text.css defines, and a
|
||||
* caller's own class handed on whole. Everything else a view draws comes from its `data-md-*`
|
||||
* hooks.
|
||||
*
|
||||
* One rule for every group's stylesheet test, so the groups cannot drift apart.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user