Merge branch 'worktree-agent-a41c756369d016a29'
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
@layer components {
|
||||
[data-timepicker-field] .field-control {
|
||||
[data-timepicker-field] [data-md-field-control] {
|
||||
cursor: pointer;
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user