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>
328 lines
14 KiB
CSS
328 lines
14 KiB
CSS
/*
|
|
* `<x-drawer>`: M3's side sheet, modal by default and co-planar as `standard` from 840px
|
|
* (docs/reference/m3/components-actions-communication-containment.md § Side sheets → Specs; no
|
|
* androidx Compose token file exists for it — Compose support is "Unavailable" per the site — so
|
|
* the numbers come from the specs page and, for the standard variant's 0dp elevation,
|
|
* material-components-android's `SideSheet.md`).
|
|
*
|
|
* [data-md-drawer] the wrapper; data-md-open, data-md-standard
|
|
* [data-md-drawer-scrim] modal only (a standard sheet's is hidden from 840px)
|
|
* [data-md-drawer-sheet] surface-container-low, elevation 1; data-md-side
|
|
* [data-md-drawer-head] title/subtitle/close, and the separator's divider
|
|
* [data-md-drawer-head-row], [data-md-drawer-heading], [data-md-drawer-title],
|
|
* [data-md-drawer-subtitle], [data-md-drawer-close]
|
|
* [data-md-drawer-body] a size container, so its contents lay out by the sheet's own
|
|
* width (`@md:`), never the viewport's
|
|
* [data-md-drawer-actions] left-aligned, 72dp — a dialog's are trailing-aligned
|
|
*
|
|
* 400px cap, 24dp start/end padding, a large corner on the inner edge only — from the specs
|
|
* table. It enters on the emphasized-decelerate easing rather than a spring, because a sheet
|
|
* anchored to an edge that overshot would open a gap, and leaves on emphasized accelerate: the
|
|
* view keeps `x-show`, so `@starting-style` gives the entry its start and the view's `x-transition`
|
|
* holds `display` through the exit, whose timing comes from the closed state's own rule (a
|
|
* transition takes the after-change style's). Not `allow-discrete` on `display`: Firefox does not
|
|
* transition `display`, and in the other engines it would hold a second time after Alpine's hold.
|
|
* Alpine holds for the first `transition-duration` listed, so each exit's longest transition is
|
|
* listed first. Reduced motion zeroes both duration tokens (tokens/motion.css).
|
|
* Below `expanded` it slides, from whichever edge `data-md-side` names in the document's direction;
|
|
* once `standard` makes it co-planar it only fades (an in-flow box does not need to travel). Every
|
|
* state rule matches the sheet and scrim as the root's children, so a sheet nested in an open one
|
|
* keeps its own.
|
|
*
|
|
* A `standard` sheet drops the scrim, the trap and the elevation, and swaps surface-container-low
|
|
* for `surface` with an outline-variant rule down its inner edge, in place of the scrim
|
|
* (its anatomy's "Divider (optional)"). Below `expanded` (840px) it is the modal sheet — M3 caps
|
|
* a side sheet at 400dp, and a 600px window has too little room left beside one, so the switch
|
|
* sits at `expanded` rather than `medium`. Opening it shrinks the body beside it and closing it
|
|
* gives the room back (M3's side sheets, "Adaptive"): the root's `inline-size` springs from none to
|
|
* the sheet's width — the sheet itself keeps its width, sits at the root's far edge and is clipped,
|
|
* so it is uncovered from its inner edge — together with a negative margin as wide as the flex
|
|
* parent's gap (`--md-drawer-gap`, which the view measures), so the content beside does not jump by
|
|
* the gap when the root leaves the layout. `data-md-drawer-collapsed` (the view, while the window is
|
|
* ≥ 840px and the sheet is closed) then takes the root out of the layout entirely, but only once the
|
|
* exit has run: the view's `closing` holds it, where it once cut the fade and the resize off on
|
|
* their first frame.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './button.css';
|
|
@import './divider.css';
|
|
|
|
@layer material.components {
|
|
/* The scrim fades in from `@starting-style` and back out while the view's `x-transition` holds
|
|
its `display` for this duration. Not `allow-discrete` on `display`: Firefox does not transition
|
|
`display`, so the scrim vanished there, and in the other engines it would hold a second time
|
|
after Alpine's hold. */
|
|
[data-md-drawer-scrim] {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 40;
|
|
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
|
opacity: 0;
|
|
transition-property: opacity;
|
|
transition-duration: var(--md-sys-motion-effects-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-effects-default);
|
|
}
|
|
|
|
[data-md-drawer][data-md-open] > [data-md-drawer-scrim] {
|
|
opacity: 1;
|
|
}
|
|
|
|
@starting-style {
|
|
[data-md-drawer][data-md-open] > [data-md-drawer-scrim] {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* Closed, the sheet waits one width past its own edge (`--sheet-offset`, mirrored in RTL) and
|
|
slides there on emphasized accelerate; open, it comes back on emphasized decelerate. */
|
|
[data-md-drawer-sheet] {
|
|
--sheet-offset: 100%;
|
|
|
|
position: fixed;
|
|
top: var(--material-safe-top, env(safe-area-inset-top));
|
|
bottom: 0;
|
|
z-index: 50;
|
|
display: flex;
|
|
width: 100%;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
padding: var(--md-sys-measurement-space300);
|
|
padding-block-end: calc(var(--md-sys-measurement-space300) + var(--material-safe-bottom, env(safe-area-inset-bottom)));
|
|
background-color: var(--md-sys-color-surface-container-low);
|
|
color: var(--md-sys-color-on-surface);
|
|
box-shadow: var(--md-sys-elevation-1);
|
|
translate: var(--sheet-offset) 0;
|
|
transition-property: translate;
|
|
transition-duration: var(--md-sys-motion-effects-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate);
|
|
}
|
|
|
|
[data-md-drawer-sheet][data-md-side='end'] {
|
|
inset-inline-end: 0;
|
|
}
|
|
|
|
[data-md-drawer-sheet][data-md-side='start'] {
|
|
--sheet-offset: -100%;
|
|
|
|
inset-inline-start: 0;
|
|
}
|
|
|
|
[data-md-drawer-sheet][data-md-side='end']:is([dir='rtl'], [dir='rtl'] *) {
|
|
--sheet-offset: -100%;
|
|
}
|
|
|
|
[data-md-drawer-sheet][data-md-side='start']:is([dir='rtl'], [dir='rtl'] *) {
|
|
--sheet-offset: 100%;
|
|
}
|
|
|
|
[data-md-drawer][data-md-open] > [data-md-drawer-sheet] {
|
|
translate: 0 0;
|
|
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
|
|
}
|
|
|
|
@starting-style {
|
|
[data-md-drawer][data-md-open] > [data-md-drawer-sheet] {
|
|
translate: var(--sheet-offset) 0;
|
|
}
|
|
}
|
|
|
|
@media (width >= 600px) {
|
|
[data-md-drawer-sheet] {
|
|
width: var(--sheet-width);
|
|
max-width: calc(100vw - 64px);
|
|
}
|
|
|
|
[data-md-drawer-sheet][data-md-side='end'] {
|
|
border-start-start-radius: var(--md-sys-shape-corner-lg);
|
|
border-end-start-radius: var(--md-sys-shape-corner-lg);
|
|
}
|
|
|
|
[data-md-drawer-sheet][data-md-side='start'] {
|
|
border-start-end-radius: var(--md-sys-shape-corner-lg);
|
|
border-end-end-radius: var(--md-sys-shape-corner-lg);
|
|
}
|
|
}
|
|
|
|
/* Co-planar from expanded: sticky beside the content, no scrim, no elevation, flat on
|
|
`surface` with an outline-variant rule down the inner edge instead. It only fades here —
|
|
an in-flow box does not travel like an overlay does. It sticks under the top safe area
|
|
(`--drawer-top`), as tall as the window below it, and its padding keeps its foot clear of
|
|
the bottom one, so a status bar never covers its head once the page scrolls. */
|
|
@media (width >= 840px) {
|
|
[data-md-drawer][data-md-standard] {
|
|
--drawer-top: var(--material-safe-top, env(safe-area-inset-top));
|
|
|
|
position: sticky;
|
|
top: var(--drawer-top);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
height: calc(100dvh - var(--drawer-top));
|
|
flex-shrink: 0;
|
|
align-self: flex-start;
|
|
overflow: clip;
|
|
inline-size: 0;
|
|
margin-inline-start: calc(-1 * var(--md-drawer-gap, 0px));
|
|
transition-property: inline-size, margin-inline;
|
|
transition-duration: var(--md-sys-motion-effects-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate);
|
|
}
|
|
|
|
/* In <x-scaffold>, a sticky `<x-app-bar>` in the `top` slot spans the content region, sheet
|
|
and all, so the sheet sticks under it: a small, centre-aligned or search bar is its 64px
|
|
row (AppBarSmallTokens.ContainerHeight) under the safe area it pads itself with; a medium
|
|
or large bar collapses to that 64px row alone, which the safe area may still be taller
|
|
than. */
|
|
[data-md-scaffold-content]:has(> [data-md-app-bar][data-md-sticky]) [data-md-drawer][data-md-standard] {
|
|
--drawer-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 64px);
|
|
}
|
|
|
|
[data-md-scaffold-content]:has(> [data-md-app-bar][data-md-sticky]:is([data-md-variant='medium'], [data-md-variant='large'])) [data-md-drawer][data-md-standard] {
|
|
--drawer-top: max(var(--material-safe-top, env(safe-area-inset-top)), 64px);
|
|
}
|
|
|
|
/* A start sheet sits at the start edge, with the gap after it. */
|
|
[data-md-drawer][data-md-standard]:where(:has(> [data-md-drawer-sheet][data-md-side='start'])) {
|
|
justify-content: flex-start;
|
|
margin-inline-start: 0;
|
|
margin-inline-end: calc(-1 * var(--md-drawer-gap, 0px));
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard][data-md-open] {
|
|
inline-size: min(var(--sheet-width), calc(100vw - 64px));
|
|
margin-inline: 0;
|
|
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
|
|
}
|
|
|
|
@starting-style {
|
|
[data-md-drawer][data-md-standard][data-md-open] {
|
|
inline-size: 0;
|
|
margin-inline-start: calc(-1 * var(--md-drawer-gap, 0px));
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard][data-md-open]:where(:has(> [data-md-drawer-sheet][data-md-side='start'])) {
|
|
margin-inline-start: 0;
|
|
margin-inline-end: calc(-1 * var(--md-drawer-gap, 0px));
|
|
}
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard][data-md-drawer-collapsed] {
|
|
display: none;
|
|
}
|
|
|
|
/* Before the view has settled, a sheet not rendered open is out of the layout already. The
|
|
view writes `data-md-drawer-collapsed` and measures the row's gap only once Alpine runs,
|
|
so until then a closed sheet would be a zero-wide flex item that still costs its row one
|
|
gap, and the content beside it would widen when the script started. A sheet whose
|
|
`wire:model` property is open is rendered `data-md-open` and stands at its width. */
|
|
[data-md-drawer][data-md-standard]:not([data-md-open], [data-md-drawer-settled]) {
|
|
display: none;
|
|
}
|
|
|
|
/* Until the view settles (`data-md-drawer-settled`, two frames after Alpine starts), the
|
|
state the page loads with is drawn at once: a sheet that starts open does not grow in. */
|
|
[data-md-drawer][data-md-standard]:not([data-md-drawer-settled]),
|
|
[data-md-drawer][data-md-standard]:not([data-md-drawer-settled]) > [data-md-drawer-sheet] {
|
|
transition: none;
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard] > [data-md-drawer-scrim] {
|
|
display: none;
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard] > [data-md-drawer-sheet] {
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
top: 0;
|
|
z-index: auto;
|
|
height: 100%;
|
|
translate: 0 0;
|
|
border-radius: var(--md-sys-shape-corner-none);
|
|
background-color: var(--md-sys-color-surface);
|
|
box-shadow: none;
|
|
opacity: 0;
|
|
transition-property: opacity;
|
|
transition-duration: var(--md-sys-motion-effects-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate);
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard][data-md-open] > [data-md-drawer-sheet] {
|
|
opacity: 1;
|
|
transition-duration: var(--md-sys-motion-spatial-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
|
|
}
|
|
|
|
@starting-style {
|
|
[data-md-drawer][data-md-standard][data-md-open] > [data-md-drawer-sheet] {
|
|
translate: 0 0;
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='end'] {
|
|
border-inline-start: 1px solid var(--md-sys-color-outline-variant);
|
|
}
|
|
|
|
[data-md-drawer][data-md-standard] > [data-md-drawer-sheet][data-md-side='start'] {
|
|
border-inline-end: 1px solid var(--md-sys-color-outline-variant);
|
|
}
|
|
}
|
|
|
|
[data-md-drawer-head] {
|
|
margin-bottom: var(--md-sys-measurement-space200);
|
|
}
|
|
|
|
[data-md-drawer-head-row] {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
[data-md-drawer-heading] {
|
|
min-width: 0;
|
|
}
|
|
|
|
[data-md-drawer-title] {
|
|
font: var(--md-sys-typescale-title-lg);
|
|
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
|
}
|
|
|
|
[data-md-drawer-subtitle] {
|
|
margin-top: var(--md-sys-measurement-space50);
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-md);
|
|
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
|
}
|
|
|
|
[data-md-drawer-close] {
|
|
display: inline-flex;
|
|
flex-shrink: 0;
|
|
margin-top: calc(-1 * var(--md-sys-measurement-space100));
|
|
margin-inline-end: -12px;
|
|
}
|
|
|
|
[data-md-drawer-head] > [data-md-divider] {
|
|
margin-top: var(--md-sys-measurement-space200);
|
|
}
|
|
|
|
[data-md-drawer-body] {
|
|
container-type: inline-size;
|
|
flex: 1 1 0%;
|
|
}
|
|
|
|
[data-md-drawer-actions] {
|
|
display: flex;
|
|
min-height: var(--md-sys-measurement-space900);
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
gap: var(--md-sys-measurement-space100);
|
|
padding-top: var(--md-sys-measurement-space200);
|
|
}
|
|
}
|