Draw the slider without Tailwind
<x-slider> renders data-md-slider with its size, colour, value label, orientation and centring as data-md-* attributes and its parts as data-md-slider-*; every size, colour role, disabled state, the vertical quarter turn, RTL mirroring and the no-script fallback move into components/slider.css as custom properties per size and colour (plan step 36). slider.js reads the props from the root and marks the renamed handle, tick and icon hooks. Browser tests pin the handle centred on the track at each size, Space held with an arrow, and the vertical slider's drag along Y, keys and value label beside the handle. 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
1bb5adab0e
commit
281c21b0a4
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user