Files
livewire-material/resources/css/components/search.css
T
Andreas Reinhold / reiniandClaude Opus 5 247c596c3a Cut duplicated and speculative code across the package
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>
2026-09-17 19:29:21 +02:00

328 lines
13 KiB
CSS

/*
* M3's search: a search bar that opens into a search view (SearchBarTokens, SearchViewTokens,
* androidx Compose Material 3, Apache-2.0).
*
* The bar is a 56px pill in surface-container-high: a leading search icon, the input in
* body-large, a clear button once something is typed, and whatever the caller trails it with (an
* avatar, an icon button). Focus opens the view. On a medium or wider window the view is docked
* over a scrim: the bar grows down into an extra-large-cornered container at elevation 3 that
* holds the results. On a compact window it takes the whole screen, and the search icon turns into
* a back arrow. `docked` keeps it docked at every width.
*
* Widths are M3's: never wider than 720px, and the bar grows to that width while it is focused —
* `--search-width` is its resting width (the room it is given, by default) and
* `--search-open-width` the focused one, so a caller that wants M3's 360px resting bar sets
* `--search-width: 360px` on a wrapper. The leading and trailing padding is 24px unfocused and
* 16px focused, as the search specs table gives it.
*
* Hovered and focused buttons in the bar take M3's state-layer opacities (tokens/state.css), and the
* docked scrim is M3's scrim role at 32%, which no system token names. The breakpoint that makes the
* view full screen is decided in resources/js/search.js (`upTo('medium')`, 600px).
*
* [data-md-search] the root; data-md-open, data-md-full-screen, data-md-trigger
* [data-md-search-scrim] over the page while the view is docked
* [data-md-search-trigger] data-md-trigger="icon" only: the icon button that expands the view
* [data-md-search-bar] the pill, above the view
* [data-md-search-leading], [data-md-search-field] (the combobox around
* [data-md-search-input]), [data-md-search-clear], [data-md-search-trailing]
* [data-md-search-view] the container behind the bar
* [data-md-search-suggestions] before the first keystroke
* [data-md-search-results] once something is typed; [data-md-search-empty] when nothing is
* [data-md-search-status] the polite live region that counts whichever list is on screen
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './icon.css';
@layer material.components {
[data-md-search] {
--search-height: 56px;
/* M3: min 360px, max 720px, and wider while focused. */
--search-width: 100%;
--search-open-width: 720px;
position: relative;
}
/* Above the page while open, and while the view leaves: full screen through `data-md-full-screen`,
docked through the `md-transition` class Alpine's `x-transition` carries for the length of the
view's exit, so later positioned content on the page never covers the fading view. */
[data-md-search]:is([data-md-open], [data-md-full-screen], :has(> [data-md-search-view].md-transition)) {
z-index: 50;
}
/* Docked, the view opens over the page: M3 puts a scrim under it. Behind the bar and the view
inside the root's own stacking context, above everything the page draws outside it. */
[data-md-search-scrim] {
position: fixed;
inset: 0;
z-index: -1;
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
transition-property: opacity;
transition-duration: var(--md-sys-motion-effects-fast-duration);
transition-timing-function: var(--md-sys-motion-effects-fast);
}
@starting-style {
[data-md-search-scrim] {
opacity: 0;
}
}
/* Closing (or turning full screen), the scrim fades back out while the view's `x-transition`
holds its `display`: not `allow-discrete`, which Firefox does not honour for `display`. */
[data-md-search]:is(:not([data-md-open]), [data-md-full-screen]) > [data-md-search-scrim] {
opacity: 0;
}
[data-md-search-bar] {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: var(--md-sys-measurement-space50);
width: min(100%, var(--search-width));
height: var(--search-height);
padding-inline: 12px;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-high);
color: var(--md-sys-color-on-surface);
cursor: text;
transition-property: background-color, width, padding-inline;
transition-duration: var(--md-sys-motion-effects-fast-duration);
transition-timing-function: var(--md-sys-motion-effects-fast);
}
/* Focused: wider, and the padding drops from 24px to the contained style's 16px. */
[data-md-search][data-md-open] [data-md-search-bar] {
width: min(100%, var(--search-open-width));
padding-inline: var(--md-sys-measurement-space50);
}
@media (hover: hover) {
[data-md-search]:not([data-md-open]) [data-md-search-bar]:hover {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-sys-color-surface-container-high));
}
}
[data-md-search][data-md-open] [data-md-search-bar] {
background-color: transparent;
}
[data-md-search-leading],
[data-md-search-clear] {
display: grid;
flex: none;
place-items: center;
width: var(--md-sys-measurement-space600);
height: var(--md-sys-measurement-space600);
border-radius: var(--md-sys-shape-corner-full);
}
[data-md-search-leading] {
color: var(--md-sys-color-on-surface);
}
[data-md-search-clear] {
color: var(--md-sys-color-on-surface-variant);
cursor: pointer;
}
[data-md-search-bar] button {
outline: none;
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
[data-md-search-bar] button:hover {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
}
}
[data-md-search-bar] button:focus-visible {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: -3px;
}
/* The combobox wrapper is only a role holder; it has to lay out as the input used to. */
[data-md-search-field] {
display: flex;
flex: 1 1 0%;
min-width: 0;
height: 100%;
}
[data-md-search-input] {
flex: 1 1 0%;
min-width: 0;
height: 100%;
padding-inline: var(--md-sys-measurement-space50);
appearance: none;
background: transparent;
color: var(--md-sys-color-on-surface);
caret-color: var(--md-sys-color-primary);
outline: none;
font: var(--md-sys-typescale-body-lg);
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
}
[data-md-search-input]::-webkit-search-cancel-button,
[data-md-search-input]::-webkit-search-decoration {
appearance: none;
display: none;
}
[data-md-search-bar]:has([data-md-search-input]:placeholder-shown) [data-md-search-clear] {
display: none;
}
[data-md-search-trailing] {
display: flex;
flex: none;
align-items: center;
gap: var(--md-sys-measurement-space50);
padding-inline-end: var(--md-sys-measurement-space50);
}
[data-md-search][data-md-open] [data-md-search-trailing] {
display: none;
}
[data-md-search-view] {
position: absolute;
inset-inline: 0;
top: 0;
display: flex;
flex-direction: column;
max-width: var(--search-open-width);
max-height: min(640px, 70dvh);
padding-top: var(--search-height);
overflow: hidden;
border-radius: var(--md-sys-shape-corner-xl);
background-color: var(--md-sys-color-surface-container-high);
box-shadow: var(--md-sys-elevation-3);
transform-origin: top;
transition-property: opacity, scale;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
@starting-style {
[data-md-search-view] {
opacity: 0;
scale: 1 0.9;
}
}
/* Closing, the view goes back the way it came, into the bar, while its `x-transition` holds
`display` for the same duration. */
[data-md-search]:not([data-md-open]) > [data-md-search-view] {
opacity: 0;
scale: 1 0.9;
}
/* M3: the docked container is at least 240px tall — once there is something in it to be tall
about; a search with nothing to show stays the height of its bar. */
[data-md-search-view]:has([data-md-search-results], [data-md-search-suggestions]) {
min-height: 240px;
}
/* No divider: that belongs to the divided style, which Expressive deprecates in favour of the
contained one this file draws. The suggestions stand in the same place, before the first
keystroke; only one of the two is ever on screen. */
[data-md-search-results],
[data-md-search-suggestions] {
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding-block: var(--md-sys-measurement-space100);
}
/* M3's second entry point: search as a secondary action, one icon button that expands into the
full-screen view. The root keeps the button's 48px whether the view is open or not, so a
toolbar does not shift under it, and the bar is the view's header rather than a resting bar. */
[data-md-search][data-md-trigger='icon'] {
flex: none;
width: var(--md-sys-measurement-space600);
height: var(--md-sys-measurement-space600);
}
[data-md-search-trigger] {
display: grid;
place-items: center;
width: var(--md-sys-measurement-space600);
height: var(--md-sys-measurement-space600);
border-radius: var(--md-sys-shape-corner-full);
color: var(--md-sys-color-on-surface-variant);
cursor: pointer;
outline: none;
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
[data-md-search-trigger]:hover {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
}
}
[data-md-search-trigger]:focus-visible {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: -3px;
}
[data-md-search-trigger]:active {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
}
[data-md-search][data-md-trigger='icon']:not([data-md-open], [data-md-full-screen]) [data-md-search-bar] {
display: none;
}
/* Full screen, on a compact window. */
[data-md-search][data-md-full-screen] [data-md-search-bar] {
position: fixed;
inset: 0 0 auto;
width: auto;
height: calc(var(--md-sys-measurement-space900) + var(--material-safe-top, env(safe-area-inset-top)));
padding-top: var(--material-safe-top, env(safe-area-inset-top));
padding-inline: var(--md-sys-measurement-space50);
border-radius: 0;
}
/* Leaving full screen (`data-md-full-screen` outlives `data-md-open` by the view's exit,
resources/js/search.js): the header bar fades with the view, on the view's own spring, rather
than going back to its resting pill — or to nothing, behind the icon — on the first frame. */
[data-md-search][data-md-full-screen]:not([data-md-open]) [data-md-search-bar] {
opacity: 0;
background-color: transparent;
transition-property: opacity;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
[data-md-search][data-md-full-screen] [data-md-search-view] {
position: fixed;
inset: 0;
max-width: none;
max-height: none;
padding-top: calc(var(--md-sys-measurement-space900) + var(--material-safe-top, env(safe-area-inset-top)));
/* The full-screen layout has its own container role, one step from the docked one. */
background-color: var(--md-sys-color-surface-container-low);
border-radius: 0;
box-shadow: none;
}
/* What the caller says when nothing matches: body-medium in on-surface-variant, padded like a
list row (16px across, 12px down). */
[data-md-search-empty] {
padding: 12px var(--md-sys-measurement-space200);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-md);
letter-spacing: var(--md-sys-typescale-body-md-tracking);
font-variation-settings: normal;
}
}