Pin every written-out copy of the rail's collapsed conditions

Plan step 36, navigation review. The Tailwind-era rail-collapsed:
variant is now plain selectors in 26 rules, each repeated in five
window bands, across navigation-rail.css, navigation-rail-item.css,
navigation-rail-section.css and layout/scaffold.css; the tests pinned
a few of them. A new test walks every rule whose :where() names the
rail and fails when a copy drifts from the seven conditions for its
band, or when a rule is drawn collapsed in some bands but not all five.
ComponentStylesheet gains rules() for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 01:12:27 +02:00
co-authored by Claude Opus 5
parent cbd058f948
commit f31acf82d7
2 changed files with 78 additions and 0 deletions
@@ -228,3 +228,70 @@ it('draws the item\'s own state layer split between the item and its indicator',
->and($item->declarations('[data-md-navigation-rail-item][data-md-navigation-rail-item] [data-md-navigation-indicator]::before')) ->and($item->declarations('[data-md-navigation-rail-item][data-md-navigation-rail-item] [data-md-navigation-indicator]::before'))
->toBe(['background-color' => 'var(--md-sys-color-on-secondary-container)', 'opacity' => 'var(--navigation-indicator-layer)']); ->toBe(['background-color' => 'var(--md-sys-color-on-secondary-container)', 'opacity' => 'var(--navigation-indicator-layer)']);
}); });
/**
* Plain CSS has no variant to name "drawn collapsed" once, so every rule with a collapsed shape
* writes the conditions out: navigation-rail.css, navigation-rail-item.css,
* navigation-rail-section.css, and layout/scaffold.css for the actions row inside the rail's footer.
* This walks every copy in those files — not one pinned rule — and fails if any drifts from the
* seven conditions the Tailwind-era `rail-collapsed:` variant had, or if a rule has its collapsed
* shape in some window bands but not in all five.
*/
dataset('stylesheets that draw a collapsed rail', [
'navigation-rail',
'navigation-rail-item',
'navigation-rail-section',
'../layout/scaffold',
]);
it('writes "collapsed" with the same seven conditions in every copy, in every band', function (string $name) {
$closed = ':not([data-md-open])';
$bands = [
'always' => ["[data-md-navigation-rail='collapsed']", "[data-rail='collapsed'] [data-md-navigation-rail='collapsible']{$closed}", "[data-md-navigation-rail='modal']{$closed}"],
'@media (width < 600px)' => ["[data-md-navigation-rail='collapsible']{$closed}"],
'@media (width < 840px)' => ["[data-md-navigation-rail='adaptive']{$closed}"],
'@media (840px <= width < 1200px)' => [":is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']{$closed}"],
'@media (width >= 1200px)' => ["[data-rail='collapsed'] [data-md-navigation-rail='adaptive']{$closed}"],
];
// A condition matches the rail itself and everything in it; the scaffold's actions row can only
// ever be inside a rail, so its copy keeps the descendant half alone.
$both = fn (array $conditions): array => array_merge(...array_map(fn (string $condition): array => [$condition, "{$condition} *"], $conditions));
$inside = fn (array $conditions): array => array_map(fn (string $condition): string => "{$condition} *", $conditions);
$groups = [];
foreach (ComponentStylesheet::read($name)->rules() as $rule) {
preg_match_all('/:where\(((?:[^()]++|\((?1)\))*)\)/', $rule['selector'], $wheres, PREG_SET_ORDER);
$wheres = array_filter($wheres, fn (array $where): bool => str_contains($where[1], 'data-md-navigation-rail='));
if ($wheres === []) {
continue;
}
$media = array_values(array_filter($rule['at'], fn (string $at): bool => str_starts_with($at, '@media')));
$band = match ($media) {
[] => 'always',
['@media (width >= 840px)', '@media (width < 1200px)'] => '@media (840px <= width < 1200px)',
default => implode(' ', $media),
};
expect(array_key_exists($band, $bands))->toBeTrue("{$name}.css: `{$rule['selector']}` sits in {$band}, which is no band of the rail's");
foreach ($wheres as [$where, $conditions]) {
$written = array_map(trim(...), preg_split('/,(?![^()]*\))/', $conditions));
expect(in_array($written, [$both($bands[$band]), $inside($bands[$band])], true))
->toBeTrue("{$name}.css: `{$where}` in {$band} is not the rail's collapsed conditions for that band");
}
$key = str_replace(array_column($wheres, 0), '', $rule['selector']).' '.json_encode($rule['declarations']);
$groups[$key][] = $band;
}
expect($groups)->not->toBeEmpty();
foreach ($groups as $key => $found) {
expect(array_values(array_unique($found)))->toEqualCanonicalizing(array_keys($bands), "{$name}.css: {$key} is drawn collapsed in some bands only");
}
})->with('stylesheets that draw a collapsed rail');
+11
View File
@@ -117,6 +117,17 @@ final class ComponentStylesheet
return $declarations; return $declarations;
} }
/**
* Every rule, nesting resolved, in source order: for a test that has to walk all of them rather
* than name one.
*
* @return list<array{selector: string, at: list<string>, declarations: array<string, string>}>
*/
public function rules(): array
{
return $this->rules;
}
public function has(string $selector, array $at = []): bool public function has(string $selector, array $at = []): bool
{ {
try { try {