diff --git a/resources/css/components.css b/resources/css/components.css index 5aba67ca..c22b2871 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -27,6 +27,7 @@ @import './components/chip.css'; @import './components/chip-set.css'; @import './components/choices.css'; +@import './components/slider.css'; /* Containment */ diff --git a/resources/css/components/slider.css b/resources/css/components/slider.css new file mode 100644 index 00000000..01583e9f --- /dev/null +++ b/resources/css/components/slider.css @@ -0,0 +1,403 @@ +/* + * : 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; + } + } +} diff --git a/resources/js/slider.js b/resources/js/slider.js index f8945571..5e19acf9 100644 --- a/resources/js/slider.js +++ b/resources/js/slider.js @@ -200,8 +200,10 @@ function sliderGeometry({ width, size, fractions, centered = false, range = fals * effect, and anything reactive read or written on the way to the drawing would re-run it. */ function slider(root) { + // The component's root carries the props; `root` is the control inside it the pointer presses. + const host = root.closest('[data-md-slider]') ?? root const inputs = [...root.querySelectorAll(':scope > input[type="range"]')] - const drawing = root.querySelector('[data-slider-drawing]') + const drawing = root.querySelector('[data-md-slider-drawing]') const range = inputs.length > 1 const cleanups = [] let queued = false @@ -228,24 +230,24 @@ function slider(root) { } /** M3 Expressive's second orientation: the drawing is turned a quarter, the geometry is not. */ - const vertical = () => root.dataset.orientation === 'vertical' + const vertical = () => host.dataset.mdOrientation === 'vertical' /** The track's length: the slider's height while it stands up, its width while it lies down. */ const trackLength = () => Math.max((vertical() ? root.clientHeight : root.clientWidth) - HANDLE_WIDTH, 0) const values = () => inputs.map((input) => Number.parseFloat(input.value)) - const thumbOf = (input) => drawing?.querySelector(`[data-handle="${inputs.indexOf(input) === 1 ? 'end' : 'start'}"]`) + const thumbOf = (input) => drawing?.querySelector(`[data-md-slider-handle="${inputs.indexOf(input) === 1 ? 'end' : 'start'}"]`) - const tickFractions = () => [...(drawing?.querySelectorAll('[data-tick]') ?? [])].map((tick) => Number.parseFloat(tick.dataset.tick)) + const tickFractions = () => [...(drawing?.querySelectorAll('[data-md-slider-tick]') ?? [])].map((tick) => Number.parseFloat(tick.dataset.mdSliderTick)) const geometry = (width) => { const { min, max } = bounds() return sliderGeometry({ width, - size: root.dataset.size, + size: host.dataset.mdSize, fractions: values().map((value) => (Number.isFinite(value) ? clamp((value - min) / (max - min), 0, 1) : 0)), - centered: root.hasAttribute('data-centered'), + centered: host.hasAttribute('data-md-centered'), range, tickFractions: tickFractions(), }) @@ -257,13 +259,13 @@ function slider(root) { } const width = trackLength() - const size = SIZES[root.dataset.size] ?? SIZES.xs + const size = SIZES[host.dataset.mdSize] ?? SIZES.xs const { segments, stops, ticks, handles, activeTrack, endTrackStart } = geometry(width) const part = (selector) => drawing.querySelector(selector) const px = (number) => `${Math.round(number * 100) / 100}px` for (const name of ['start', 'active', 'end']) { - const element = part(`[data-segment="${name}"]`) + const element = part(`[data-md-slider-segment="${name}"]`) const segment = segments[name] if (element) { @@ -277,7 +279,7 @@ function slider(root) { } for (const name of ['start', 'end']) { - const element = part(`[data-stop="${name}"]`) + const element = part(`[data-md-slider-stop="${name}"]`) if (element) { element.hidden = stops[name] === undefined @@ -288,20 +290,20 @@ function slider(root) { const fractions = tickFractions() const spacing = fractions.length > 1 ? (width - size.corner * 2) * Math.abs(fractions[1] - fractions[0]) : Infinity - drawing.querySelectorAll('[data-tick]').forEach((element, i) => { + drawing.querySelectorAll('[data-md-slider-tick]').forEach((element, i) => { const tick = ticks[i] element.hidden = !tick || spacing < TICK_MIN_SPACING if (tick) { element.style.left = px(tick.x) - element.toggleAttribute('data-active', tick.active) + element.toggleAttribute('data-md-active', tick.active) } }) handles.forEach((x, i) => { - const thumb = part(`[data-handle="${i === 1 ? 'end' : 'start'}"]`) - const label = thumb?.querySelector('[data-value-label]') + const thumb = part(`[data-md-slider-handle="${i === 1 ? 'end' : 'start'}"]`) + const label = thumb?.querySelector('[data-md-slider-value]') if (thumb) { thumb.style.left = px(x) @@ -313,7 +315,7 @@ function slider(root) { }) // The inset icon: at the start of the active track while it fits, else of the inactive one. - const icon = part('[data-track-icon]') + const icon = part('[data-md-slider-icon]') if (icon) { const room = size.icon + ICON_PADDING * 2 @@ -321,7 +323,7 @@ function slider(root) { const inInactive = !inActive && endTrackStart !== null && width - endTrackStart >= room icon.hidden = !inActive && !inInactive - icon.toggleAttribute('data-active', inActive) + icon.toggleAttribute('data-md-active', inActive) icon.style.left = px((inActive ? activeTrack[0] : (endTrackStart ?? 0)) + ICON_PADDING) } } @@ -371,10 +373,10 @@ function slider(root) { } /** Whether a handle shows keyboard focus: narrowed, ringed and labelled. */ - const showFocus = (input, shown) => thumbOf(input)?.toggleAttribute('data-focused', shown) + const showFocus = (input, shown) => thumbOf(input)?.toggleAttribute('data-md-focused', shown) /** Compose narrows a handle and shows its label while it is pressed or dragged. */ - const pressed = (index) => drawing?.querySelectorAll('[data-handle]').forEach((thumb, i) => thumb.toggleAttribute('data-pressed', i === index)) + const pressed = (index) => drawing?.querySelectorAll('[data-md-slider-handle]').forEach((thumb, i) => thumb.toggleAttribute('data-md-pressed', i === index)) inputs.forEach((input) => { // A value written by script fires no event: catch it on the input's own setters. @@ -493,7 +495,7 @@ function slider(root) { const mutations = new MutationObserver(schedule) inputs.forEach((input) => mutations.observe(input, { attributes: true, attributeFilter: ['min', 'max', 'step', 'value', 'disabled'] })) - mutations.observe(root, { attributes: true, attributeFilter: ['data-size', 'data-centered', 'data-orientation', 'dir'] }) + mutations.observe(host, { attributes: true, attributeFilter: ['data-md-size', 'data-md-centered', 'data-md-orientation', 'dir'] }) const resizes = new ResizeObserver(schedule) resizes.observe(root) diff --git a/resources/views/components/slider.blade.php b/resources/views/components/slider.blade.php index 6853a75a..671c8fcb 100644 --- a/resources/views/components/slider.blade.php +++ b/resources/views/components/slider.blade.php @@ -30,8 +30,8 @@ drawing, turned a quarter so the value grows upwards, with the value label beside the handle instead of above it and every size unchanged. The keyboard is the native range's, so Up and Right raise the value and Down and Left lower it. A vertical slider is as wide as a horizontal - one is tall and takes its length from the wrapper, so give it an explicit height — - `class="h-64"`, which the label and the hint share; without one it is 192px. M3 keeps range + one is tall and takes its length from the wrapper, so give it an explicit height — a height + rule on its `class`, which the label and the hint share; without one it is 192px. M3 keeps range sliders horizontal ("never use range sliders vertically — too much cognitive load"), so `range` wins over `orientation`. @@ -63,7 +63,11 @@ The server draws the first frame; the drawing is `wire:ignore`, so a Livewire render never fights a handle being dragged, and a value the server changes moves the handle after the - morph. --}} + morph. + + The root renders `data-md-slider` with `data-md-size`, `data-md-color`, `data-md-value-label`, + `data-md-orientation` and `data-md-centered`; the parts are `data-md-slider-*`, and everything + they look like is resources/css/components/slider.css. `class` and `style` land on the root. --}} @props([ 'label' => null, @@ -142,7 +146,6 @@ $id = $attributes->get('id') ?? 'material-slider-'.substr(md5(($model ?? $name ?? '').'|'.$label.'|'.($model ?? $name ?? \Illuminate\Support\Str::random(10))), 0, 10); $described = $messages !== [] || filled($hint) ? "{$id}-hint" : null; - $sized = preg_match('/(^|\s)w-/', (string) $attributes->get('class')) === 1; $inputAttributes = $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except(['class', 'style', 'wire:key', 'id']); // The first frame: Compose's layout and drawTrack, with every position a percentage of the @@ -234,182 +237,78 @@ $segmentStyle = fn (?array $segment): string => $segment === null ? '' : 'left: '.$calc($segment[0]).'; width: '.$calc([$segment[1][0] - $segment[0][0], $segment[1][1] - $segment[0][1]]) .'; border-radius: '.$segment[2].'px '.$segment[3].'px '.$segment[3].'px '.$segment[2].'px'; - // Every colour class written out whole, so Tailwind compiles it. - $activeInk = [ - 'primary' => 'bg-primary', 'secondary' => 'bg-secondary', 'tertiary' => 'bg-tertiary', 'error' => 'bg-error', - 'success' => 'bg-success', 'warning' => 'bg-warning', 'info' => 'bg-info', - ]; - $inactiveInk = [ - 'primary' => 'bg-secondary-container', 'secondary' => 'bg-secondary-container', 'tertiary' => 'bg-tertiary-container', - 'error' => 'bg-error-container', 'success' => 'bg-success-container', 'warning' => 'bg-warning-container', 'info' => 'bg-info-container', - ]; - // A tick, a stop indicator and the icon are drawn on the track, so each takes the "on" colour of - // the part it sits on: on-primary over the active track, on-secondary-container over the - // inactive one (M3's nine slider colour roles, in anatomy order). `data-active` marks the ones - // over the active track. - $tickInk = [ - 'primary' => 'bg-on-secondary-container data-active:bg-on-primary', 'secondary' => 'bg-on-secondary-container data-active:bg-on-secondary', - 'tertiary' => 'bg-on-tertiary-container data-active:bg-on-tertiary', 'error' => 'bg-on-error-container data-active:bg-on-error', - 'success' => 'bg-on-success-container data-active:bg-on-success', 'warning' => 'bg-on-warning-container data-active:bg-on-warning', - 'info' => 'bg-on-info-container data-active:bg-on-info', - ]; - // A stop indicator only ever sits at an open end of the track, which is inactive. - $stopInk = [ - 'primary' => 'bg-on-secondary-container', 'secondary' => 'bg-on-secondary-container', 'tertiary' => 'bg-on-tertiary-container', - 'error' => 'bg-on-error-container', 'success' => 'bg-on-success-container', 'warning' => 'bg-on-warning-container', - 'info' => 'bg-on-info-container', - ]; - $iconInk = [ - 'primary' => 'text-on-secondary-container data-active:text-on-primary', 'secondary' => 'text-on-secondary-container data-active:text-on-secondary', - 'tertiary' => 'text-on-tertiary-container data-active:text-on-tertiary', 'error' => 'text-on-error-container data-active:text-on-error', - 'success' => 'text-on-success-container data-active:text-on-success', 'warning' => 'text-on-warning-container data-active:text-on-warning', - 'info' => 'text-on-info-container data-active:text-on-info', - ]; - $accent = [ - 'primary' => 'noscript:accent-primary', 'secondary' => 'noscript:accent-secondary', 'tertiary' => 'noscript:accent-tertiary', - 'error' => 'noscript:accent-error', 'success' => 'noscript:accent-success', 'warning' => 'noscript:accent-warning', 'info' => 'noscript:accent-info', - ]; - - $trackHeight = ['xs' => 'h-4', 'sm' => 'h-6', 'md' => 'h-10', 'lg' => 'h-14', 'xl' => 'h-24'][$size]; - // A focused handle is 6px shorter, so its ring stays clear of the label (Material Components' - // m3_slider_focus_ring_thumb_height_decrease). - $handleHeight = [ - 'xs' => 'h-11 group-data-focused/thumb:h-9.5', 'sm' => 'h-11 group-data-focused/thumb:h-9.5', 'md' => 'h-13 group-data-focused/thumb:h-11.5', - 'lg' => 'h-17 group-data-focused/thumb:h-15.5', 'xl' => 'h-27 group-data-focused/thumb:h-25.5', - ][$size]; - // The label's bottom sits 4px above the handle: half of 44, 44, 52, 68 and 108px, plus 4. - $labelBottom = ['xs' => 'bottom-[calc(50%+1.625rem)]', 'sm' => 'bottom-[calc(50%+1.625rem)]', 'md' => 'bottom-[calc(50%+1.875rem)]', 'lg' => 'bottom-[calc(50%+2.375rem)]', 'xl' => 'bottom-[calc(50%+3.625rem)]'][$size]; - // The measure across the slider — the handle's height, and so the slider's own. Standing up it - // is the width instead, and the length comes from the wrapper the caller sized. - $across = [ - 'xs' => $vertical ? 'w-12' : 'h-12', - 'sm' => $vertical ? 'w-12' : 'h-12', - 'md' => $vertical ? 'w-13' : 'h-13', - 'lg' => $vertical ? 'w-17' : 'h-17', - 'xl' => $vertical ? 'w-27' : 'h-27', - ][$size]; $thumbs = $range ? ['start', 'end'] : ['start']; @endphp -
only(['class', 'style', 'wire:key'])->class(['min-w-0', 'w-full' => ! $sized && ! $vertical, 'flex w-fit flex-col' => $vertical]) }}> +
only(['class', 'style', 'wire:key']) }} +> @if (filled($label)) @if ($range) - {{ $label }} + {{ $label }} @else - + @endif @endif
! $vertical, - // Standing up: 192px long unless the wrapper says otherwise, and a size container, - // which is what lets the drawing below be as long as this is tall. - 'touch-pan-x h-48 min-h-0 flex-auto [container-type:size]' => $vertical, - // Room for a label that never hides: half the handle, 4px and the 44px pill — above the - // track when the slider lies down, beside it when it stands up. - 'mt-12' => $valueLabel === 'always' && ! $vertical, - 'ms-12' => $valueLabel === 'always' && $vertical, - 'noscript:h-auto noscript:cursor-auto noscript:space-y-2', - ]) > -