diff --git a/src/Testing/DesignGuard.php b/src/Testing/DesignGuard.php index aff72808..58aab999 100644 --- a/src/Testing/DesignGuard.php +++ b/src/Testing/DesignGuard.php @@ -923,7 +923,9 @@ class DesignGuard $value = trim($value); $line = substr_count(substr($css, 0, $offset), "\n") + 1; - if (isset(self::LITERAL_PROPERTIES[$property]) && ! $this->isSafeLiteralValue($value)) { + $isRing = $property === 'box-shadow' && $this->isSafeBoxShadowRing($value); + + if (isset(self::LITERAL_PROPERTIES[$property]) && ! $this->isSafeLiteralValue($value) && ! $isRing) { $config = self::LITERAL_PROPERTIES[$property]; $found[] = [$line, "literal {$config['kind']} `{$property}: {$value}` — {$config['hint']}"]; } @@ -932,7 +934,11 @@ class DesignGuard continue; } - if (preg_match('/cubic-bezier\(|steps\(|(?px` in a `var(--md-sys-color-*)`, or one mixed toward transparent for a disabled ring + * (`color-mix(in srgb, var(--md-sys-color-*) , transparent)`) — the shape the package's own + * stylesheets (and their stylesheet tests) use for a day's or a year's "current" outline, a + * focused field's edge, a selected chip's border: legitimate M3 CSS, not a hand-made shadow. + * `$value` has already had every `var()`/`calc()` call's own arguments blanked (see + * `withoutTokenFunctions()`), so the colour itself is read here only as an empty shell. + */ + protected function isSafeBoxShadowRing(string $value): bool + { + return preg_match( + '/^(?:inset\s+)?0\s+0\s+0\s+\d+(?:\.\d+)?px\s+(?:var\([ \t]*\)|color-mix\(in srgb,\s*var\([ \t]*\)[^,]*,\s*transparent\s*\))$/', + trim($value), + ) === 1; + } + /** * Every width in an `@media` condition that is not one of M3's four breakpoints written in * px: `(width >= 840px)`, `(min-width: 840px)` and `(600px <= width < 840px)` pass, `(width > diff --git a/tests/Feature/DesignGuardTest.php b/tests/Feature/DesignGuardTest.php index 5931f3fc..9097bf30 100644 --- a/tests/Feature/DesignGuardTest.php +++ b/tests/Feature/DesignGuardTest.php @@ -540,6 +540,17 @@ it('finds every literal design value and off-scale media query in the applicatio ]); }); +it('leaves a box-shadow ring in a colour role alone, but still reports a blurred or literal-coloured one', function () { + $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/shadows/rings.css'))->violations()); + + expect($violations)->toBe([ + 'shadows/rings.css:16 literal colour `rgba(0, 0, 0, 0.2)` — use `var(--md-sys-color-*)`', + 'shadows/rings.css:16 literal shadow `box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2)` — use `var(--md-sys-elevation-*)`', + 'shadows/rings.css:20 literal colour `#000` — use `var(--md-sys-color-*)`', + 'shadows/rings.css:20 literal shadow `box-shadow: inset 0 0 4px 1px #000` — use `var(--md-sys-elevation-*)`', + ]); +}); + it('ignores the generated material-scheme.css, by name and by its generated header, wherever it sits among the scanned paths', function () { $withScheme = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/app-css'))->violations()); $withoutScheme = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/app-css/app.css'))->violations()); diff --git a/tests/Fixtures/design-guard/shadows/rings.css b/tests/Fixtures/design-guard/shadows/rings.css new file mode 100644 index 00000000..fb8e5da4 --- /dev/null +++ b/tests/Fixtures/design-guard/shadows/rings.css @@ -0,0 +1,21 @@ +/* A ring in a colour role (a focused field's edge, a day's "current" outline), and the two shapes + check (iii) still reports: a blurred shadow, and one with a literal colour. */ +.ring-outline { + box-shadow: inset 0 0 0 1px var(--md-sys-color-outline); +} + +.ring-secondary { + box-shadow: 0 0 0 3px var(--md-sys-color-secondary); +} + +.ring-disabled { + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent); +} + +.literal-shadow { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.blurred-ring { + box-shadow: inset 0 0 4px 1px #000; +}