diff --git a/src/Testing/DesignGuard.php b/src/Testing/DesignGuard.php index a42206cf..aff72808 100644 --- a/src/Testing/DesignGuard.php +++ b/src/Testing/DesignGuard.php @@ -133,6 +133,14 @@ class DesignGuard ['/^shadow-elevation-(?[0-5])$/', '1.x utility `%s` compiles to nothing — use `var(--md-sys-elevation-{level})` in your own CSS'], ['/^ease-(?(?:spatial|effects)-(?:fast|default|slow))$/', '1.x utility `%s` compiles to nothing — pair `var(--md-sys-motion-{easing})` with `var(--md-sys-motion-{easing}-duration)` in your own `transition`'], ['/^ease-(?standard|emphasized)(?-accelerate|-decelerate)?$/', '1.x utility `%s` compiles to nothing — use `var(--md-sys-motion-easing-{easing}{phase})` in your own `transition`'], + // Tailwind's cleared easing and duration scale: matched only here, in a class list a + // Blade or PHP file writes literally (never line-by-line across every file, the way + // outsideTheScale() still reads its other scale steps) — a plain PHP or JS string such as + // `matchMedia(…) ? 'linear' : 'ease-out'` names a real CSS keyword, not a Tailwind class, + // and reading every line for the bare word flagged both that and the word appearing inside + // a stylesheet test's own regex literal (`ease-in`, `ease-out`) as if it were one. + ['/^ease-(?:in-out|linear|in|out)$/', 'value outside the M3 scale `%s` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`'], + ['/^duration-\d+$/', 'value outside the M3 scale `%s` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`'], ['/^[a-z][a-z-]*-\((?--[\w-]+)\)$/', 'Tailwind custom-property utility `%s` compiles to nothing — write `var({property})` in your own CSS'], ['/^(?:static|fixed|absolute|relative|sticky)$/', "Tailwind position utility `%s` compiles to nothing — write `position` in your own CSS (a FAB goes in ``'s `fab` slot)"], @@ -566,8 +574,9 @@ class DesignGuard } /** - * The radius, shadow, type-size, weight, leading, tracking, easing and duration utilities - * Tailwind shipped and M3's own scales replace. + * The radius, shadow, type-size, weight, leading and tracking utilities Tailwind shipped and + * M3's own scales replace. The easing and duration steps of the same Tailwind scale are not + * read here — see the class header's note by the `ease-*`/`duration-*` entries of `FAMILIES`. * * @return list */ @@ -581,8 +590,6 @@ class DesignGuard .'|font-(?thin|extralight|light|normal|medium|semibold|bold|extrabold|black)' .'|leading-(?none|tight|snug|normal|relaxed|loose|\d+(?:\.\d+)?)' .'|tracking-(?tighter|tight|normal|wider|widest|wide)' - .'|ease-(?in-out|linear|in|out)' - .'|duration-(?\d+)' .')(?![\w-])/'; preg_match_all($pattern, $text, $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL); @@ -606,8 +613,6 @@ class DesignGuard isset($match['corner']) => 'use `var(--md-sys-shape-corner-'.self::CORNERS[$match['corner']].')` in your own CSS, or ``', isset($match['elevation']) => 'use `var(--md-sys-elevation-'.self::ELEVATIONS[$match['elevation']].')` in your own CSS', isset($match['weight']) => 'use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight', - isset($match['easing']) => 'pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`', - isset($match['duration']) => 'pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`', default => 'use one of the `md-type-*` classes (text.css), which set size, line height and tracking together', }; } diff --git a/tests/Feature/DesignGuardTest.php b/tests/Feature/DesignGuardTest.php index d1c19161..5931f3fc 100644 --- a/tests/Feature/DesignGuardTest.php +++ b/tests/Feature/DesignGuardTest.php @@ -150,6 +150,16 @@ it('sends an easing and a duration to the motion tokens', function () { ]); }); +it('leaves an easing or a duration word alone in a JavaScript string outside a class list', function () { + // 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). + $violations = DesignGuard::scan(realpath(GUARD_FIXTURES.'/scale/motion.js'))->violations(); + + expect($violations)->toBe([]); +}); + it('finds a colour written as a value', function () { expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->violations()))->toBe([ 'colour/badge.blade.php:1 arbitrary colour `bg-[#1d7afc]`, use an M3 role', diff --git a/tests/Fixtures/design-guard/scale/motion.js b/tests/Fixtures/design-guard/scale/motion.js new file mode 100644 index 00000000..b5851273 --- /dev/null +++ b/tests/Fixtures/design-guard/scale/motion.js @@ -0,0 +1,4 @@ +// 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;