Plan step 39 (part 4). With Tailwind gone the Workbench's Vite build still turns every `:dir(rtl)` into a long `:lang()` list (its own CSS minifier, not Tailwind's doing — confirmed by rebuilding and grepping the output). Every RTL mirror rule across the eleven stylesheets that had one now selects `:is([dir='rtl'], [dir='rtl'] *)` instead, matching `:dir(rtl)`'s own specificity (one pseudo-class) and its inherited "this element or a descendant of one carrying the attribute" reach; the built CSS now keeps the selector as written (no `:lang(` or `:dir(` left). RTL browser tests drop the `lang="ar"` workaround the old rewrite needed (ContainmentTest's side-sheet probe, LayoutTest's shared layoutPage() helper) and set only dir="rtl"; CarouselTest's probe already did. Feature tests asserting the selector's literal text (Progress, Overlay, Menu, ListDetail, Icon, Carousel) updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
114 lines
4.8 KiB
CSS
114 lines
4.8 KiB
CSS
/*
|
|
* <x-carousel>: M3 Expressive's carousel — multi-browse, hero, uncontained, the uncontained
|
|
* multi-aspect-ratio layout (added November 2025) and full-screen, on a native scroll container
|
|
* with CSS scroll snap. resources/js/carousel.js ports Compose's keyline maths (Arrangement.kt,
|
|
* Keylines.kt, KeylineList.kt, Strategy.kt, KeylineSnapPosition.kt, Carousel.kt, androidx Compose
|
|
* Material 3 @ commit 7ac433e44e797de53af85226797862687f37735f, Apache-2.0) and writes the masks
|
|
* and sizes as inline custom properties every scroll frame; this file draws everything the script
|
|
* does not — the row's layout per layout, and the controls — and carousel-item.css draws the
|
|
* item and its mask (docs/reference/m3/components-actions-communication-containment.md
|
|
* § Carousel; docs/audits/m3-alignment/containment.md).
|
|
*
|
|
* [data-md-carousel] the row wrapper; its value is the layout
|
|
* [data-md-carousel-probe] hidden, measures the preferred item width for the script
|
|
* [data-md-carousel-scroller] the scroll container (role="region")
|
|
* [data-md-carousel-item] one slide — carousel-item.css
|
|
* [data-md-carousel-controls] previous/next, "auto" (pointer: fine only) or "always"
|
|
* [data-md-carousel-previous], [data-md-carousel-next]
|
|
*
|
|
* Specs table: 16dp leading/trailing padding, 8dp top/bottom and between items, for every layout
|
|
* but uncontained (leading only) and full-screen (0, edge to edge — C-12); a `>` combinator
|
|
* throughout, never a bare descendant one, because a carousel item can itself hold a nested
|
|
* carousel (a `multi-aspect` gallery inside a `hero` slide) whose own root would otherwise match
|
|
* its parent's rules too. Full-screen scrolls vertically, one edge-to-edge item at a time, capped
|
|
* at the 840px medium window it is meant for (C-11); reduced motion needs no rule here at all —
|
|
* the script writes zero inset, shift and full opacity itself for every layout (C-05).
|
|
*
|
|
* The previous/next icons mirror in RTL by scaling the whole button, as the row's arrow keys and
|
|
* scroll direction already do (`resources/js/carousel.js`'s own `state.rtl`), rather than through
|
|
* `<x-icon mirror-rtl>`: `<x-button icon>` has no prop to reach it, and flipping a round icon
|
|
* button whole reads the same as flipping only its glyph.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './button.css';
|
|
@import './carousel-item.css';
|
|
|
|
@layer material.components {
|
|
[data-md-carousel] {
|
|
position: relative;
|
|
}
|
|
|
|
[data-md-carousel] > [data-md-carousel-probe] {
|
|
position: absolute;
|
|
inset-inline-start: 0;
|
|
top: 0;
|
|
height: 0;
|
|
width: var(--material-carousel-item-width);
|
|
visibility: hidden;
|
|
pointer-events: none;
|
|
}
|
|
|
|
[data-md-carousel] > [data-md-carousel-scroller] {
|
|
display: flex;
|
|
scrollbar-width: none;
|
|
|
|
&::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
[data-md-carousel='full-screen'] > [data-md-carousel-scroller] {
|
|
margin-inline: auto;
|
|
height: var(--material-carousel-height);
|
|
max-width: 840px;
|
|
flex-direction: column;
|
|
gap: var(--md-sys-measurement-space200);
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
overscroll-behavior-y: contain;
|
|
scroll-snap-type: y mandatory;
|
|
}
|
|
|
|
[data-md-carousel]:not([data-md-carousel='full-screen']) > [data-md-carousel-scroller] {
|
|
height: calc(var(--material-carousel-height) + 16px);
|
|
gap: var(--md-sys-measurement-space100);
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
overscroll-behavior-x: contain;
|
|
padding-block: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-carousel='multi-aspect'] > [data-md-carousel-scroller] {
|
|
padding-inline-start: var(--material-carousel-pad);
|
|
}
|
|
|
|
[data-md-carousel='multi-browse'] > [data-md-carousel-scroller],
|
|
[data-md-carousel='hero'] > [data-md-carousel-scroller] {
|
|
scroll-snap-type: x mandatory;
|
|
}
|
|
|
|
[data-md-carousel] > [data-md-carousel-controls] {
|
|
display: none;
|
|
margin-top: 12px;
|
|
justify-content: flex-end;
|
|
gap: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-carousel] > [data-md-carousel-controls='always'] {
|
|
display: flex;
|
|
}
|
|
|
|
@media (pointer: fine) {
|
|
[data-md-carousel] > [data-md-carousel-controls='auto'] {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
[data-md-carousel]:not([data-md-carousel='full-screen']) > [data-md-carousel-controls] > [data-md-carousel-previous]:is([dir='rtl'], [dir='rtl'] *),
|
|
[data-md-carousel]:not([data-md-carousel='full-screen']) > [data-md-carousel-controls] > [data-md-carousel-next]:is([dir='rtl'], [dir='rtl'] *) {
|
|
transform: scaleX(-1);
|
|
}
|
|
}
|