tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m4s
tests / lint (push) Successful in 59s
tests / browser (chrome, chromium) (push) Successful in 2m54s
tests / browser (firefox, firefox) (push) Successful in 3m3s
tests / browser (safari, webkit) (push) Successful in 4m13s
Outlined and filled text fields (input, password, auto-growing textarea, native select with the customizable select menu, file), checkbox with an indeterminate state, radio buttons, the M3 switch and form, ported from ReStride and extended. The first half of Phase 6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
345 lines
12 KiB
CSS
345 lines
12 KiB
CSS
/*
|
||
* M3's selection controls: the checkbox, the radio button and the switch (CheckboxTokens,
|
||
* RadioButtonTokens, SwitchTokens, androidx Compose Material 3, Apache-2.0).
|
||
*
|
||
* 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.
|
||
*
|
||
* [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)
|
||
*
|
||
* 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.
|
||
*/
|
||
|
||
@layer components {
|
||
[data-selection] {
|
||
cursor: pointer;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
[data-selection]: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-checkbox],
|
||
[data-radio] {
|
||
position: relative;
|
||
display: grid;
|
||
flex: none;
|
||
place-items: center;
|
||
}
|
||
|
||
[data-checkbox] > *,
|
||
[data-radio] > * {
|
||
grid-area: 1 / 1;
|
||
}
|
||
|
||
[data-checkbox]::before,
|
||
[data-radio]::before,
|
||
[data-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 {
|
||
margin: 0;
|
||
appearance: none;
|
||
cursor: inherit;
|
||
outline: none;
|
||
transition-property: background-color, border-color;
|
||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||
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) {
|
||
--selection-ink: var(--md-sys-color-primary);
|
||
}
|
||
|
||
[data-selection][data-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-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-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);
|
||
}
|
||
}
|