An over-engineering audit of the whole tree, applied in five reviewed batches. Behaviour stays the same except where UPGRADE.md says otherwise. PHP: the showcase and error-page stylesheets are prebuilt into resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import (first occurrence kept, the order an application's build gives), instead of Stylesheets::bundle() inlining imports on every request; only the import walk DesignGuard needs stays. SchemeStylesheet::withProfiles() replaces three copies of the scheme-plus-profiles loop, material:scheme leaves spec and contrast checks to the node script that already made them, and the error page's scheme cache, the hashed view namespace, the translations path with no lang/ folder and DesignGuard's 1.x-name hints are gone. JS: the androidx shape port progress.js and both bin scripts each carried lives once in resources/js/shapes.js (the generated SVGs are unchanged); util.js holds ringIndex(), ms(), reopenGuard() and remember(), which were written out several times; listeners are released through AbortController; tooltip.js's hoverPopover() serves the rich tooltip too. CSS: every rule for an element inside the navigation rail queries `--md-navigation-rail-value` instead of repeating the seven collapsed conditions under five media branches; badge, alert, progress, slider and button read one non-inheriting colour-role table (components/color.css); the dialog chrome, the submenu's popover chrome, the chip's state layer and touch target, and the visually-hidden inputs use the shared rules they copied; foundation/tokens.css is folded into foundation.css. Views: Support\Field and Support\Link replace the error-key, bound-value and link-attribute blocks copied into the fields and link components; the timepicker period group, the menu filter and the showcase head are partials; the datepicker's steppers and entry fields are loops; component docblocks no longer restate SKILL.md. Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces four per-group files, DesignGuardTest and the layout-component tests use datasets, browser tests share one ready() helper, CSS parsing lives in ComponentStylesheet alone. docs/audits and the finding IDs citing it are removed, as are pestphp/pest-plugin-laravel, the unused composer scripts and check:font; the lint job runs in the feature job, which now installs node packages so the prebuilt-stylesheet staleness test runs in CI. Feature suite 1177 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
110 lines
4.7 KiB
CSS
110 lines
4.7 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).
|
|
*
|
|
* [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); 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; reduced motion needs no rule here at all —
|
|
* the script writes zero inset, shift and full opacity itself for every layout.
|
|
*
|
|
* 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;
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|