Merge branch 'worktree-agent-a41c756369d016a29'

This commit is contained in:
Andreas Reinhold / reini
2026-09-14 15:40:58 +02:00
67 changed files with 3434 additions and 1497 deletions
+17
View File
@@ -14,6 +14,23 @@
/* Actions and communication */
/* Inputs, selection and data */
@import './components/form.css';
@import './components/field.css';
@import './components/input.css';
@import './components/password.css';
@import './components/textarea.css';
@import './components/select.css';
@import './components/file.css';
@import './components/checkbox.css';
@import './components/radio.css';
@import './components/toggle.css';
@import './components/chip.css';
@import './components/chip-set.css';
@import './components/choices.css';
@import './components/slider.css';
@import './components/search.css';
@import './components/table.css';
@import './components/sort-header.css';
/* Containment */
+114
View File
@@ -0,0 +1,114 @@
/*
* <x-checkbox>: M3's checkbox (resources/views/components/checkbox.blade.php), on the row the
* selection controls share (components/selection.css).
*
* CheckboxTokens (androidx Compose Material 3, Apache-2.0): an 18px box with a 2px on-surface-variant
* outline and 2px corners, filled with primary and holding an on-primary tick (or dash, for the
* mixed state) at the 18px icon size once it is ticked; the state layer a 40px circle around it,
* 11px past each edge. The tick grows in and fades in on the fast spatial spring. With the server's
* error the outline, the fill and the state layer are error and the tick on-error. Disabled, the
* outline and the fill are on-surface at 38% and the tick is surface. Beside a hint the box drops
* 3px, to sit on the label's first line.
*
* [data-md-checkbox] the root: the row and its errors; `class` and `style` land here
* [data-md-selection-row]
* [data-md-checkbox-box] the box: input, [data-md-checkbox-check], [data-md-checkbox-mixed]
* [data-md-selection-support] the errors, 4px under the row
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './selection.css';
@import './icon.css';
@layer material.components {
[data-md-checkbox] {
min-width: 0;
}
[data-md-checkbox-box] {
position: relative;
display: grid;
flex: none;
place-items: center;
width: 18px;
height: 18px;
}
[data-md-checkbox-box] > * {
grid-area: 1 / 1;
}
[data-md-selection-row]:has([data-md-selection-hint]) [data-md-checkbox-box] {
margin-top: 3px;
}
[data-md-checkbox-box]::before {
inset: -11px;
}
[data-md-checkbox-box]:has(input:focus-visible)::before {
outline: 3px solid var(--md-sys-color-secondary);
}
[data-md-checkbox-box] input {
width: 18px;
height: 18px;
border: 2px solid var(--md-sys-color-on-surface-variant);
border-radius: 2px;
}
[data-md-checkbox-box] input:is(:checked, :indeterminate) {
border-color: var(--md-sys-color-primary);
background-color: var(--md-sys-color-primary);
}
[data-md-checkbox-box] :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
color: var(--md-sys-color-on-primary);
opacity: 0;
scale: 0.5;
pointer-events: none;
transition-property: opacity, scale;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
[data-md-checkbox-box] input:checked:not(:indeterminate) ~ [data-md-checkbox-check],
[data-md-checkbox-box] input:indeterminate ~ [data-md-checkbox-mixed] {
opacity: 1;
scale: 1;
}
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] input {
border-color: var(--md-sys-color-error);
}
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] input:is(:checked, :indeterminate) {
background-color: var(--md-sys-color-error);
}
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
color: var(--md-sys-color-on-error);
}
[data-md-checkbox-box] input:disabled {
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
[data-md-checkbox-box] input:disabled:is(:checked, :indeterminate) {
border-color: transparent;
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
[data-md-checkbox-box] input:disabled ~ :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
color: var(--md-sys-color-surface);
}
[data-md-checkbox] [data-md-selection-support] {
margin-top: var(--md-sys-measurement-space50);
color: var(--md-sys-color-error);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
font-variation-settings: normal;
}
}
+128
View File
@@ -0,0 +1,128 @@
/*
* <x-chip-set>: a set of M3 chips (resources/views/components/chip-set.blade.php), named for screen
* readers as a group.
*
* The chips sit 8px apart (M3's chip spacing), in a row that wraps. Above it the label in
* label-large on-surface-variant, 8px over the row; under it the hint, or the errors in its place,
* body-small, 4px under the row.
*
* `data-md-scroll` keeps the chips on one line that scrolls sideways, as M3 lays chips out on a
* narrow screen. M3's chips accessibility page asks that a row which overflows say so: the edge the
* row can still scroll towards (`data-md-scroll-start`, `data-md-scroll-end`, set by
* resources/js/chips.js) fades over 24px through a mask, black meaning only "opaque" there, and in a
* right-to-left document the fade runs the other way. Where the pointer is fine, and so there is no
* swipe, a 32px round button in surface-container-high at elevation 1 sits over each fading edge;
* the row's scroll padding grows from 24px to 40px there, so a chip the keyboard reaches scrolls
* clear of the button as well as the fade. The row reaches 6px and 8px past its box so a focused
* chip's ring and an elevated chip's shadow are not clipped by the scroller.
*
* [data-md-chip-set] the group; data-md-scroll
* [data-md-chip-set-label]
* [data-md-chip-set-scroller] (scrolling) the row and its buttons
* [data-md-chip-set-row] the chips; data-md-scroll-start, data-md-scroll-end
* [data-md-chip-scroll="start|end"]
* [data-md-chip-set-hint] | [data-md-chip-set-errors]
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './chip.css';
@import './icon.css';
@layer material.components {
[data-md-chip-set] {
min-width: 0;
}
[data-md-chip-set-label] {
margin-bottom: var(--md-sys-measurement-space100);
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;
}
[data-md-chip-set-row] {
display: flex;
flex-wrap: wrap;
gap: var(--md-sys-measurement-space100);
}
[data-md-chip-set-hint],
[data-md-chip-set-errors] > p {
margin-top: var(--md-sys-measurement-space50);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
font-variation-settings: normal;
}
[data-md-chip-set-errors] > p {
color: var(--md-sys-color-error);
}
/* ---- Scrolling --------------------------------------------------------------------------- */
[data-md-chip-set-scroller] {
position: relative;
}
[data-md-chip-set-scroller] > [data-md-chip-set-row] {
--chip-fade-start: 0px;
--chip-fade-end: 0px;
flex-wrap: nowrap;
margin: calc(-1 * var(--md-sys-measurement-space100)) calc(-1 * var(--md-sys-measurement-space75));
padding: var(--md-sys-measurement-space100) var(--md-sys-measurement-space75);
overflow-x: auto;
scroll-padding-inline: var(--md-sys-measurement-space300);
scrollbar-width: none;
mask-image: linear-gradient(to right, transparent, black var(--chip-fade-start), black calc(100% - var(--chip-fade-end)), transparent);
&:dir(rtl) {
mask-image: linear-gradient(to left, transparent, black var(--chip-fade-start), black calc(100% - var(--chip-fade-end)), transparent);
}
&[data-md-scroll-start] {
--chip-fade-start: var(--md-sys-measurement-space300);
}
&[data-md-scroll-end] {
--chip-fade-end: var(--md-sys-measurement-space300);
}
@media (pointer: fine) {
scroll-padding-inline: var(--md-sys-measurement-space500);
}
}
[data-md-chip-scroll] {
position: absolute;
inset-block: 0;
display: none;
place-items: center;
width: 32px;
height: 32px;
margin-block: auto;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-high);
color: var(--md-sys-color-on-surface-variant);
box-shadow: var(--md-sys-elevation-1);
}
[data-md-chip-scroll='start'] {
inset-inline-start: 0;
}
[data-md-chip-scroll='end'] {
inset-inline-end: 0;
}
/* Only while the row can still scroll that way, and only where the pointer cannot swipe. */
@media (pointer: fine) {
[data-md-chip-set-row][data-md-scroll-start] ~ [data-md-chip-scroll='start'],
[data-md-chip-set-row][data-md-scroll-end] ~ [data-md-chip-scroll='end'] {
display: grid;
}
}
}
+444
View File
@@ -0,0 +1,444 @@
/*
* <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';
/* TODO(step 36): @import './tooltip.css' once <x-tooltip> leaves Tailwind (actions stream). */
@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 ------------------------------ */
[data-md-chip]:not([data-md-chip='input']) {
isolation: isolate;
padding-inline: 15px;
outline: none;
cursor: pointer;
/* The 48px target, past the chip's 32px. */
&::after {
content: '';
position: absolute;
inset-inline: 0;
top: 50%;
height: var(--md-sys-measurement-space600);
translate: 0 -50%;
}
&::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background-color: currentColor;
opacity: 0;
pointer-events: none;
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,
&:has(:focus-visible)::before {
opacity: var(--md-sys-state-focus-state-layer-opacity);
}
&:active::before {
opacity: var(--md-sys-state-pressed-state-layer-opacity);
}
&:is(:focus-visible, :has(:focus-visible)) {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
&:is(:disabled, [aria-disabled='true'], [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);
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
/*
* <x-choices>: choosing from a list, as M3's filter chips or as a searchable field with a menu
* (resources/views/components/choices.blade.php).
*
* Almost nothing of its own to draw: the chips are <x-chip-set>'s and <x-chip>'s, the searchable
* field is <x-field>'s with its arrow turning over while the list is open (components/field.css),
* and the open list is M3's menu, drawn by components/menu.css on `data-md-field-menu` and
* `data-md-field-option`. What is left is the row that says nothing matches: body-medium in
* on-surface-variant, padded like a menu row (16px across, 12px down).
*
* [data-md-choices] the root; data-md-searchable
* [data-md-choices-empty] the list's "Nothing matches" row
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './field.css';
@import './icon.css';
@import './chip-set.css';
@import './chip.css';
/* TODO(step 36): @import './menu.css' once the menu leaves Tailwind (actions stream); its field-menu rules draw the open list. */
@layer material.components {
[data-md-choices-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;
}
}
+137 -258
View File
@@ -9,30 +9,44 @@
* `<legend>`, carrying the label at its floated size, cuts the gap the label sits in, so the gap is
* right on any background. The filled field has no outline: a surface-container-highest box with
* extra-small top corners and an active indicator line, the label floating inside it
* (FilledTextFieldTokens / OutlinedTextFieldTokens, androidx Compose Material 3, Apache-2.0).
* (FilledTextFieldTokens / OutlinedTextFieldTokens, androidx Compose Material 3, Apache-2.0): 56px
* tall, 16px padding, 24px icons 16px from the text, the label in body-small once it floats.
* The `sm` (40px) and `xs` (32px) sizes are this package's, for an unlabelled field in a toolbar.
* Disabled is on-surface at M3's 38% content and 12% container opacities (tokens/state.css); the
* filled field's disabled container is on-surface at 4%, FilledTextFieldTokens'
* DisabledContainerOpacity, which has no system token.
*
* Anatomy (resources/views/components/field.blade.php):
*
* .field the whole thing; `class` from the call site lands here; data-variant, data-size
* .field-box the 56px row: icon, prefix, control, suffix, trailing
* .field-control the <input>, <select> or <textarea>
* .field-trailing what ends the row; .field-error is the error icon M3 asks for as the
* error state's second indicator
* .field-outline the fieldset and its legend (the filled field's indicator line)
* .field-label the visible label
* .field-support-row the row under the field
* .field-support the hint, or the error in its place
* .field-counter M3's character counter, `n/max`, at the end of that row
* [data-md-field] the whole thing; `class` from the call site lands here;
* data-md-variant, data-md-size, data-md-invalid, data-md-floated,
* data-md-mono, data-md-full, data-md-readonly
* [data-md-field-box] the 56px row: icon, prefix, control, suffix, trailing
* [data-md-field-icon] the leading icon
* [data-md-field-affix] a prefix or suffix beside the value
* [data-md-field-control] the <input>, <select> or <textarea>
* [data-md-field-trailing] what ends the row; [data-md-field-error] is the error icon M3 asks
* for as the error state's second indicator; [data-md-field-button]
* a trailing icon button (clear, copy, reveal, open a picker)
* [data-md-field-outline] the fieldset and its legend (the filled field's indicator line)
* [data-md-field-label] the visible label
* [data-md-field-support-row] the row under the field
* [data-md-field-support] the hint, or the error in its place
* [data-md-field-counter] M3's character counter, `n/max`, at the end of that row
*
* A select's open list is not part of the field: it is the dropdown menu in components/menu.css.
*/
@layer components {
.field {
--field-height: 3.5rem;
--field-pad: 1rem;
--field-icon: 1.5rem;
--field-gap: 1rem;
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './icon.css';
@layer material.components {
[data-md-field] {
--field-height: 56px;
--field-pad: var(--md-sys-measurement-space200);
--field-icon: 24px;
--field-gap: var(--md-sys-measurement-space200);
--field-start: var(--field-pad);
--field-edge: var(--md-sys-color-outline);
--field-ink: var(--md-sys-color-on-surface-variant);
@@ -45,41 +59,42 @@
/* M3 § Text Fields, Behaviour: "compact breakpoints can let a text field span full width;
* medium/expanded should bound it with flexible margins/other containers — never let it span the
* full width of a large screen." The site names no number, so 40rem is this package's: it is
* under half the `large` class's 1200px, a little over the `expanded` class's half, and holds
* about the 70 characters body-large reads best at. It is a ceiling, not a width — a narrower
* pane still gets a narrower field. A `max-w-*` class from the call site beats it, because
* utilities come after this layer, and `full` takes it off for a field that really is the width
* of its pane (a search-and-filter row, an editor). Below `medium` nothing is bounded. */
@media (width >= 37.5rem) {
.field:not([data-full]) {
* under half the `large` breakpoint's 1200px, a little over half the `expanded` one's, and holds
* about the 70 characters body-large reads best at — a measure of text, so it is in rem and grows
* with the text. It is a ceiling, not a width — a narrower pane still gets a narrower field. A
* width rule from the call site beats it, because an application's CSS and a utility both
* outrank this layer, and `full` takes it off for a field that really is the width of its pane (a
* search-and-filter row, an editor). Below `medium` nothing is bounded. */
@media (width >= 600px) {
[data-md-field]:not([data-md-full]) {
max-width: 40rem;
}
}
/* A filled field's resting indicator line is on-surface-variant, where an outline is outline. */
.field[data-variant="filled"] {
[data-md-field][data-md-variant='filled'] {
--field-edge: var(--md-sys-color-on-surface-variant);
}
.field[data-size="sm"] {
--field-height: 2.5rem;
--field-pad: 0.75rem;
--field-icon: 1.25rem;
--field-gap: 0.5rem;
[data-md-field][data-md-size='sm'] {
--field-height: 40px;
--field-pad: 12px;
--field-icon: 20px;
--field-gap: var(--md-sys-measurement-space100);
}
.field[data-size="xs"] {
--field-height: 2rem;
--field-pad: 0.625rem;
--field-icon: 1rem;
--field-gap: 0.375rem;
[data-md-field][data-md-size='xs'] {
--field-height: 32px;
--field-pad: var(--md-sys-measurement-space125);
--field-icon: 16px;
--field-gap: var(--md-sys-measurement-space75);
}
.field:has(.field-icon) {
[data-md-field]:has([data-md-field-icon]) {
--field-start: calc(var(--field-pad) + var(--field-icon) + var(--field-gap));
}
.field-box {
[data-md-field-box] {
position: relative;
display: flex;
align-items: center;
@@ -90,7 +105,7 @@
cursor: text;
}
.field-control {
[data-md-field-control] {
flex: 1 1 0%;
min-width: 0;
align-self: stretch;
@@ -102,154 +117,38 @@
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
}
.field[data-size="xs"] .field-control {
[data-md-field][data-md-size='xs'] [data-md-field-control] {
font: var(--md-sys-typescale-body-md);
letter-spacing: var(--md-sys-typescale-body-md-tracking);
}
.field[data-mono] .field-control {
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
[data-md-field][data-md-mono] [data-md-field-control] {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
.field-control::placeholder {
[data-md-field-control]::placeholder {
color: var(--md-sys-color-on-surface-variant);
opacity: 1;
}
/* Autofill paints its own background, which on a card is a pale block in the
wrong colour. Delaying the transition for a week keeps the field's own. */
.field-control:-webkit-autofill {
[data-md-field-control]:-webkit-autofill {
-webkit-text-fill-color: var(--md-sys-color-on-surface);
transition: background-color 604800s, color 604800s;
}
/* The browser draws the resize grip in the textarea's own corner, so the
textarea reaches to just inside the outline's end and bottom — the grip sits
in the field's corner, not on the bottom outline a padding in from it — and
its padding puts the text back where it was. */
textarea.field-control {
--field-grip: 0.25rem;
/* The first half-line of the top offset is a margin, not padding: text scrolled up in a full
textarea disappears under the edge instead of running through the label. */
--textarea-clear: 0.5rem;
--textarea-pad-top: calc((var(--field-height) - 1.5rem) / 2 - var(--textarea-clear));
--textarea-pad-bottom: calc((var(--field-height) - 1.5rem) / 2 - var(--field-grip));
margin-block: var(--textarea-clear) var(--field-grip);
margin-inline-end: calc(var(--field-grip) - var(--field-pad));
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
padding-inline-end: calc(var(--field-pad) - var(--field-grip));
resize: vertical;
}
/* `<x-textarea>` grows with what is typed, from `rows` lines up to `max-rows`. Where the browser
cannot size a field to its content, resources/js/field.js sets the height instead. */
textarea.field-control[data-autogrow] {
field-sizing: content;
min-block-size: calc(var(--field-rows, 3) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
max-block-size: calc(var(--field-max-rows, 1000) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
resize: none;
}
/* A select covers its whole field — out over the padding and the leading icon at
the start, under the arrow to the end — and its own padding puts the value back
where it was. So a press anywhere on the field opens it, and the list, which
the browser anchors to the select, is the field's width (components/menu.css). */
select.field-control {
--field-end: calc(var(--field-icon) + var(--field-gap) + var(--field-pad));
appearance: none;
cursor: pointer;
margin-inline: calc(-1 * var(--field-start)) calc(-1 * var(--field-end));
padding-inline: var(--field-start) var(--field-end);
}
select.field-control:disabled {
cursor: default;
}
/* Where the browser has a customizable select, the closed select is its own
button: without the border, padding and arrow that button brings, it is the
same field as before. While its list is open the field reads as focused —
said from `:open`, because focus inside the list is in the top layer — and
the arrow turns over, as the searchable choices' does. */
@supports (appearance: base-select) {
select.field-control {
display: flex;
align-items: center;
border: 0;
padding-block: 0;
border-radius: 0;
background: transparent;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
select.field-control::picker-icon {
display: none;
}
.field-box:has(select.field-control:open) {
--field-edge: var(--md-sys-color-primary);
--field-ink: var(--md-sys-color-primary);
.field-outline {
border-width: 2px;
}
}
.field:has(select.field-control:open) .field-arrow {
rotate: 180deg;
}
}
/* `<x-file>`: the browser's own "No file chosen" line is transparent (the caller shows what was
chosen), and its button is a tonal pill — the thing to press. */
input[type="file"].field-control {
align-self: center;
color: transparent;
cursor: pointer;
}
input[type="file"].field-control::file-selector-button {
height: 2rem;
margin-inline-end: 0.75rem;
padding-inline: 1rem;
border: 0;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
cursor: pointer;
font: var(--md-sys-typescale-label-lg);
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
input[type="file"].field-control:hover::file-selector-button {
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
}
}
input[type="file"].field-control:disabled::file-selector-button {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
cursor: default;
}
/* A combobox's arrow turns over while its list is open, as a select's does. */
.field:has(.field-control[aria-expanded="true"]) .field-arrow {
[data-md-field]:has([data-md-field-control][aria-expanded='true']) [data-md-field-arrow] {
rotate: 180deg;
}
.field-arrow {
[data-md-field-arrow] {
transition: rotate var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
.field-icon,
.field-trailing {
[data-md-field-icon],
[data-md-field-trailing] {
flex: none;
width: var(--field-icon);
height: var(--field-icon);
@@ -257,69 +156,71 @@
}
/* The error's second indicator: M3 asks that the colour change never stand alone. */
.field-error {
[data-md-field-error] {
color: var(--md-sys-color-error);
}
.field-icon,
.field-arrow {
[data-md-field-icon],
[data-md-field-arrow] {
pointer-events: none;
}
/* A trailing button — clear, copy, reveal — is an icon button: the icon in a 40px state layer. */
.field-button {
[data-md-field-button] {
position: relative;
display: grid;
place-items: center;
margin-inline-end: -0.25rem;
margin-inline-end: calc(-1 * var(--md-sys-measurement-space50));
border-radius: var(--md-sys-shape-corner-full);
cursor: pointer;
outline: none;
}
.field-button::before {
content: "";
[data-md-field-button]::before {
content: '';
position: absolute;
inset: 50% auto auto 50%;
width: calc(var(--field-icon) + 1rem);
height: calc(var(--field-icon) + 1rem);
width: calc(var(--field-icon) + var(--md-sys-measurement-space200));
height: calc(var(--field-icon) + var(--md-sys-measurement-space200));
translate: -50% -50%;
border-radius: inherit;
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
.field-button:hover::before {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 8%, transparent);
[data-md-field-button]:hover::before {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
}
}
.field-button:focus-visible::before {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent);
[data-md-field-button]:focus-visible::before {
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: 2px;
}
.field-button:active::before {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent);
[data-md-field-button]:active::before {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
}
.field-affix {
[data-md-field-affix] {
flex: none;
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-lg);
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
}
.field[data-size="xs"] .field-affix {
[data-md-field][data-md-size='xs'] [data-md-field-affix] {
font: var(--md-sys-typescale-body-md);
}
.field-outline {
/* The legend is body-small's 16px line less its 5px above the outline: the fieldset starts
5px up so the notch's edge runs through the middle of the floated label. */
[data-md-field-outline] {
position: absolute;
inset: -0.3125rem 0 0;
margin: 0;
padding: 0 calc(var(--field-pad) - 0.25rem);
padding: 0 calc(var(--field-pad) - var(--md-sys-measurement-space50));
border: 1px solid var(--field-edge);
border-radius: inherit;
pointer-events: none;
@@ -327,7 +228,7 @@
transition: border-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
.field-outline > legend {
[data-md-field-outline] > legend {
display: block;
width: auto;
max-width: 100%;
@@ -341,12 +242,12 @@
transition: max-width var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
.field-outline > legend > span {
[data-md-field-outline] > legend > span {
display: inline-block;
padding-inline: 0.25rem;
padding-inline: var(--md-sys-measurement-space50);
}
.field-label {
[data-md-field-label] {
position: absolute;
inset-inline-start: var(--field-pad);
top: 0;
@@ -366,47 +267,42 @@
/* `required` is marked from the control itself, in the label and in the notch,
so the gap is cut for the asterisk too. */
.field:has(.field-control:required) .field-label::after,
.field:has(.field-control:required) .field-outline > legend > span::after {
content: " *";
[data-md-field]:has([data-md-field-control]:required) [data-md-field-label]::after,
[data-md-field]:has([data-md-field-control]:required) [data-md-field-outline] > legend > span::after {
content: ' *';
}
/* Resting: a label, an empty control, and no focus. The label sits where the
text will go, at the text's size; the notch closes; the placeholder and the
prefix wait, because the label is standing where they would be. A select and
a date always hold a value, so they mark themselves `data-floated`. */
.field:not([data-floated]):has(.field-label):has(.field-control:placeholder-shown):not(:focus-within) {
.field-label {
a date always hold a value, so they mark themselves `data-md-floated`. */
[data-md-field]:not([data-md-floated]):has([data-md-field-label]):has([data-md-field-control]:placeholder-shown):not(:focus-within) {
[data-md-field-label] {
top: 50%;
inset-inline-start: var(--field-start);
font: var(--md-sys-typescale-body-lg);
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
}
.field-outline > legend {
[data-md-field-outline] > legend {
max-width: 0.01px;
}
.field-affix {
[data-md-field-affix] {
visibility: hidden;
}
}
.field[data-size="xs"]:not([data-floated]):has(.field-label):has(.field-control:placeholder-shown):not(:focus-within) .field-label {
[data-md-field][data-md-size='xs']:not([data-md-floated]):has([data-md-field-label]):has([data-md-field-control]:placeholder-shown):not(:focus-within) [data-md-field-label] {
font: var(--md-sys-typescale-body-md);
}
/* A textarea's label rests on its first line, not in the middle of the box. */
.field:not([data-floated]):has(.field-label):has(textarea.field-control:placeholder-shown):not(:focus-within) .field-label {
top: calc(var(--field-height) / 2);
}
.field:has(.field-label):not(:focus-within) .field-control::placeholder {
[data-md-field]:has([data-md-field-label]):not(:focus-within) [data-md-field-control]::placeholder {
color: transparent;
}
/* Clearing is offered only once there is something to clear. */
.field:has(.field-control:placeholder-shown) .field-clear {
[data-md-field]:has([data-md-field-control]:placeholder-shown) [data-md-field-clear] {
display: none;
}
@@ -414,82 +310,82 @@
control no state layer at all, and the disabled edge below is inherited, so a declaration
here would beat it whatever the selector weighs. */
@media (hover: hover) {
.field:not(:has(.field-control:disabled)) .field-box:hover {
[data-md-field]:not(:has([data-md-field-control]:disabled)) [data-md-field-box]:hover {
--field-edge: var(--md-sys-color-on-surface);
}
}
.field-box:focus-within {
[data-md-field-box]:focus-within {
--field-edge: var(--md-sys-color-primary);
--field-ink: var(--md-sys-color-primary);
}
.field-box:focus-within .field-outline {
[data-md-field-box]:focus-within [data-md-field-outline] {
border-width: 2px;
}
.field[data-invalid] {
[data-md-field][data-md-invalid] {
--field-edge: var(--md-sys-color-error);
--field-ink: var(--md-sys-color-error);
}
@media (hover: hover) {
.field[data-invalid]:not(:has(.field-control:disabled)) .field-box:hover {
[data-md-field][data-md-invalid]:not(:has([data-md-field-control]:disabled)) [data-md-field-box]:hover {
--field-edge: var(--md-sys-color-on-error-container);
}
}
.field[data-invalid] .field-box:focus-within {
[data-md-field][data-md-invalid] [data-md-field-box]:focus-within {
--field-edge: var(--md-sys-color-error);
--field-ink: var(--md-sys-color-error);
}
/* `data-readonly` marks a field the caller made read-only — on the field, not read off the
/* `data-md-readonly` marks a field the caller made read-only — on the field, not read off the
control, because a date picker makes its own input read-only too. Nothing here draws it
differently: M3 says a read-only field keeps "the same visual style as an editable field, but
clearly labeled read-only", and the native `readonly` attribute is what carries that. The hook
stays for an application that wants a mark of its own. */
.field:has(.field-control:disabled) {
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
--field-ink: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
[data-md-field]:has([data-md-field-control]:disabled) {
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
--field-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
.field-box {
[data-md-field-box] {
cursor: default;
}
.field-control,
.field-icon,
.field-affix,
.field-trailing {
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
[data-md-field-control],
[data-md-field-icon],
[data-md-field-affix],
[data-md-field-trailing] {
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
}
/* The hint or the error, and — when the field has a maximum — M3's character counter at the far
end of the same row (specs: "Padding between supporting text and counter | 16dp", which the
two paddings between them already make). */
.field-support-row {
two paddings between them already make). Supporting text sits 4px under the field. */
[data-md-field-support-row] {
display: flex;
align-items: baseline;
}
.field-support {
[data-md-field-support] {
min-width: 0;
padding: 0.25rem var(--field-pad) 0;
padding: var(--md-sys-measurement-space50) var(--field-pad) 0;
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
}
.field[data-invalid] .field-support {
[data-md-field][data-md-invalid] [data-md-field-support] {
color: var(--md-sys-color-error);
}
.field-counter {
[data-md-field-counter] {
flex: none;
margin-inline-start: auto;
padding-block-start: 0.25rem;
padding-block-start: var(--md-sys-measurement-space50);
padding-inline-end: var(--field-pad);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-sm);
@@ -498,26 +394,26 @@
}
/* Past the maximum the count is an error, and says so in the error colour. */
.field-counter[data-over] {
[data-md-field-counter][data-md-over] {
color: var(--md-sys-color-error);
}
/* ---- The filled text field ---------------------------------------------------------------- */
.field[data-variant="filled"] .field-box {
[data-md-field][data-md-variant='filled'] [data-md-field-box] {
border-radius: var(--md-sys-shape-corner-xs) var(--md-sys-shape-corner-xs) 0 0;
background-color: var(--md-sys-color-surface-container-highest);
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
.field[data-variant="filled"] .field-box:hover {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, var(--md-sys-color-surface-container-highest));
[data-md-field][data-md-variant='filled'] [data-md-field-box]: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-highest));
}
}
/* The outline becomes the active indicator: one line along the bottom. */
.field[data-variant="filled"] .field-outline {
[data-md-field][data-md-variant='filled'] [data-md-field-outline] {
inset: auto 0 0;
padding: 0;
border: 0;
@@ -525,51 +421,34 @@
border-radius: 0;
}
.field[data-variant="filled"] .field-outline > legend {
[data-md-field][data-md-variant='filled'] [data-md-field-outline] > legend {
display: none;
}
.field[data-variant="filled"] .field-box:focus-within .field-outline {
[data-md-field][data-md-variant='filled'] [data-md-field-box]:focus-within [data-md-field-outline] {
border-block-end-width: 2px;
}
/* The label floats inside the box, above the value, and the value sits below it. */
.field[data-variant="filled"] .field-label {
top: 1rem;
[data-md-field][data-md-variant='filled'] [data-md-field-label] {
top: var(--md-sys-measurement-space200);
inset-inline-start: var(--field-start);
max-width: calc(100% - var(--field-start) - var(--field-pad));
}
.field[data-variant="filled"]:has(.field-label) .field-control {
padding-block: 1.5rem 0.5rem;
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) [data-md-field-control] {
padding-block: var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
}
.field[data-variant="filled"]:has(.field-label) textarea.field-control {
--textarea-clear: 1.5rem;
--textarea-pad-top: 0rem;
--textarea-pad-bottom: calc(0.5rem - var(--field-grip));
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
}
/* A file picker's button is taller than a line of text: it sits lower, and a little smaller. */
.field[data-variant="filled"]:has(.field-label) input[type="file"].field-control {
padding-block: 1.375rem 0.375rem;
}
.field[data-variant="filled"] input[type="file"].field-control::file-selector-button {
height: 1.75rem;
}
.field[data-variant="filled"]:has(.field-label) .field-affix {
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) [data-md-field-affix] {
align-self: stretch;
padding-block: 1.5rem 0.5rem;
padding-block: var(--md-sys-measurement-space300) var(--md-sys-measurement-space100);
}
.field[data-variant="filled"]:has(.field-control:disabled) {
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
[data-md-field][data-md-variant='filled']:has([data-md-field-control]:disabled) {
--field-edge: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
.field-box {
[data-md-field-box] {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 4%, transparent);
}
}
+62
View File
@@ -0,0 +1,62 @@
/*
* <x-file>: the browser's file input in the text field's chrome
* (resources/views/components/file.blade.php).
*
* The chrome is the field's (components/field.css); this file restyles only what the browser
* draws. Its "No file chosen" line is transparent, because it can be neither truncated nor wrapped
* and the caller shows what was chosen; its `::file-selector-button` is a tonal pill — a 32px
* button in secondary-container with label-large, 16px padding, 12px before the line — so it reads
* as the thing to press, with M3's hover state layer and disabled opacities (tokens/state.css). In
* a filled field the button sits under the floating label, 28px tall.
*
* The field's root carries `data-md-file`.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './field.css';
@layer material.components {
/* `<x-file>`: the browser's own "No file chosen" line is transparent (the caller shows what was
chosen), and its button is a tonal pill — the thing to press. */
input[type='file'][data-md-field-control] {
align-self: center;
color: transparent;
cursor: pointer;
}
input[type='file'][data-md-field-control]::file-selector-button {
height: 32px;
margin-inline-end: 12px;
padding-inline: var(--md-sys-measurement-space200);
border: 0;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
cursor: pointer;
font: var(--md-sys-typescale-label-lg);
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
input[type='file'][data-md-field-control]:hover::file-selector-button {
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-sys-color-secondary-container));
}
}
input[type='file'][data-md-field-control]:disabled::file-selector-button {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
cursor: default;
}
/* A file picker's button is taller than a line of text: it sits lower, and a little smaller. */
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) input[type='file'][data-md-field-control] {
padding-block: 22px var(--md-sys-measurement-space75);
}
[data-md-field][data-md-variant='filled'] input[type='file'][data-md-field-control]::file-selector-button {
height: 28px;
}
}
+37
View File
@@ -0,0 +1,37 @@
/*
* <x-form>: its fields in one column, and its actions at the foot
* (resources/views/components/form.blade.php).
*
* The column is a one-track grid whose track is `minmax(0, 1fr)`: a bare grid's implicit column
* floors at its content's min-content, so on a phone a wide field would push the form past its
* pane. Rows take their content's height. 16px (`space200`) between fields, because a text field
* floats its label half above its outline and any less lets the hint under one field run into the
* label of the next.
*
* The actions wrap, end-aligned, 8px (`space100`) apart — M3's gap between buttons in a row. A
* caller's class on the `actions` slot lands on the row and outranks this layer.
*
* [data-md-form] the <form>
* [data-md-form-actions] the row of actions, under the divider when `separator` asks for one
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
/* TODO(step 36): @import './divider.css' once <x-divider> leaves Tailwind (containment stream). */
@layer material.components {
[data-md-form] {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-auto-rows: min-content;
gap: var(--md-sys-measurement-space200);
}
[data-md-form-actions] {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: var(--md-sys-measurement-space100);
}
}
+13
View File
@@ -0,0 +1,13 @@
/*
* <x-input>: M3's one-line text field (resources/views/components/input.blade.php).
*
* Nothing of its own to draw: the outlined and filled chrome, the floating label, the prefix and
* suffix, the clear and copy buttons and the character counter are the field's
* (components/field.css), and its icons are <x-icon>s. The field's root carries `data-md-input`
* for an application that wants to tell a one-line field from the others.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './field.css';
@import './icon.css';
+23 -23
View File
@@ -6,7 +6,7 @@
* - `<x-select>`'s: the native <select>, opted into the browser's customizable select
* (`appearance: base-select`). `::picker(select)` is the menu and each <option> a row, while the
* keyboard, type-to-find, the screen reader and the form value stay the browser's own.
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `.field-menu` and `.field-option`.
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `[data-md-field-menu]` and `[data-md-field-option]`.
*
* M3's menu as its tokens have it: surface-container at elevation 2, extra-small corner, 48px rows in
* body-large, the chosen row in secondary-container with a tick at its end (so it is not told by colour
@@ -149,7 +149,7 @@
}
}
.field-menu {
[data-md-field-menu] {
max-block-size: 18rem;
overflow-y: auto;
padding-block: 0.5rem;
@@ -160,7 +160,7 @@
/* As a popover (`<x-choices searchable>`), it hangs under its field, as wide, and goes above only
when there is no room below. */
.field-menu[popover] {
[data-md-field-menu][popover] {
inset: auto;
top: anchor(bottom);
left: anchor(left);
@@ -172,12 +172,12 @@
position-try-fallbacks: flip-block;
}
.field-option[aria-disabled="true"] {
[data-md-field-option][aria-disabled="true"] {
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
cursor: default;
}
.field-option {
[data-md-field-option] {
display: flex;
align-items: center;
gap: 0.75rem;
@@ -190,30 +190,30 @@
}
/* `data-active` is the row the arrow keys or the pointer are on. */
.field-option[data-active] {
[data-md-field-option][data-active] {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}
.field-option[aria-selected="true"] {
[data-md-field-option][aria-selected="true"] {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
.field-option[aria-selected="true"][data-active] {
[data-md-field-option][aria-selected="true"][data-active] {
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
}
.field-check {
[data-md-field-check] {
margin-inline-start: auto;
}
.field-option:not([aria-selected="true"]) .field-check {
[data-md-field-option]:not([aria-selected="true"]) [data-md-field-check] {
visibility: hidden;
}
@supports (appearance: base-select) {
select.field-control,
select.field-control::picker(select) {
select[data-md-field-control],
select[data-md-field-control]::picker(select) {
appearance: base-select;
}
@@ -223,7 +223,7 @@
order tries the roomier side first, which opened a menu upwards with room to
spare under it. It opens with a fade and a grow on the spatial spring and
closes with a fade, as `<x-menu>` does. */
select.field-control::picker(select) {
select[data-md-field-control]::picker(select) {
position-try-order: normal;
max-block-size: 18rem;
max-inline-size: calc(100vw - 2rem);
@@ -242,7 +242,7 @@
transition-behavior: allow-discrete;
}
select.field-control:open::picker(select) {
select[data-md-field-control]:open::picker(select) {
opacity: 1;
transition-property: opacity, scale, display, overlay;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
@@ -250,13 +250,13 @@
}
@starting-style {
select.field-control:open::picker(select) {
select[data-md-field-control]:open::picker(select) {
opacity: 0;
scale: 0.95;
}
}
select.field-control option {
select[data-md-field-control] option {
display: flex;
align-items: center;
gap: 0.75rem;
@@ -272,31 +272,31 @@
}
@media (hover: hover) {
select.field-control option:hover {
select[data-md-field-control] option:hover {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}
}
select.field-control option:focus-visible {
select[data-md-field-control] option:focus-visible {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
}
select.field-control option:checked {
select[data-md-field-control] option:checked {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
@media (hover: hover) {
select.field-control option:checked:hover {
select[data-md-field-control] option:checked:hover {
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
}
}
select.field-control option:checked:focus-visible {
select[data-md-field-control] option:checked:focus-visible {
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 10%, var(--md-sys-color-secondary-container));
}
select.field-control option:disabled {
select[data-md-field-control] option:disabled {
background: transparent;
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
cursor: default;
@@ -305,7 +305,7 @@
/* The tick, at the row's end: the browser draws it before the label, and hides
it on every row but the chosen one. The Material Symbol, as a mask, so it
takes the row's ink. */
select.field-control option::checkmark {
select[data-md-field-control] option::checkmark {
content: "";
order: 1;
flex: none;
+12
View File
@@ -0,0 +1,12 @@
/*
* <x-password>: a text field with M3's trailing eye (resources/views/components/password.blade.php).
*
* Nothing of its own to draw: the chrome and the eye, a trailing icon button, are the field's
* (components/field.css), and the eye's two glyphs are <x-icon>s. The field's root carries
* `data-md-password`.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './field.css';
@import './icon.css';
+133
View File
@@ -0,0 +1,133 @@
/*
* <x-radio>: M3's radio buttons (resources/views/components/radio.blade.php), on the row the
* selection controls share (components/selection.css).
*
* RadioButtonTokens (androidx Compose Material 3, Apache-2.0): a 20px ring with a 2px
* on-surface-variant outline, primary once chosen, and a 10px primary dot that grows in on the fast
* spatial spring; the state layer a 40px circle around it. With the server's error the ring and
* the dot are error. Disabled, both are on-surface at 38%. Beside a hint the ring drops 2px, to sit
* on the label's first line.
*
* The group is a <fieldset> whose legend (label-large, on-surface-variant, 12px above the options)
* is the question. The options stack 16px apart, as M3 lays radio buttons out; `data-md-inline`
* lays them in a wrapping row 24px apart from `medium` (600px), and compact always stacks. The
* hint, or the errors in its place, sits 8px under the options.
*
* [data-md-radio] the <fieldset>; `class` and `style` land here; data-md-inline
* [data-md-radio-legend]
* [data-md-radio-options]
* [data-md-selection-row]
* [data-md-radio-button] the ring: input, [data-md-radio-dot]
* [data-md-selection-support] the hint, or the errors
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './selection.css';
@layer material.components {
[data-md-radio] {
min-width: 0;
}
[data-md-radio-legend] {
margin-bottom: 12px;
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;
}
[data-md-radio-options] {
display: grid;
gap: var(--md-sys-measurement-space200);
}
/* M3 stacks radio buttons and cautions against a row at any width; `inline` is for two or three
short labels, and only from medium. */
@media (width >= 600px) {
[data-md-radio][data-md-inline] [data-md-radio-options] {
display: flex;
flex-wrap: wrap;
column-gap: var(--md-sys-measurement-space300);
}
}
[data-md-radio] [data-md-selection-support] {
margin-top: var(--md-sys-measurement-space100);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
font-variation-settings: normal;
}
[data-md-radio] [data-md-selection-support][role='alert'] {
color: var(--md-sys-color-error);
}
[data-md-radio-button] {
position: relative;
display: grid;
flex: none;
place-items: center;
width: 20px;
height: 20px;
}
[data-md-radio-button] > * {
grid-area: 1 / 1;
}
[data-md-selection-row]:has([data-md-selection-hint]) [data-md-radio-button] {
margin-top: var(--md-sys-measurement-space25);
}
[data-md-radio-button]::before {
inset: -10px;
}
[data-md-radio-button]:has(input:focus-visible)::before {
outline: 3px solid var(--md-sys-color-secondary);
}
[data-md-radio-button] input {
width: 20px;
height: 20px;
border: 2px solid var(--md-sys-color-on-surface-variant);
border-radius: var(--md-sys-shape-corner-full);
}
[data-md-radio-button] [data-md-radio-dot] {
width: 10px;
height: 10px;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-primary);
scale: 0;
pointer-events: none;
transition: scale var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
}
[data-md-radio-button] input:checked {
border-color: var(--md-sys-color-primary);
}
[data-md-radio-button] input:checked ~ [data-md-radio-dot] {
scale: 1;
}
[data-md-selection-row][data-md-invalid] [data-md-radio-button] input {
border-color: var(--md-sys-color-error);
}
[data-md-selection-row][data-md-invalid] [data-md-radio-button] [data-md-radio-dot] {
background-color: var(--md-sys-color-error);
}
[data-md-radio-button] input:disabled {
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
[data-md-radio-button] input:disabled ~ [data-md-radio-dot] {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
}
+94 -74
View File
@@ -12,36 +12,46 @@
* 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: 22.5rem` on a wrapper. The leading and trailing padding is 24px unfocused and
* `--search-width: 360px` on a wrapper. The leading and trailing padding is 24px unfocused and
* 16px focused, as the search specs table gives it.
*
* [data-search] the root; data-open, data-full-screen, data-trigger
* [data-search-scrim] over the page while the view is docked
* [data-search-trigger] data-trigger="icon" only: the icon button that expands the view
* [data-search-bar] the pill, above the view
* [data-search-leading], [data-search-field] (the combobox around [data-search-input]),
* [data-search-clear], [data-search-trailing]
* [data-search-view] the container behind the bar
* [data-search-suggestions] before the first keystroke
* [data-search-results] once something is typed
* [data-search-status] the polite live region that counts whichever list is on screen
* 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 components {
[data-search] {
--search-height: 3.5rem;
@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: 45rem;
--search-open-width: 720px;
position: relative;
}
[data-search][data-open] {
[data-md-search][data-md-open] {
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-search-scrim] {
[data-md-search-scrim] {
position: fixed;
inset: 0;
z-index: -1;
@@ -53,20 +63,20 @@
}
@starting-style {
[data-search-scrim] {
[data-md-search-scrim] {
opacity: 0;
}
}
[data-search-bar] {
[data-md-search-bar] {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 0.25rem;
gap: var(--md-sys-measurement-space50);
width: min(100%, var(--search-width));
height: var(--search-height);
padding-inline: 0.75rem;
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);
@@ -77,70 +87,70 @@
}
/* Focused: wider, and the padding drops from 24px to the contained style's 16px. */
[data-search][data-open] [data-search-bar] {
[data-md-search][data-md-open] [data-md-search-bar] {
width: min(100%, var(--search-open-width));
padding-inline: 0.25rem;
padding-inline: var(--md-sys-measurement-space50);
}
@media (hover: hover) {
[data-search]:not([data-open]) [data-search-bar]:hover {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, var(--md-sys-color-surface-container-high));
[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-search][data-open] [data-search-bar] {
[data-md-search][data-md-open] [data-md-search-bar] {
background-color: transparent;
}
[data-search-leading],
[data-search-clear] {
[data-md-search-leading],
[data-md-search-clear] {
display: grid;
flex: none;
place-items: center;
width: 3rem;
height: 3rem;
width: var(--md-sys-measurement-space600);
height: var(--md-sys-measurement-space600);
border-radius: var(--md-sys-shape-corner-full);
}
[data-search-leading] {
[data-md-search-leading] {
color: var(--md-sys-color-on-surface);
}
[data-search-clear] {
[data-md-search-clear] {
color: var(--md-sys-color-on-surface-variant);
cursor: pointer;
}
[data-search-bar] button {
[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-search-bar] button:hover {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 8%, transparent);
[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-search-bar] button:focus-visible {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, 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-search-field] {
[data-md-search-field] {
display: flex;
flex: 1 1 0%;
min-width: 0;
height: 100%;
}
[data-search-input] {
[data-md-search-input] {
flex: 1 1 0%;
min-width: 0;
height: 100%;
padding-inline: 0.25rem;
padding-inline: var(--md-sys-measurement-space50);
appearance: none;
background: transparent;
color: var(--md-sys-color-on-surface);
@@ -150,41 +160,41 @@
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
}
[data-search-input]::placeholder {
[data-md-search-input]::placeholder {
color: var(--md-sys-color-on-surface-variant);
opacity: 1;
}
[data-search-input]::-webkit-search-cancel-button,
[data-search-input]::-webkit-search-decoration {
[data-md-search-input]::-webkit-search-cancel-button,
[data-md-search-input]::-webkit-search-decoration {
appearance: none;
display: none;
}
[data-search-bar]:has([data-search-input]:placeholder-shown) [data-search-clear] {
[data-md-search-bar]:has([data-md-search-input]:placeholder-shown) [data-md-search-clear] {
display: none;
}
[data-search-trailing] {
[data-md-search-trailing] {
display: flex;
flex: none;
align-items: center;
gap: 0.25rem;
padding-inline-end: 0.25rem;
gap: var(--md-sys-measurement-space50);
padding-inline-end: var(--md-sys-measurement-space50);
}
[data-search][data-open] [data-search-trailing] {
[data-md-search][data-md-open] [data-md-search-trailing] {
display: none;
}
[data-search-view] {
[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(40rem, 70dvh);
max-height: min(640px, 70dvh);
padding-top: var(--search-height);
overflow: hidden;
border-radius: var(--md-sys-shape-corner-xl);
@@ -198,7 +208,7 @@
}
@starting-style {
[data-search-view] {
[data-md-search-view] {
opacity: 0;
scale: 1 0.9;
}
@@ -206,35 +216,35 @@
/* 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-search-view]:has([data-search-results], [data-search-suggestions]) {
min-height: 15rem;
[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-search-results],
[data-search-suggestions] {
[data-md-search-results],
[data-md-search-suggestions] {
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding-block: 0.5rem;
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-search][data-trigger="icon"] {
[data-md-search][data-md-trigger='icon'] {
flex: none;
width: 3rem;
height: 3rem;
width: var(--md-sys-measurement-space600);
height: var(--md-sys-measurement-space600);
}
[data-search-trigger] {
[data-md-search-trigger] {
display: grid;
place-items: center;
width: 3rem;
height: 3rem;
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;
@@ -243,45 +253,55 @@
}
@media (hover: hover) {
[data-search-trigger]:hover {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 8%, transparent);
[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-search-trigger]:focus-visible {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, 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-search-trigger]:active {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent);
[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-search][data-trigger="icon"]:not([data-open]) [data-search-bar] {
[data-md-search][data-md-trigger='icon']:not([data-md-open]) [data-md-search-bar] {
display: none;
}
/* Full screen, on a compact window. */
[data-search][data-full-screen] [data-search-bar] {
[data-md-search][data-md-full-screen] [data-md-search-bar] {
position: fixed;
inset: 0 0 auto;
width: auto;
height: calc(4.5rem + var(--material-safe-top, env(safe-area-inset-top)));
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: 0.25rem;
padding-inline: var(--md-sys-measurement-space50);
border-radius: 0;
}
[data-search][data-full-screen] [data-search-view] {
[data-md-search][data-md-full-screen] [data-md-search-view] {
position: fixed;
inset: 0;
max-width: none;
max-height: none;
padding-top: calc(4.5rem + var(--material-safe-top, env(safe-area-inset-top)));
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;
}
}
+74
View File
@@ -0,0 +1,74 @@
/*
* <x-select>: the native <select> in the text field's chrome, drawn as M3's exposed dropdown menu
* (resources/views/components/select.blade.php).
*
* The chrome is the field's (components/field.css); this file is what a <select> changes in it. The
* select covers its whole field, so a press anywhere opens it and the list the browser anchors to
* it is the field's width. Where the browser has the customizable select (`appearance:
* base-select`), the closed select loses the button chrome that brings, the field reads as focused
* while the list is open, and the arrow turns over. The open list itself is M3's menu, drawn by
* components/menu.css.
*
* The field's root carries `data-md-select`.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './field.css';
@import './icon.css';
/* TODO(step 36): @import './menu.css' once the menu leaves Tailwind (actions stream); its select rules draw the open list. */
@layer material.components {
/* A select covers its whole field — out over the padding and the leading icon at
the start, under the arrow to the end — and its own padding puts the value back
where it was. So a press anywhere on the field opens it, and the list, which
the browser anchors to the select, is the field's width (components/menu.css). */
select[data-md-field-control] {
--field-end: calc(var(--field-icon) + var(--field-gap) + var(--field-pad));
appearance: none;
cursor: pointer;
margin-inline: calc(-1 * var(--field-start)) calc(-1 * var(--field-end));
padding-inline: var(--field-start) var(--field-end);
}
select[data-md-field-control]:disabled {
cursor: default;
}
/* Where the browser has a customizable select, the closed select is its own
button: without the border, padding and arrow that button brings, it is the
same field as before. While its list is open the field reads as focused —
said from `:open`, because focus inside the list is in the top layer — and
the arrow turns over, as the searchable choices' does. */
@supports (appearance: base-select) {
select[data-md-field-control] {
display: flex;
align-items: center;
border: 0;
padding-block: 0;
border-radius: 0;
background: transparent;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
select[data-md-field-control]::picker-icon {
display: none;
}
[data-md-field-box]:has(select[data-md-field-control]:open) {
--field-edge: var(--md-sys-color-primary);
--field-ink: var(--md-sys-color-primary);
[data-md-field-outline] {
border-width: 2px;
}
}
[data-md-field]:has(select[data-md-field-control]:open) [data-md-field-arrow] {
rotate: 180deg;
}
}
}
+67 -296
View File
@@ -1,70 +1,90 @@
/*
* M3's selection controls: the checkbox, the radio button and the switch (CheckboxTokens,
* RadioButtonTokens, SwitchTokens, androidx Compose Material 3, Apache-2.0).
* What M3's three selection controls share: the checkbox (components/checkbox.css), the radio button
* (components/radio.css) and the switch (components/toggle.css) — CheckboxTokens, RadioButtonTokens,
* SwitchTokens, androidx Compose Material 3, Apache-2.0. Each control's stylesheet imports this one.
*
* Each is a real `<input>` with its appearance removed and drawn over, so focus, the keyboard
* (Space; the arrow keys between radios), the form value, `wire:model` and what a screen reader
* announces stay the browser's. The row around it is a `<label>`, so its text is pressable too.
* Hooks are data attributes, not class names: `checkbox`, `radio` and `toggle` are daisyUI's, which
* the design guard rejects.
* announces stay the browser's. The row around it is a `<label>`, so its text is pressable too:
*
* [data-selection] the <label> row; `data-invalid` when the server has an error for it
* [data-checkbox] the 18px box, its state layer a 40px circle around it
* input, [data-check], [data-mixed]
* [data-radio] the 20px ring and its 10px dot
* input, [data-dot]
* [data-switch] the 52×32 track; `data-icons` = "both" or "selected"
* input, [data-handle] (with [data-on] and [data-off] icons)
* [data-md-selection-row] the <label>: the control and its text, 16px apart; with a hint
* the control aligns to the first line; `data-md-right` puts the
* control at the end; `data-md-invalid` when the server has an error
* [data-md-checkbox-box] | [data-md-radio-button] | [data-md-switch] the control
* [data-md-selection-text] the label (body-large, on-surface) and the hint under it
* [data-md-selection-label], [data-md-selection-hint]
*
* Every state layer is on-surface when unselected and primary when selected, 8% on hover (only
* where a pointer can hover) and 10% on focus and press; the focus indicator is the package's
* secondary ring. A disabled control is on-surface at 38% and has no state layer.
* Every state layer is on-surface when unselected and primary when selected, at M3's hover opacity
* (only where a pointer can hover) and its focus and press opacities (tokens/state.css); the focus
* indicator is the package's secondary ring. A disabled control is on-surface at 38% and has no
* state layer.
*
* The state layer is a ::before with no pointer events, so it is not a target: a labelled control
* is pressed anywhere along its row, and a control with no label wears the `touch-target` utility
* (resources/css/tokens/state.css) so its box catches presses over M3's 48px minimum.
* is pressed anywhere along its row, and a control with no label wears `md-touch-target`
* (foundation/interaction.css) so its box catches presses over M3's 48px minimum.
*/
@layer components {
[data-selection] {
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@layer material.components {
[data-md-selection-row] {
/* The state layer's tint. */
--selection-ink: var(--md-sys-color-on-surface);
display: flex;
align-items: center;
gap: var(--md-sys-measurement-space200);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
[data-selection]:has(input:disabled) {
[data-md-selection-row]:has([data-md-selection-hint]) {
align-items: flex-start;
}
[data-md-selection-row][data-md-right] {
flex-direction: row-reverse;
justify-content: space-between;
}
[data-md-selection-row]:has(input:disabled) {
cursor: default;
}
[data-selection]:has(input:disabled) [data-selection-label] {
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
[data-md-selection-text] {
min-width: 0;
}
[data-checkbox],
[data-radio] {
position: relative;
display: grid;
flex: none;
place-items: center;
[data-md-selection-label] {
display: block;
color: var(--md-sys-color-on-surface);
font: var(--md-sys-typescale-body-lg);
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
font-variation-settings: normal;
}
[data-checkbox] > *,
[data-radio] > * {
grid-area: 1 / 1;
[data-md-selection-hint] {
display: block;
margin-top: var(--md-sys-measurement-space25);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
font-variation-settings: normal;
}
[data-checkbox]::before,
[data-radio]::before,
[data-handle]::before {
content: "";
[data-md-selection-row]:has(input:disabled) [data-md-selection-label] {
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
content: '';
position: absolute;
border-radius: var(--md-sys-shape-corner-full);
pointer-events: none;
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
[data-checkbox] input,
[data-radio] input,
[data-switch] input {
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch]) input {
margin: 0;
appearance: none;
cursor: inherit;
@@ -74,275 +94,26 @@
transition-timing-function: var(--md-sys-motion-effects-fast);
}
/* The state layer. `--selection-ink` is what it is tinted with. */
[data-selection] {
--selection-ink: var(--md-sys-color-on-surface);
}
[data-selection]:has(input:checked),
[data-selection]:has(input:indeterminate) {
[data-md-selection-row]:has(input:checked),
[data-md-selection-row]:has(input:indeterminate) {
--selection-ink: var(--md-sys-color-primary);
}
[data-selection][data-invalid] {
[data-md-selection-row][data-md-invalid] {
--selection-ink: var(--md-sys-color-error);
}
@media (hover: hover) {
[data-selection]:hover:not(:has(input:disabled)) :is([data-checkbox], [data-radio], [data-handle])::before {
background-color: color-mix(in srgb, var(--selection-ink) 8%, transparent);
[data-md-selection-row]:hover:not(:has(input:disabled)) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
}
}
[data-selection]:has(input:focus-visible) :is([data-checkbox], [data-radio], [data-handle])::before,
[data-selection]:not(:has(input:disabled)):active :is([data-checkbox], [data-radio], [data-handle])::before {
background-color: color-mix(in srgb, var(--selection-ink) 10%, transparent);
[data-md-selection-row]:has(input:focus-visible) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
}
[data-checkbox]:has(input:focus-visible)::before,
[data-radio]:has(input:focus-visible)::before {
outline: 3px solid var(--md-sys-color-secondary);
}
/* ---- Checkbox ---------------------------------------------------------------------------- */
[data-checkbox] {
width: 1.125rem;
height: 1.125rem;
}
[data-checkbox]::before {
inset: -0.6875rem;
}
[data-checkbox] input {
width: 1.125rem;
height: 1.125rem;
border: 2px solid var(--md-sys-color-on-surface-variant);
border-radius: 2px;
}
[data-checkbox] input:is(:checked, :indeterminate) {
border-color: var(--md-sys-color-primary);
background-color: var(--md-sys-color-primary);
}
[data-checkbox] :is([data-check], [data-mixed]) {
color: var(--md-sys-color-on-primary);
opacity: 0;
scale: 0.5;
pointer-events: none;
transition-property: opacity, scale;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
[data-checkbox] input:checked:not(:indeterminate) ~ [data-check],
[data-checkbox] input:indeterminate ~ [data-mixed] {
opacity: 1;
scale: 1;
}
[data-selection][data-invalid] [data-checkbox] input {
border-color: var(--md-sys-color-error);
}
[data-selection][data-invalid] [data-checkbox] input:is(:checked, :indeterminate) {
background-color: var(--md-sys-color-error);
}
[data-selection][data-invalid] [data-checkbox] :is([data-check], [data-mixed]) {
color: var(--md-sys-color-on-error);
}
[data-checkbox] input:disabled {
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
}
[data-checkbox] input:disabled:is(:checked, :indeterminate) {
border-color: transparent;
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
}
[data-checkbox] input:disabled ~ :is([data-check], [data-mixed]) {
color: var(--md-sys-color-surface);
}
/* ---- Radio button ------------------------------------------------------------------------ */
[data-radio] {
width: 1.25rem;
height: 1.25rem;
}
[data-radio]::before {
inset: -0.625rem;
}
[data-radio] input {
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(--md-sys-color-on-surface-variant);
border-radius: var(--md-sys-shape-corner-full);
}
[data-radio] [data-dot] {
width: 0.625rem;
height: 0.625rem;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-primary);
scale: 0;
pointer-events: none;
transition: scale var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
}
[data-radio] input:checked {
border-color: var(--md-sys-color-primary);
}
[data-radio] input:checked ~ [data-dot] {
scale: 1;
}
[data-selection][data-invalid] [data-radio] input {
border-color: var(--md-sys-color-error);
}
[data-selection][data-invalid] [data-radio] [data-dot] {
background-color: var(--md-sys-color-error);
}
[data-radio] input:disabled {
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
}
[data-radio] input:disabled ~ [data-dot] {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
}
/* ---- Switch -------------------------------------------------------------------------------
*
* The handle's centre is 16px from the track's start unselected and 36px selected; it is 16px
* across unselected, 24px with an icon or selected, and 28px while pressed. The positions below
* are those centres less half the size, measured inside the track's 2px outline. */
[data-switch] {
position: relative;
display: grid;
flex: none;
width: 3.25rem;
height: 2rem;
}
[data-switch] input {
width: 100%;
height: 100%;
border: 2px solid var(--md-sys-color-outline);
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-highest);
}
[data-switch] input:focus-visible {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
[data-handle] {
--handle: 1rem;
position: absolute;
top: 50%;
inset-inline-start: calc(1rem - var(--handle) / 2);
display: grid;
place-items: center;
width: var(--handle);
height: var(--handle);
translate: 0 -50%;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-outline);
color: var(--md-sys-color-surface-container-highest);
pointer-events: none;
transition-property: inset-inline-start, width, height, background-color;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
[data-handle]::before {
inset: 50% auto auto 50%;
width: 2.5rem;
height: 2.5rem;
translate: -50% -50%;
}
[data-handle] > svg {
grid-area: 1 / 1;
opacity: 0;
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
[data-switch][data-icons="both"] [data-handle] {
--handle: 1.5rem;
}
[data-switch][data-icons="both"] input:not(:checked) ~ [data-handle] [data-off],
[data-switch][data-icons] input:checked ~ [data-handle] [data-on] {
opacity: 1;
}
[data-switch] input:checked {
border-color: var(--md-sys-color-primary);
background-color: var(--md-sys-color-primary);
}
[data-switch] input:checked ~ [data-handle] {
--handle: 1.5rem;
inset-inline-start: calc(2.25rem - var(--handle) / 2);
background-color: var(--md-sys-color-on-primary);
color: var(--md-sys-color-on-primary-container);
}
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
--handle: 1.75rem;
}
@media (hover: hover) {
[data-selection]:hover [data-switch] input:not(:disabled) ~ [data-handle] {
background-color: var(--md-sys-color-on-surface-variant);
}
[data-selection]:hover [data-switch] input:checked:not(:disabled) ~ [data-handle] {
background-color: var(--md-sys-color-primary-container);
}
}
[data-switch] input:focus-visible ~ [data-handle],
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
background-color: var(--md-sys-color-on-surface-variant);
}
[data-switch] input:checked:focus-visible ~ [data-handle],
[data-selection]:active [data-switch] input:checked:not(:disabled) ~ [data-handle] {
background-color: var(--md-sys-color-primary-container);
}
[data-switch] input:disabled {
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
background-color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 12%, transparent);
}
[data-switch] input:disabled ~ [data-handle] {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 38%, transparent);
}
[data-switch] input:disabled:checked {
border-color: transparent;
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
}
[data-switch] input:disabled:checked ~ [data-handle] {
background-color: var(--md-sys-color-surface);
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
[data-md-selection-row]:not(:has(input:disabled)):active :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
}
}
+403
View File
@@ -0,0 +1,403 @@
/*
* <x-slider>: M3 Expressive's slider (resources/views/components/slider.blade.php), drawn over native
* range inputs by resources/js/slider.js, which places every part with inline `left`, `width` and
* `border-radius`; this file gives the parts their size, colour and states.
*
* From androidx Compose Material 3 at commit 27cf9a7 (Slider.kt, SliderTokens.kt, Apache-2.0) and
* M3's sliders/specs table: a 4px handle, 2px while pressed or focused and 6px shorter while its
* focus ring shows (Material Components' m3_slider_focus_ring_thumb_height_decrease); 4px stop
* indicators and ticks; the active track and the handle in the colour, the inactive track in its
* container, and a stop, a tick or the inset icon in the "on" colour of the part of the track it
* sits on. Disabled, the active parts are on-surface at 38% and the inactive track, and the ticks
* over the active track, at 12% (tokens/state.css). The value label is M3's label container: 44px
* tall and at least 48px wide in inverse-surface with label-large, 4px above the handle, growing in
* on the fast effects spring while the handle is pressed or focused (`data-md-value-label="drag"`).
*
* Expressive's sizes, by `data-md-size`, as custom properties on the root:
*
* size track handle (and the slider across) inset icon
* xs 16px 44px (the slider 48px) —
* sm 24px 44px (48px) —
* md 40px 52px 24px
* lg 56px 68px 24px
* xl 96px 108px 32px
*
* `data-md-orientation="vertical"` turns the same drawing a quarter inside a size container: the
* slider is as wide as a horizontal one is tall and 192px long unless the root's height says
* otherwise; the value label turns back so it reads across, beside the handle. A right-to-left page
* mirrors a horizontal drawing and turns its label and icon back. Without script the inputs show as
* native ranges in the colour.
*
* [data-md-slider] the root; data-md-size, data-md-color, data-md-value-label,
* data-md-orientation, data-md-centered
* [data-md-slider-label]
* [data-md-slider-control] the element the pointer presses; holds the inputs
* [data-md-slider-drawing]
* [data-md-slider-track]
* [data-md-slider-segment="start|active|end"], [data-md-slider-tick] (data-md-active),
* [data-md-slider-stop="start|end"], [data-md-slider-icon] (data-md-active)
* [data-md-slider-handle="start|end"] (data-md-pressed, data-md-focused)
* [data-md-slider-thumb], [data-md-slider-value-anchor] > [data-md-slider-value]
* [data-md-slider-input="start|end"]
* [data-md-slider-hint] | [data-md-slider-errors]
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './icon.css';
@layer material.components {
[data-md-slider] {
--slider-active: var(--md-sys-color-primary);
--slider-inactive: var(--md-sys-color-secondary-container);
--slider-on-active: var(--md-sys-color-on-primary);
--slider-on-inactive: var(--md-sys-color-on-secondary-container);
--slider-disabled-active: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
--slider-disabled-inactive: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
--slider-across: 48px;
--slider-track: 16px;
--slider-handle: 44px;
width: 100%;
min-width: 0;
}
[data-md-slider][data-md-size='sm'] {
--slider-track: 24px;
}
[data-md-slider][data-md-size='md'] {
--slider-across: 52px;
--slider-track: 40px;
--slider-handle: 52px;
}
[data-md-slider][data-md-size='lg'] {
--slider-across: 68px;
--slider-track: 56px;
--slider-handle: 68px;
}
[data-md-slider][data-md-size='xl'] {
--slider-across: 108px;
--slider-track: 96px;
--slider-handle: 108px;
}
[data-md-slider][data-md-color='secondary'] {
--slider-active: var(--md-sys-color-secondary);
--slider-on-active: var(--md-sys-color-on-secondary);
}
[data-md-slider][data-md-color='tertiary'] {
--slider-active: var(--md-sys-color-tertiary);
--slider-inactive: var(--md-sys-color-tertiary-container);
--slider-on-active: var(--md-sys-color-on-tertiary);
--slider-on-inactive: var(--md-sys-color-on-tertiary-container);
}
[data-md-slider][data-md-color='error'] {
--slider-active: var(--md-sys-color-error);
--slider-inactive: var(--md-sys-color-error-container);
--slider-on-active: var(--md-sys-color-on-error);
--slider-on-inactive: var(--md-sys-color-on-error-container);
}
[data-md-slider][data-md-color='success'] {
--slider-active: var(--md-sys-color-success);
--slider-inactive: var(--md-sys-color-success-container);
--slider-on-active: var(--md-sys-color-on-success);
--slider-on-inactive: var(--md-sys-color-on-success-container);
}
[data-md-slider][data-md-color='warning'] {
--slider-active: var(--md-sys-color-warning);
--slider-inactive: var(--md-sys-color-warning-container);
--slider-on-active: var(--md-sys-color-on-warning);
--slider-on-inactive: var(--md-sys-color-on-warning-container);
}
[data-md-slider][data-md-color='info'] {
--slider-active: var(--md-sys-color-info);
--slider-inactive: var(--md-sys-color-info-container);
--slider-on-active: var(--md-sys-color-on-info);
--slider-on-inactive: var(--md-sys-color-on-info-container);
}
/* Disabled: the colours give way to on-surface at M3's disabled opacities. */
[data-md-slider]:has([data-md-slider-input]:disabled) {
--slider-active: var(--slider-disabled-active);
--slider-inactive: var(--slider-disabled-inactive);
--slider-on-active: var(--slider-disabled-inactive);
--slider-on-inactive: var(--slider-disabled-active);
}
[data-md-slider][data-md-orientation='vertical'] {
display: flex;
flex-direction: column;
width: fit-content;
}
[data-md-slider-label] {
display: block;
margin-bottom: var(--md-sys-measurement-space100);
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;
}
[data-md-slider-control] {
position: relative;
height: var(--slider-across);
cursor: pointer;
touch-action: pan-y;
-webkit-user-select: none;
user-select: none;
}
[data-md-slider-control]:has(:disabled) {
cursor: not-allowed;
}
/* Standing up: as wide as a lying slider is tall, 192px long unless the root says otherwise, and
a size container, which lets the drawing be as long as this is tall. */
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-control] {
flex: auto;
width: var(--slider-across);
height: 192px;
min-height: 0;
container-type: size;
touch-action: pan-x;
}
/* Room for a label that never hides: half the handle, 4px and the 44px pill — above the track
lying down, beside it standing up. */
[data-md-slider][data-md-value-label='always'] [data-md-slider-control] {
margin-top: var(--md-sys-measurement-space600);
}
[data-md-slider][data-md-value-label='always'][data-md-orientation='vertical'] [data-md-slider-control] {
margin-top: 0;
margin-inline-start: var(--md-sys-measurement-space600);
}
[data-md-slider-drawing] {
position: absolute;
inset-block: 0;
inset-inline: var(--md-sys-measurement-space25);
pointer-events: none;
&:dir(rtl) {
scale: -1 1;
}
}
/* The same drawing, laid out as long as the slider is tall and turned a quarter anticlockwise so
the value grows upwards. Everything inside it is placed as if the slider were lying down. */
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-drawing] {
inset: auto;
top: 50%;
left: 50%;
width: calc(100cqh - var(--md-sys-measurement-space50));
height: 100cqw;
translate: -50% -50%;
rotate: -90deg;
scale: none;
}
[data-md-slider-track] {
position: absolute;
inset-inline: 0;
top: 50%;
height: var(--slider-track);
translate: 0 -50%;
}
[data-md-slider-segment] {
position: absolute;
inset-block: 0;
background-color: var(--slider-inactive);
}
[data-md-slider-segment='active'] {
background-color: var(--slider-active);
}
:is([data-md-slider-tick], [data-md-slider-stop]) {
position: absolute;
top: 50%;
width: 4px;
height: 4px;
translate: -50% -50%;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--slider-on-inactive);
}
[data-md-slider-tick][data-md-active] {
background-color: var(--slider-on-active);
}
/* A disabled icon is on-surface at 38% wherever it sits. */
[data-md-slider]:has([data-md-slider-input]:disabled) [data-md-slider-icon] {
color: var(--slider-disabled-active);
}
[data-md-slider-icon] {
position: absolute;
top: 50%;
display: flex;
translate: 0 -50%;
color: var(--slider-on-inactive);
&[data-md-active] {
color: var(--slider-on-active);
}
&:dir(rtl) {
scale: -1 1;
}
}
/* Turned back the quarter the drawing turns, so the glyph stands up. */
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-icon] {
rotate: 90deg;
scale: none;
}
[data-md-slider-handle] {
position: absolute;
inset-block: 0;
width: 0;
}
[data-md-slider-thumb] {
position: absolute;
top: 50%;
left: 0;
width: 4px;
height: var(--slider-handle);
translate: -50% -50%;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--slider-active);
outline-offset: 2px;
transition-property: width, height;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
:is([data-md-pressed], [data-md-focused]) > [data-md-slider-thumb] {
width: 2px;
}
[data-md-focused] > [data-md-slider-thumb] {
height: calc(var(--slider-handle) - 6px);
outline: 3px solid var(--md-sys-color-secondary);
}
[data-md-slider-value-anchor] {
position: absolute;
bottom: calc(50% + var(--slider-handle) / 2 + var(--md-sys-measurement-space50));
left: 0;
z-index: 10;
display: flex;
justify-content: center;
translate: -50% 0;
&:dir(rtl) {
scale: -1 1;
}
}
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-value-anchor] {
scale: none;
}
[data-md-slider-value] {
display: flex;
align-items: center;
justify-content: center;
min-width: var(--md-sys-measurement-space600);
height: 44px;
padding-inline: var(--md-sys-measurement-space125);
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-inverse-surface);
color: var(--md-sys-color-inverse-on-surface);
font: var(--md-sys-typescale-label-lg);
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
font-variation-settings: normal;
white-space: nowrap;
transform-origin: bottom;
transition-property: opacity, scale;
transition-duration: var(--md-sys-motion-effects-fast-duration);
transition-timing-function: var(--md-sys-motion-effects-fast);
}
/* Turned back the quarter the drawing turns: the pill lands beside the handle and reads across. */
[data-md-slider][data-md-orientation='vertical'] [data-md-slider-value] {
transform-origin: center;
rotate: 90deg;
}
[data-md-slider][data-md-value-label='drag'] [data-md-slider-value] {
opacity: 0;
scale: 0.75;
}
[data-md-slider][data-md-value-label='drag'] :is([data-md-pressed], [data-md-focused]) [data-md-slider-value] {
opacity: 1;
scale: 1;
}
[data-md-slider-input] {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
appearance: none;
background: transparent;
opacity: 0;
pointer-events: none;
accent-color: var(--slider-active);
}
[data-md-slider-hint],
[data-md-slider-errors] > p {
margin-top: var(--md-sys-measurement-space50);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
font-variation-settings: normal;
}
[data-md-slider-errors] > p {
color: var(--md-sys-color-error);
}
/* Without script there is no drawing: the inputs show as native ranges, one under the other. */
@media (scripting: none) {
:is([data-md-slider], [data-md-slider][data-md-orientation]) [data-md-slider-control] {
height: auto;
cursor: auto;
}
[data-md-slider-control] > :not(:last-child) {
margin-block-end: var(--md-sys-measurement-space100);
}
[data-md-slider-drawing] {
display: none;
}
[data-md-slider-input] {
position: static;
display: block;
width: 100%;
height: 24px;
appearance: auto;
opacity: 1;
pointer-events: auto;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
/*
* <x-sort-header>: a column header that sorts the table (resources/views/components/sort-header.blade.php),
* inside <x-table> (components/table.css), which draws the cell itself.
*
* The button is the header's own text, in the header cell's type and colour, with a 16px arrow 4px
* after it; the corner is extra-small, for the focus ring the button wears (`md-focus-ring`), and
* `md-touch-target` gives the one-line button M3's 48px target (foundation/interaction.css). The
* sorted column (`data-md-active`) reads in on-surface with its arrow showing the direction; another
* column turns on-surface only while hovered, and shows a faint upward arrow while hovered or
* focused, at 60% of the ink — a hint of what pressing would do, not a state M3 names, fading on the
* fast effects spring.
*
* [data-md-sort-header] the <th>; data-md-active, aria-sort
* [data-md-sort-header-button]
* [data-md-sort-header-arrow]
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './icon.css';
@layer material.components {
[data-md-sort-header-button] {
display: inline-flex;
align-items: center;
gap: var(--md-sys-measurement-space50);
border-radius: var(--md-sys-shape-corner-xs);
cursor: pointer;
}
[data-md-sort-header][data-md-active] [data-md-sort-header-button] {
color: var(--md-sys-color-on-surface);
}
@media (hover: hover) {
[data-md-sort-header-button]:hover {
color: var(--md-sys-color-on-surface);
}
}
[data-md-sort-header-arrow] {
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
[data-md-sort-header]:not([data-md-active]) [data-md-sort-header-arrow] {
opacity: 0;
}
[data-md-sort-header]:not([data-md-active]) [data-md-sort-header-button]:focus-visible [data-md-sort-header-arrow] {
opacity: 0.6;
}
@media (hover: hover) {
[data-md-sort-header]:not([data-md-active]) [data-md-sort-header-button]:hover [data-md-sort-header-arrow] {
opacity: 0.6;
}
}
}
+35 -30
View File
@@ -2,26 +2,32 @@
* Data tables: `<x-table>` (resources/views/components/table.blade.php), whose callers write plain
* `<thead>`, `<tr>`, `<th>` and `<td>` inside it.
*
* So the styling is descendant selectors on the one attribute the component sets, all inside
* `:where()` and `@layer components`: a caller's `text-end` or `whitespace-nowrap` on a cell always
* wins. Header cells in title-small on-surface-variant over an outline-variant rule, body cells in
* body-medium between outline-variant rules. A row that opens something is `data-list-row` and
* So the styling is descendant selectors on the one attribute the component sets, `data-md-table`,
* with every cell and row selector inside `:where()`: a caller's alignment or wrapping on a cell
* always wins, whatever layer it comes from. Header cells in title-small on-surface-variant over an
* outline-variant rule, body cells in body-medium between outline-variant rules — the role itself,
* never a fraction of it, since M3 reserves opacity for state layers and disabled. A selected row
* (`aria-selected="true"`) is secondary-container. A row that opens something is `data-list-row` and
* answers a pointer as a list row does (components/list.css).
*
* A row is 52px: 16px above and below a body-medium line. Density is never applied by itself —
* "don't apply density by default; offer an explicit density opt-in instead, keeping opt-out targets
* at >= 48x48 CSS px" (docs/reference/m3/foundations-supplement.md § Accessibility) — so `dense`
* (`data-dense`) is the caller's decision, and it is the caller's to justify: a dense row is 36px,
* and a dense row inside an `xs` table 24px.
* A row is 52px: 16px above and below a body-medium line, 12px beside each cell. Density is never
* applied by itself — "don't apply density by default; offer an explicit density opt-in instead,
* keeping opt-out targets at >= 48x48 CSS px" (docs/reference/m3/foundations-supplement.md
* § Accessibility) — so `dense` (`data-md-dense`) is the caller's decision, and it is the caller's
* to justify: a dense row is 36px (8px above and below). `data-md-size="xs"`, for a table inside a
* panel inside a panel, is body-small with label-medium headers, 8px around each cell (32px rows),
* and 4px above and below when dense (24px).
*/
@layer components {
/* `relative` makes the table the containing block for anything absolute inside it — an `sr-only`
header label, a tooltip. Without it they escape the scroll box and widen a phone's layout
viewport. */
[data-table] {
--cell-x: 0.75rem;
--cell-y: 1rem;
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@layer material.components {
/* `position: relative` makes the table the containing block for anything absolute inside it —
a visually hidden header label, a tooltip. Without it they escape the scroll box and widen a
phone's layout viewport. */
[data-md-table] {
--cell-x: 12px;
--cell-y: var(--md-sys-measurement-space200);
position: relative;
width: 100%;
@@ -31,29 +37,29 @@
letter-spacing: var(--md-sys-typescale-body-md-tracking);
}
[data-table][data-size="xs"] {
--cell-x: 0.5rem;
--cell-y: 0.5rem;
[data-md-table][data-md-size='xs'] {
--cell-x: var(--md-sys-measurement-space100);
--cell-y: var(--md-sys-measurement-space100);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
}
[data-table][data-dense] {
--cell-y: 0.5rem;
[data-md-table][data-md-dense] {
--cell-y: var(--md-sys-measurement-space100);
}
[data-table][data-size="xs"][data-dense] {
--cell-y: 0.25rem;
[data-md-table][data-md-size='xs'][data-md-dense] {
--cell-y: var(--md-sys-measurement-space50);
}
[data-table] :where(th, td) {
[data-md-table] :where(th, td) {
padding: var(--cell-y) var(--cell-x);
text-align: start;
vertical-align: middle;
}
[data-table] :where(thead th) {
[data-md-table] :where(thead th) {
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-title-sm);
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
@@ -61,21 +67,20 @@
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
[data-table][data-size="xs"] :where(thead th) {
[data-md-table][data-md-size='xs'] :where(thead th) {
font: var(--md-sys-typescale-label-md);
letter-spacing: var(--md-sys-typescale-label-md-tracking);
}
/* The role itself, not a fraction of it: M3 reserves opacity for state layers and disabled. */
[data-table] :where(tbody tr) {
[data-md-table] :where(tbody tr) {
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
[data-table] :where(tbody tr:last-child) {
[data-md-table] :where(tbody tr:last-child) {
border-bottom: 0;
}
[data-table] :where(tbody tr[aria-selected="true"]) {
[data-md-table] :where(tbody tr[aria-selected='true']) {
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
+63
View File
@@ -0,0 +1,63 @@
/*
* <x-textarea>: the text field grown to several lines (resources/views/components/textarea.blade.php).
*
* The chrome is the field's (components/field.css); this file is what a <textarea> changes in it.
* The browser's resize grip belongs in the field's corner, so the textarea reaches to just inside
* the outline's end and bottom and its padding puts the text back. Its label rests on the first
* line rather than in the middle of the box. With `data-md-autogrow` it grows with its text from
* `--field-rows` lines to `--field-max-rows` (both set inline by the view) through
* `field-sizing: content`, and resources/js/field.js sets the height where the browser cannot.
* A line is body-large's 1.5rem, so the row arithmetic stays in rem with the text.
*
* The field's root carries `data-md-textarea`.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './field.css';
@layer material.components {
/* The browser draws the resize grip in the textarea's own corner, so the
textarea reaches to just inside the outline's end and bottom — the grip sits
in the field's corner, not on the bottom outline a padding in from it — and
its padding puts the text back where it was. */
textarea[data-md-field-control] {
--field-grip: var(--md-sys-measurement-space50);
/* The first half-line of the top offset is a margin, not padding: text scrolled up in a full
textarea disappears under the edge instead of running through the label. */
--textarea-clear: var(--md-sys-measurement-space100);
--textarea-pad-top: calc((var(--field-height) - 1.5rem) / 2 - var(--textarea-clear));
--textarea-pad-bottom: calc((var(--field-height) - 1.5rem) / 2 - var(--field-grip));
margin-block: var(--textarea-clear) var(--field-grip);
margin-inline-end: calc(var(--field-grip) - var(--field-pad));
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
padding-inline-end: calc(var(--field-pad) - var(--field-grip));
resize: vertical;
}
/* `<x-textarea>` grows with what is typed, from `rows` lines up to `max-rows`. Where the browser
cannot size a field to its content, resources/js/field.js sets the height instead. */
textarea[data-md-field-control][data-md-autogrow] {
field-sizing: content;
min-block-size: calc(var(--field-rows, 3) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
max-block-size: calc(var(--field-max-rows, 1000) * 1.5rem + var(--textarea-pad-top) + var(--textarea-pad-bottom));
resize: none;
}
/* A textarea's label rests on its first line, not in the middle of the box. */
[data-md-field]:not([data-md-floated]):has([data-md-field-label]):has(textarea[data-md-field-control]:placeholder-shown):not(:focus-within) [data-md-field-label] {
top: calc(var(--field-height) / 2);
}
/* Filled, the label floats inside the box: the text starts under it, 24px down, and the
bottom keeps the field's 8px. */
[data-md-field][data-md-variant='filled']:has([data-md-field-label]) textarea[data-md-field-control] {
--textarea-clear: var(--md-sys-measurement-space300);
--textarea-pad-top: 0px;
--textarea-pad-bottom: calc(var(--md-sys-measurement-space100) - var(--field-grip));
padding-block: var(--textarea-pad-top) var(--textarea-pad-bottom);
}
}
+1 -1
View File
@@ -53,7 +53,7 @@
}
@layer components {
[data-timepicker-field] .field-control {
[data-timepicker-field] [data-md-field-control] {
cursor: pointer;
caret-color: transparent;
}
+151
View File
@@ -0,0 +1,151 @@
/*
* <x-toggle>: M3's switch (resources/views/components/toggle.blade.php), on the row the selection
* controls share (components/selection.css). The hooks say `switch` because `toggle` is daisyUI's
* class, which the design guard rejects.
*
* SwitchTokens (androidx Compose Material 3, Apache-2.0): a 52×32 track with a 2px outline, in
* surface-container-highest when off and primary when on. The handle's centre is 16px from the
* track's start off and 36px on; it is 16px across off, 24px with an icon or on, and 28px while
* pressed, moving and growing on the fast spatial spring. Off it is outline, on-surface-variant
* while hovered, focused or pressed; on it is on-primary, primary-container in those states. The
* state layer is a 40px circle around the handle. Its icons are 16px, on-primary-container when on.
* Disabled, the track is on-surface at 12% and the handle on-surface at 38% (surface when on).
*
* [data-md-toggle] the root; `class` and `style` land here
* [data-md-selection-row] data-md-right puts the switch at the end of the row
* [data-md-switch] the track; `data-md-icons` = "both" or "selected"
* input, [data-md-switch-handle] (with [data-md-switch-on] and [data-md-switch-off] icons)
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './selection.css';
@import './icon.css';
@layer material.components {
[data-md-toggle] {
min-width: 0;
}
/* The positions below are the handle's centres less half its size, measured inside the track's
2px outline. */
[data-md-switch] {
position: relative;
display: grid;
flex: none;
width: 52px;
height: 32px;
}
[data-md-switch] input {
width: 100%;
height: 100%;
border: 2px solid var(--md-sys-color-outline);
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-highest);
}
[data-md-switch] input:focus-visible {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
[data-md-switch-handle] {
--handle: 16px;
position: absolute;
top: 50%;
inset-inline-start: calc(16px - var(--handle) / 2);
display: grid;
place-items: center;
width: var(--handle);
height: var(--handle);
translate: 0 -50%;
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-outline);
color: var(--md-sys-color-surface-container-highest);
pointer-events: none;
transition-property: inset-inline-start, width, height, background-color;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
transition-timing-function: var(--md-sys-motion-spatial-fast);
}
[data-md-switch-handle]::before {
inset: 50% auto auto 50%;
width: 40px;
height: 40px;
translate: -50% -50%;
}
[data-md-switch-handle] > svg {
grid-area: 1 / 1;
opacity: 0;
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
[data-md-switch][data-md-icons='both'] [data-md-switch-handle] {
--handle: 24px;
}
[data-md-switch][data-md-icons='both'] input:not(:checked) ~ [data-md-switch-handle] [data-md-switch-off],
[data-md-switch][data-md-icons] input:checked ~ [data-md-switch-handle] [data-md-switch-on] {
opacity: 1;
}
[data-md-switch] input:checked {
border-color: var(--md-sys-color-primary);
background-color: var(--md-sys-color-primary);
}
[data-md-switch] input:checked ~ [data-md-switch-handle] {
--handle: 24px;
inset-inline-start: calc(36px - var(--handle) / 2);
background-color: var(--md-sys-color-on-primary);
color: var(--md-sys-color-on-primary-container);
}
[data-md-selection-row]:active [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
--handle: 28px;
}
@media (hover: hover) {
[data-md-selection-row]:hover [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
background-color: var(--md-sys-color-on-surface-variant);
}
[data-md-selection-row]:hover [data-md-switch] input:checked:not(:disabled) ~ [data-md-switch-handle] {
background-color: var(--md-sys-color-primary-container);
}
}
[data-md-switch] input:focus-visible ~ [data-md-switch-handle],
[data-md-selection-row]:active [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
background-color: var(--md-sys-color-on-surface-variant);
}
[data-md-switch] input:checked:focus-visible ~ [data-md-switch-handle],
[data-md-selection-row]:active [data-md-switch] input:checked:not(:disabled) ~ [data-md-switch-handle] {
background-color: var(--md-sys-color-primary-container);
}
[data-md-switch] input:disabled {
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
background-color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
}
[data-md-switch] input:disabled ~ [data-md-switch-handle] {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
[data-md-switch] input:disabled:checked {
border-color: transparent;
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
}
[data-md-switch] input:disabled:checked ~ [data-md-switch-handle] {
background-color: var(--md-sys-color-surface);
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
}
}
-4
View File
@@ -22,12 +22,8 @@
@import './components/menu.css';
/* Inputs, selection and data — leaving in step 36 */
@import './components/field.css';
@import './components/selection.css';
@import './components/search.css';
@import './components/datepicker.css';
@import './components/timepicker.css';
@import './components/table.css';
/* Containment — leaving in step 36 */
@import './components/list.css';