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>
418 lines
15 KiB
CSS
418 lines
15 KiB
CSS
/*
|
||
* <x-chip>: M3's assist, filter, input and suggestion chips (resources/views/components/chip.blade.php).
|
||
*
|
||
* From androidx Compose Material 3 (Chip.kt, AssistChipTokens, FilterChipTokens, InputChipTokens,
|
||
* SuggestionChipTokens at androidx/androidx 27cf9a7, Apache-2.0): 32px tall, the small corner,
|
||
* label-large, 18px icons and a 24px avatar; 16px padding beside a label and 8px beside an icon,
|
||
* 8px between (an input chip: 12px, 8px, and 4px beside an avatar). Compose draws the 1px border
|
||
* inside the chip; a CSS border takes room, so every padding beside it is 1px short of its token
|
||
* (7px for 8, 11px for 12, 15px for 16).
|
||
*
|
||
* Colours by type and state, each one role (never an opacity but M3's disabled 12% and 38%,
|
||
* tokens/state.css):
|
||
*
|
||
* flat outline-variant border; on-surface ink (assist) or on-surface-variant (the rest);
|
||
* the border turns on-surface (assist) or on-surface-variant while keyboard-focused
|
||
* elevated surface-container-low at elevation 1, 2 on hover, no border (not for input chips)
|
||
* selected a filter chip that is checked or pressed, an input chip marked `data-md-selected`:
|
||
* secondary-container with on-secondary-container ink, no border; a flat selected
|
||
* filter chip rises to elevation 1 on hover
|
||
* disabled on-surface ink at 38%; the border (flat) or the container (elevated, selected) at 12%
|
||
*
|
||
* Leading icons are primary on assist, suggestion and filter chips; on an input chip they are
|
||
* on-surface-variant, primary on a selected chip and while a chip with its own action is hovered,
|
||
* focused or pressed (`data-md-actionable`). A trailing icon is primary on assist and suggestion
|
||
* chips. A disabled chip's icons take its ink.
|
||
*
|
||
* The state layer is the chip's own ::before in the chip's ink (an input chip draws it on its
|
||
* action, since the remove button is a second control), at M3's hover, focus and press opacities;
|
||
* the focus indicator is the package's secondary ring outside the chip. Both the chip (or an input
|
||
* chip's action) and the remove button catch presses over 48×48 — M3 lets the target extend beyond
|
||
* the visible container. The filter chip's check grows in on the fast spatial spring and fades in
|
||
* on the slow effects one; it shrinks on default effects and fades on fast effects, as
|
||
* AnimatingChipContent does.
|
||
*
|
||
* [data-md-chip="assist|filter|input|suggestion"] the chip; data-md-elevated, data-md-disabled,
|
||
* data-md-selected, data-md-actionable,
|
||
* data-md-icon-start, data-md-icon-end,
|
||
* data-md-avatar, data-md-removable
|
||
* [data-md-chip-check-slot] a filter chip's check, over its icon when it has one
|
||
* [data-md-chip-icon], [data-md-chip-check]
|
||
* [data-md-chip-action] an input chip's own button or link (a <span> without one)
|
||
* [data-md-chip-avatar], [data-md-chip-icon], [data-md-chip-label]
|
||
* [data-md-chip-remove] an input chip's remove button
|
||
* [data-md-chip-label], [data-md-chip-icon], [data-md-chip-trailing]
|
||
*/
|
||
|
||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||
|
||
@import './icon.css';
|
||
@import './tooltip.css';
|
||
|
||
@layer material.components {
|
||
[data-md-chip] {
|
||
--chip-disabled-container: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||
--chip-disabled-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||
|
||
position: relative;
|
||
display: inline-flex;
|
||
flex-shrink: 0;
|
||
align-items: center;
|
||
max-width: 100%;
|
||
height: 32px;
|
||
border: 1px solid var(--md-sys-color-outline-variant);
|
||
border-radius: var(--md-sys-shape-corner-sm);
|
||
color: var(--md-sys-color-on-surface-variant);
|
||
font: var(--md-sys-typescale-label-lg);
|
||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||
font-variation-settings: normal;
|
||
white-space: nowrap;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
transition-property: background-color, border-color, color, box-shadow;
|
||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
/* ---- Assist, filter and suggestion: the chip is the control ------------------------------ */
|
||
|
||
/* The state layer and the 48px target are the foundation's `md-state-layer md-touch-target`
|
||
classes now (chip.blade.php, as group and scheme-picker's own controls render them;
|
||
foundation/interaction.css), at the same opacities and off the same `currentColor` this
|
||
chip always painted them from. Three extras stay chip's own: a filter chip's label is never
|
||
itself `:focus-visible` — the checkbox inside it is, caught with `:has()`, the same way the
|
||
ring below already had to; `data-md-disabled` marks a chip with no native `disabled` to
|
||
catch (a link, or a toggle button with no press of its own). */
|
||
[data-md-chip]:not([data-md-chip='input']) {
|
||
padding-inline: 15px;
|
||
outline: none;
|
||
cursor: pointer;
|
||
|
||
&:has(:focus-visible)::before {
|
||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||
}
|
||
|
||
&:is(:focus-visible, :has(:focus-visible)) {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
&[data-md-disabled]::before {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
[data-md-chip]:is([data-md-chip='filter'], [data-md-icon-start]):not([data-md-chip='input']) {
|
||
padding-inline-start: 7px;
|
||
}
|
||
|
||
[data-md-chip][data-md-icon-end] {
|
||
padding-inline-end: 7px;
|
||
}
|
||
|
||
[data-md-chip='assist'] {
|
||
color: var(--md-sys-color-on-surface);
|
||
}
|
||
|
||
[data-md-chip='assist']:not([data-md-elevated], [data-md-disabled]):focus-visible {
|
||
border-color: var(--md-sys-color-on-surface);
|
||
}
|
||
|
||
:is([data-md-chip='suggestion'], [data-md-chip='filter']):not([data-md-elevated], [data-md-disabled], :has(:checked), [aria-pressed='true']):is(:focus-visible, :has(:focus-visible)) {
|
||
border-color: var(--md-sys-color-on-surface-variant);
|
||
}
|
||
|
||
[data-md-chip][data-md-elevated] {
|
||
border-color: transparent;
|
||
background-color: var(--md-sys-color-surface-container-low);
|
||
box-shadow: var(--md-sys-elevation-1);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
[data-md-chip][data-md-elevated]:not([data-md-disabled]):hover {
|
||
box-shadow: var(--md-sys-elevation-2);
|
||
}
|
||
}
|
||
|
||
/* Selected: a checked filter chip, or a pressed one. */
|
||
[data-md-chip='filter']:is(:has(:checked), [aria-pressed='true']),
|
||
[data-md-chip='input'][data-md-selected] {
|
||
border-color: transparent;
|
||
background-color: var(--md-sys-color-secondary-container);
|
||
color: var(--md-sys-color-on-secondary-container);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
[data-md-chip='filter']:not([data-md-elevated], [data-md-disabled]):is(:has(:checked), [aria-pressed='true']):hover {
|
||
box-shadow: var(--md-sys-elevation-1);
|
||
}
|
||
}
|
||
|
||
[data-md-chip][data-md-disabled] {
|
||
border-color: var(--chip-disabled-container);
|
||
color: var(--chip-disabled-ink);
|
||
box-shadow: none;
|
||
}
|
||
|
||
[data-md-chip][data-md-disabled]:is([data-md-elevated], [data-md-selected], :has(:checked), [aria-pressed='true']) {
|
||
border-color: transparent;
|
||
background-color: var(--chip-disabled-container);
|
||
}
|
||
|
||
[data-md-chip][data-md-disabled]:not(a) {
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* A disabled link leaves the pointer too. */
|
||
a[data-md-chip][aria-disabled='true'] {
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ---- Contents ------------------------------------------------------------------------------ */
|
||
|
||
[data-md-chip-label] {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
:is([data-md-chip], [data-md-chip-action]) > [data-md-chip-icon] {
|
||
margin-inline-end: var(--md-sys-measurement-space100);
|
||
}
|
||
|
||
[data-md-chip-trailing] {
|
||
margin-inline-start: var(--md-sys-measurement-space100);
|
||
}
|
||
|
||
[data-md-chip]:not([data-md-chip='input'], [data-md-disabled]) [data-md-chip-icon],
|
||
[data-md-chip='input'][data-md-selected]:not([data-md-disabled]) [data-md-chip-icon],
|
||
:is([data-md-chip='assist'], [data-md-chip='suggestion']):not([data-md-disabled]) [data-md-chip-trailing] {
|
||
color: var(--md-sys-color-primary);
|
||
}
|
||
|
||
[data-md-chip][data-md-actionable] [data-md-chip-icon] {
|
||
transition: color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
[data-md-chip][data-md-actionable]:is(:active, :has(:focus-visible)) [data-md-chip-icon] {
|
||
color: var(--md-sys-color-primary);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
[data-md-chip][data-md-actionable]:hover [data-md-chip-icon] {
|
||
color: var(--md-sys-color-primary);
|
||
}
|
||
}
|
||
|
||
/* The filter chip's check: from nothing to 18px as it is selected, or over the icon it replaces. */
|
||
[data-md-chip-check-slot] {
|
||
display: grid;
|
||
flex-shrink: 0;
|
||
width: 0;
|
||
height: 18px;
|
||
margin-inline-end: var(--md-sys-measurement-space100);
|
||
overflow: hidden;
|
||
transition: width var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||
}
|
||
|
||
[data-md-chip-check-slot] > * {
|
||
grid-area: 1 / 1;
|
||
}
|
||
|
||
[data-md-chip-check-slot]:has([data-md-chip-icon]) {
|
||
width: 18px;
|
||
transition: none;
|
||
}
|
||
|
||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check-slot]:not(:has([data-md-chip-icon])) {
|
||
width: 18px;
|
||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||
}
|
||
|
||
[data-md-chip-check-slot] [data-md-chip-icon] {
|
||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
[data-md-chip-check] {
|
||
opacity: 0;
|
||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check-slot] [data-md-chip-icon] {
|
||
opacity: 0;
|
||
}
|
||
|
||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check] {
|
||
opacity: 1;
|
||
transition-duration: var(--md-sys-motion-effects-slow-duration);
|
||
transition-timing-function: var(--md-sys-motion-effects-slow);
|
||
}
|
||
|
||
/* ---- Input chips: an action of their own, and a remove button ---------------------------- */
|
||
|
||
[data-md-chip='input'] {
|
||
isolation: isolate;
|
||
}
|
||
|
||
[data-md-chip='input']:not([data-md-selected], [data-md-disabled]):has([data-md-chip-action]:focus-visible) {
|
||
border-color: var(--md-sys-color-on-surface-variant);
|
||
}
|
||
|
||
[data-md-chip='input']:has([data-md-chip-action]:focus-visible) {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
[data-md-chip-action] {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
height: 100%;
|
||
padding-inline: 11px;
|
||
outline: none;
|
||
}
|
||
|
||
[data-md-chip][data-md-icon-start] [data-md-chip-action] {
|
||
padding-inline-start: 7px;
|
||
}
|
||
|
||
[data-md-chip][data-md-avatar] [data-md-chip-action] {
|
||
padding-inline-start: 3px;
|
||
}
|
||
|
||
[data-md-chip][data-md-removable] [data-md-chip-action] {
|
||
padding-inline-end: var(--md-sys-measurement-space100);
|
||
}
|
||
|
||
:is(a, button)[data-md-chip-action] {
|
||
cursor: pointer;
|
||
|
||
/* The 48px target and the state layer, across the whole chip. */
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset-inline: 0;
|
||
top: 50%;
|
||
height: var(--md-sys-measurement-space600);
|
||
translate: 0 -50%;
|
||
}
|
||
}
|
||
|
||
[data-md-chip]:not([data-md-disabled]) :is(a, button)[data-md-chip-action] {
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: -1px;
|
||
z-index: -1;
|
||
border-radius: var(--md-sys-shape-corner-sm);
|
||
background-color: currentColor;
|
||
opacity: 0;
|
||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
&:hover::before {
|
||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||
}
|
||
}
|
||
|
||
&:focus-visible::before {
|
||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||
}
|
||
|
||
&:active::before {
|
||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||
}
|
||
}
|
||
|
||
[data-md-chip][data-md-disabled] :is(a, button)[data-md-chip-action] {
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
[data-md-chip][data-md-disabled] a[data-md-chip-action] {
|
||
pointer-events: none;
|
||
}
|
||
|
||
[data-md-chip-avatar] {
|
||
flex-shrink: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
margin-inline-end: var(--md-sys-measurement-space100);
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
}
|
||
|
||
span[data-md-chip-avatar] {
|
||
display: grid;
|
||
place-items: center;
|
||
background-color: var(--md-sys-color-primary-container);
|
||
color: var(--md-sys-color-on-primary-container);
|
||
font: var(--md-sys-typescale-label-sm);
|
||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||
font-variation-settings: normal;
|
||
}
|
||
|
||
img[data-md-chip-avatar] {
|
||
object-fit: cover;
|
||
}
|
||
|
||
[data-md-chip][data-md-disabled] [data-md-chip-avatar] {
|
||
opacity: var(--md-sys-state-disabled-content-opacity);
|
||
}
|
||
|
||
/* An 18px icon in a 24px state layer, catching presses 15px out on every side: M3's 48px. */
|
||
[data-md-chip-remove] {
|
||
position: relative;
|
||
display: grid;
|
||
flex-shrink: 0;
|
||
place-items: center;
|
||
width: 18px;
|
||
height: 18px;
|
||
margin-inline-end: 7px;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
outline: none;
|
||
cursor: pointer;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: -15px;
|
||
}
|
||
|
||
&:disabled {
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
&:not(:disabled) {
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: -3px;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
background-color: currentColor;
|
||
opacity: 0;
|
||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
&:hover::before {
|
||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||
}
|
||
}
|
||
|
||
&:focus-visible {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
&:focus-visible::before {
|
||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||
}
|
||
|
||
&:active::before {
|
||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||
}
|
||
}
|
||
}
|
||
}
|