Draw the checkbox and the selection controls' row without Tailwind
<x-checkbox> renders data-md-checkbox, its box, tick and dash, and its row, text and errors as data-md-* attributes; the 18px tick takes a size prop and a label-less box md-touch-target (plan step 36). selection.css moves into material.components as the row, text and state layer the three controls share, and each control's drawing goes to its own file: checkbox.css, radio.css, toggle.css, on the state tokens. The radio and the switch take the renamed shared hooks now and lose their layout classes in their own commits. field.js follows data-md-indeterminate. Renaming the switch's data-handle also stops selection.css matching the slider's handles, which it drew 24px above the track. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
3083fe28b2
commit
f86afc0542
@@ -21,6 +21,9 @@
|
||||
@import './components/textarea.css';
|
||||
@import './components/select.css';
|
||||
@import './components/file.css';
|
||||
@import './components/checkbox.css';
|
||||
@import './components/radio.css';
|
||||
@import './components/toggle.css';
|
||||
|
||||
/* Containment */
|
||||
|
||||
|
||||
@@ -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,79 @@
|
||||
/*
|
||||
* <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%.
|
||||
*
|
||||
* [data-md-radio-button] the ring: input, [data-md-radio-dot]
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './selection.css';
|
||||
|
||||
@layer material.components {
|
||||
[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-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);
|
||||
}
|
||||
}
|
||||
@@ -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,145 @@
|
||||
/*
|
||||
* <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-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 {
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
@import './components/menu.css';
|
||||
|
||||
/* Inputs, selection and data — leaving in step 36 */
|
||||
@import './components/selection.css';
|
||||
@import './components/search.css';
|
||||
@import './components/datepicker.css';
|
||||
@import './components/timepicker.css';
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
* in.
|
||||
*
|
||||
* `<x-checkbox indeterminate>`: HTML has no attribute for a checkbox's mixed state, only the
|
||||
* `indeterminate` property, so the component marks the input `data-indeterminate` and this keeps
|
||||
* `indeterminate` property, so the component marks the input `data-md-indeterminate` and this keeps
|
||||
* the property in step with the mark — when the page loads, and when a render adds or removes the
|
||||
* mark. A person pressing the box clears the property as the browser always does; the mark stays
|
||||
* until the server decides again.
|
||||
*/
|
||||
const GROWING = 'textarea[data-md-autogrow]'
|
||||
const MIXED = 'input[type="checkbox"][data-indeterminate]'
|
||||
const MIXED = 'input[type="checkbox"][data-md-indeterminate]'
|
||||
|
||||
const growsByItself = window.CSS?.supports?.('field-sizing', 'content') ?? false
|
||||
|
||||
@@ -49,8 +49,8 @@ if (! growsByItself) {
|
||||
new MutationObserver((records) => {
|
||||
for (const record of records) {
|
||||
if (record.type === 'attributes') {
|
||||
if (record.attributeName === 'data-indeterminate' && record.target instanceof HTMLInputElement) {
|
||||
record.target.indeterminate = record.target.hasAttribute('data-indeterminate')
|
||||
if (record.attributeName === 'data-md-indeterminate' && record.target instanceof HTMLInputElement) {
|
||||
record.target.indeterminate = record.target.hasAttribute('data-md-indeterminate')
|
||||
} else if (! growsByItself && record.target.matches(GROWING)) {
|
||||
grow(record.target)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ new MutationObserver((records) => {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['data-indeterminate', 'data-md-autogrow'],
|
||||
attributeFilter: ['data-md-indeterminate', 'data-md-autogrow'],
|
||||
})
|
||||
|
||||
markAll(document)
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
The label and its hint sit beside it and are its accessible name — the whole row is the
|
||||
`<label>`, so either can be pressed. `right` puts the box at the end of the row, for a setting
|
||||
read left to right. `indeterminate` shows the mixed state of a "select all" whose items are
|
||||
partly ticked: HTML has no attribute for it, so the input is marked `data-indeterminate` and
|
||||
partly ticked: HTML has no attribute for it, so the input is marked `data-md-indeterminate` and
|
||||
resources/js/field.js keeps the property in step, on load and after every morph. Errors are
|
||||
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`
|
||||
(resources/css/components/selection.css).
|
||||
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`.
|
||||
The drawing is resources/css/components/checkbox.css, on the row the three selection controls
|
||||
share (resources/css/components/selection.css).
|
||||
|
||||
Without a label — a "select all" in a table header, a row's tick — the row is only the 18px box,
|
||||
so the box carries `touch-target` and catches presses over M3's 48px minimum.
|
||||
so the box carries `md-touch-target` and catches presses over M3's 48px minimum.
|
||||
|
||||
Grouping is the caller's, not this component's: M3 asks that from `expanded` (840px) a set of
|
||||
related checkboxes be gathered into a contained region rather than left as one long column
|
||||
(docs/reference/m3/components-navigation-selection-inputs.md § Checkbox, Behaviour). Lay the
|
||||
group out yourself — `<div class="grid gap-4 expanded:grid-cols-2">` around the checkboxes, or a
|
||||
`<x-card>` or side sheet holding them — and give the group a heading that names what it asks. --}}
|
||||
group out yourself — a grid of one column that takes two from `expanded`, or an `<x-card>` or
|
||||
side sheet holding them — and give the group a heading that names what it asks. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -33,42 +34,34 @@
|
||||
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
||||
<label for="{{ $id }}" data-selection @if ($messages !== []) data-invalid @endif @class([
|
||||
'flex gap-4',
|
||||
'items-start' => filled($hint),
|
||||
'items-center' => blank($hint),
|
||||
'flex-row-reverse justify-between' => $right,
|
||||
])>
|
||||
<span data-checkbox @class([
|
||||
'mt-[3px]' => filled($hint),
|
||||
'touch-target' => blank($label) && blank($hint),
|
||||
])>
|
||||
<div data-md-checkbox {{ $attributes->only(['class', 'style', 'wire:key']) }}>
|
||||
<label for="{{ $id }}" data-md-selection-row @if ($right) data-md-right @endif @if ($messages !== []) data-md-invalid @endif>
|
||||
<span data-md-checkbox-box @if (blank($label) && blank($hint)) class="md-touch-target" @endif>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
||||
{{ $attributes->except(['class', 'style', 'id', 'wire:key']) }}
|
||||
id="{{ $id }}"
|
||||
type="checkbox"
|
||||
@if ($indeterminate) data-indeterminate @endif
|
||||
@if ($indeterminate) data-md-indeterminate @endif
|
||||
@if ($messages !== []) aria-invalid="true" aria-describedby="{{ $id }}-support" @endif
|
||||
/>
|
||||
<x-livewire-material::icon name="check" class="size-4.5" optical="20" data-check />
|
||||
<x-livewire-material::icon name="remove" class="size-4.5" optical="20" data-mixed />
|
||||
<x-livewire-material::icon name="check" size="18" data-md-checkbox-check />
|
||||
<x-livewire-material::icon name="remove" size="18" data-md-checkbox-mixed />
|
||||
</span>
|
||||
|
||||
@if (filled($label) || filled($hint))
|
||||
<span class="min-w-0">
|
||||
<span data-md-selection-text>
|
||||
@if (filled($label))
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
||||
<span data-md-selection-label>{{ $label }}</span>
|
||||
@endif
|
||||
@if (filled($hint))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
||||
<span data-md-selection-hint>{{ $hint }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</label>
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $id }}-support" class="mt-1 type-body-sm text-error" role="alert">
|
||||
<div id="{{ $id }}-support" data-md-selection-support role="alert">
|
||||
@foreach ($messages as $message)
|
||||
<p>{{ $message }}</p>
|
||||
@endforeach
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(600px, M3's compact/medium boundary — compact always stacks), and
|
||||
`value` for the option chosen when the page loads (with `wire:model`, Livewire sets it).
|
||||
Errors are read from the bag under the `wire:model` name
|
||||
(resources/css/components/selection.css).
|
||||
(resources/css/components/radio.css).
|
||||
|
||||
M3 stacks radio buttons vertically and cautions against a row at any width, so `inline` is for
|
||||
a group of two or three short labels that would waste a column, not the shape to reach for.
|
||||
@@ -46,12 +46,12 @@
|
||||
@foreach ($options as $option)
|
||||
@php($optionHintText = data_get($option, $optionHint))
|
||||
|
||||
<label data-selection @if ($messages !== []) data-invalid @endif @class([
|
||||
<label data-md-selection-row @if ($messages !== []) data-md-invalid @endif @class([
|
||||
'flex gap-4',
|
||||
'items-start' => filled($optionHintText),
|
||||
'items-center' => blank($optionHintText),
|
||||
])>
|
||||
<span data-radio @class([
|
||||
<span data-md-radio-button @class([
|
||||
'mt-0.5' => filled($optionHintText),
|
||||
'touch-target' => blank(data_get($option, $optionLabel)) && blank($optionHintText),
|
||||
])>
|
||||
@@ -64,11 +64,11 @@
|
||||
@checked($value !== null && (string) $value === (string) data_get($option, $optionValue))
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
/>
|
||||
<span data-dot></span>
|
||||
<span data-md-radio-dot></span>
|
||||
</span>
|
||||
|
||||
<span class="min-w-0">
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ data_get($option, $optionLabel) }}</span>
|
||||
<span class="block type-body-lg text-on-surface" data-md-selection-label>{{ data_get($option, $optionLabel) }}</span>
|
||||
@if (filled($optionHintText))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $optionHintText }}</span>
|
||||
@endif
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
its name — a switch without `label` needs an `aria-label`. `right` puts it at the end of the
|
||||
row, where a setting's switch usually is. `icons` draws a check on the handle when on and a
|
||||
cross when off; `icons="selected"` only the check. Every other attribute reaches the `<input>`
|
||||
(resources/css/components/selection.css).
|
||||
(resources/css/components/toggle.css).
|
||||
|
||||
Without a label the row is only the 52×32 track, so the track carries `touch-target` and catches
|
||||
presses over M3's 48px minimum. --}}
|
||||
@@ -24,23 +24,23 @@
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
||||
<label for="{{ $id }}" data-selection @class([
|
||||
<label for="{{ $id }}" data-md-selection-row @class([
|
||||
'flex gap-4',
|
||||
'items-start' => filled($hint),
|
||||
'items-center' => blank($hint),
|
||||
'flex-row-reverse justify-between' => $right,
|
||||
])>
|
||||
<span data-switch @if ($icons) data-icons="{{ $icons }}" @endif @class(['touch-target' => blank($label) && blank($hint)])>
|
||||
<span data-md-switch @if ($icons) data-md-icons="{{ $icons }}" @endif @class(['touch-target' => blank($label) && blank($hint)])>
|
||||
<input
|
||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
||||
id="{{ $id }}"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
<span data-handle>
|
||||
<span data-md-switch-handle>
|
||||
@if ($icons)
|
||||
<x-livewire-material::icon name="check" class="size-4" optical="20" data-on />
|
||||
<x-livewire-material::icon name="close" class="size-4" optical="20" data-off />
|
||||
<x-livewire-material::icon name="check" class="size-4" optical="20" data-md-switch-on />
|
||||
<x-livewire-material::icon name="close" class="size-4" optical="20" data-md-switch-off />
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
@@ -48,7 +48,7 @@
|
||||
@if (filled($label) || filled($hint))
|
||||
<span class="min-w-0">
|
||||
@if (filled($label))
|
||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
||||
<span class="block type-body-lg text-on-surface" data-md-selection-label>{{ $label }}</span>
|
||||
@endif
|
||||
@if (filled($hint))
|
||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
||||
|
||||
Reference in New Issue
Block a user