Files
livewire-material/resources/views/components/slider.blade.php
T
Andreas Reinhold / reiniandClaude Fable 5.1 790ef93c6a Stand a slider up with orientation="vertical"
M3 Expressive's configuration table lists a vertical orientation the library
had no prop for (plan step 24, audit docs/audits/m3-alignment/inputs.md
§ Missing). The drawing is the horizontal one, laid out as long as the slider
is tall inside a size container and turned a quarter anticlockwise, so the
geometry, the sizes and the tokens are untouched; the value label and the inset
icon are turned back so they read across. The keyboard stays the native range's
— Up and Right raise, Down and Left lower — and the inputs say
aria-orientation. A vertical slider takes its length from its wrapper, so the
header, the SKILL.md entry and the showcase all say to give it a height. M3
keeps range sliders horizontal, so `range` wins over `orientation`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:31:07 +02:00

455 lines
27 KiB
PHP

{{-- An M3 Expressive slider: a value, or with `range` a span of values, picked along a track.
<x-slider label="Volume" wire:model.live="volume" />
<x-slider label="Price" wire:model="price" range :min="0" :max="500" :step="10" />
<x-slider label="Balance" name="balance" :min="-50" :max="50" centered ticks :step="10" />
Built on native `<input type="range">`s, one per handle, which stay the control: the arrows,
Home and End, forms, `wire:model` and `x-model`, and screen readers all work as they do on a
plain range. M3's drawing lies under them (resources/js/slider.js); the inputs are invisible
and a press on the slider moves the nearest handle to the pointer. Without JavaScript the
inputs show as native ranges in the colour, and still post.
`min` (0), `max` (100), `step` (1, or `any`) and `value` as on a range input, or the bound
property's value. The binding gets `.number`, so values arrive as numbers: Livewire compares
what it sent with what came back, and a string that returns as an int would count as a
change and move a handle back while it is dragged. `range` binds an array `[from, to]`
`wire:model="price"` binds `price.0` and `price.1`, `x-model="price"` binds `price[0]` and
`price[1]` and posts `name[]` twice, from first; the handles never cross. `centered` fills from the middle of the track, for a
value that goes below zero. `ticks` marks every step (up to 200 of them, and only while they
stand 8px apart) and places the handle on the marks, as Compose does for a discrete slider.
`value-label` is `drag` (the default: while pressed, dragged or focused from the keyboard),
`always` or `never`. `size` is Expressive's `xs` (the default: a 16px track), `sm` 24px,
`md` 40px, `lg` 56px or `xl` 96px; `icon` puts a Material Symbol inside the track of an `md`,
`lg` or `xl` slider, at the start of the active track while it fits and of the inactive
track once it does not (not on a range or centred slider). `color`: `primary` (the default),
`secondary`, `tertiary`, `error`, `success`, `warning`, `info` — the active track and the
handle; the inactive track is its container (secondary-container for primary).
`orientation="vertical"` stands the slider up, M3 Expressive's second orientation: the same
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
sliders horizontal ("never use range sliders vertically — too much cognitive load"), so
`range` wins over `orientation`.
`label` is shown above and names the input (with `range`, the group; its handles are "Range
start" and "Range end"); `hint` goes below, and a validation message for the bound property
(or `name`) replaces it. Other attributes go to the input(s).
From androidx Compose Material 3 at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326
(Apache-2.0): Slider.kt — SliderImpl, RangeSliderImpl, SliderDefaults.Track, CenteredTrack
and drawTrack, ThumbContent — and SliderTokens.kt: a 4px handle, narrowed to 2px while
pressed or focused, in a 6px gap that splits the track, 2px inside corners, a 4px stop
indicator at each open end of the track, active track and handle in primary, inactive track
in secondary-container, disabled parts in on-surface at 38% and 12%. A stop indicator, a tick
and the inset icon take the "on" colour of the part of the track they sit on — on-primary on
the active track, on-secondary-container on the inactive one — which is the order M3's own
colour list gives (docs/reference/m3/components-navigation-selection-inputs.md § Sliders),
against SliderTokens' StopIndicatorColor. Compose leaves the value label to the app; this one
is M3's label container: 44px tall and at least 48px wide in inverse-surface with label-large,
4px above the handle (the library's spacing, where SliderTokens has
ValueIndicatorActiveBottomSpace = 12dp), and the handle is 6px shorter while its focus ring
shows (Material Components' m3_slider_focus_ring_thumb_height_decrease). The Expressive sizes
are M3's sliders/specs table: tracks of 16, 24, 40, 56 and 96px with 8, 8, 12, 16 and 28px
corners, handles of 44, 44, 52, 68 and 108px, icons of 24, 24 and 32px, 10px from their
segment's end.
The keyboard is the inputs' own — the arrows, Home and End — with PageUp and PageDown moving
by Compose's page (a tenth of the steps) and Space held with an arrow moving by that same large
interval, which is M3's "Space & Arrows" row (resources/js/slider.js).
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. --}}
@props([
'label' => null,
'hint' => null,
'name' => null,
'value' => null,
'min' => 0,
'max' => 100,
'step' => 1,
'range' => false,
'centered' => false,
'size' => 'xs',
'ticks' => false,
'valueLabel' => 'drag',
'icon' => null,
'color' => 'primary',
'orientation' => 'horizontal',
'disabled' => false,
])
@php
$format = fn (float $number): string => rtrim(rtrim(number_format(round($number, 6) + 0.0, 6, '.', ''), '0'), '.');
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'xs';
$color = in_array($color, ['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info'], true) ? $color : 'primary';
$valueLabel = in_array($valueLabel, ['always', 'drag', 'never'], true) ? $valueLabel : 'drag';
$centered = $centered && ! $range;
// M3 § Sliders, Behaviour: "range sliders should stay horizontal only".
$vertical = $orientation === 'vertical' && ! $range;
$minimum = is_numeric($min) ? (float) $min : 0.0;
$maximum = is_numeric($max) && (float) $max > $minimum ? (float) $max : $minimum + 100;
$continuous = $step === 'any';
$interval = ! $continuous && is_numeric($step) && (float) $step > 0 ? (float) $step : 1.0;
// The highest value on the step grid, which is where a native range stops.
$top = $continuous ? $maximum : $minimum + floor(($maximum - $minimum) / $interval + 1e-9) * $interval;
$snap = fn (mixed $number, float $fallback): float => ! is_numeric($number)
? $fallback
: ($continuous
? min(max((float) $number, $minimum), $maximum)
: min(max($minimum + round(((float) $number - $minimum) / $interval) * $interval, $minimum), $top));
// The bound property's value, when the slider renders inside a Livewire component.
$model = $attributes->wire('model')->value() ?: null;
$livewire = $model !== null ? \Livewire\Livewire::current() : null;
$source = ($livewire !== null ? data_get($livewire, $model) : null) ?? $value;
if ($range) {
$pair = is_array($source) ? array_values($source) : (is_string($source) ? explode(',', $source) : []);
$values = [$snap($pair[0] ?? null, $minimum), $snap($pair[1] ?? null, $top)];
sort($values);
} else {
$values = [$snap($source, $centered ? $snap(($minimum + $maximum) / 2, $minimum) : $minimum)];
}
$fractions = array_map(fn (float $number): float => ($number - $minimum) / ($maximum - $minimum), $values);
// Bound as numbers: a string sent for an int property comes back "changed", and Livewire
// would write the server's copy over a handle still being dragged.
$bindingsFor = fn (int $index): array => collect($attributes->whereStartsWith(['wire:model', 'x-model'])->getAttributes())
->mapWithKeys(fn (string $expression, string $attribute): array => [
(str_contains($attribute, '.number') ? $attribute : preg_replace('/^(wire:model|x-model)/', '$1.number', $attribute)) => match (true) {
! $range => $expression,
str_starts_with($attribute, 'wire:model') => "{$expression}.{$index}",
default => "{$expression}[{$index}]",
},
])
->all();
$name ??= $model;
$key = $model ?? ($name !== null ? str_replace(['[', ']'], ['.', ''], $name) : null);
$messages = $key !== null && isset($errors)
? \Illuminate\Support\Arr::flatten([$errors->get($key), $range ? $errors->get($key.'.*') : []])
: [];
$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
// track plus pixels. What only a measured width can decide — whether a segment has room, the
// icon's place, a tick in a gap — is decided for a 320px track; the script redraws exactly.
$nominal = 320.0;
$corner = ['xs' => 8, 'sm' => 8, 'md' => 12, 'lg' => 16, 'xl' => 28][$size];
$shrink = $size !== 'xs';
$iconSize = ['md' => 24, 'lg' => 24, 'xl' => 32][$size] ?? null;
$icon = filled($icon) && $iconSize !== null && ! $range && ! $centered ? $icon : null;
$tickFractions = [];
if ($ticks && ! $continuous && ($top - $minimum) / $interval <= 200) {
for ($index = 0; $minimum + $index * $interval <= $top + 1e-9; $index++) {
$tickFractions[] = $index * $interval / ($maximum - $minimum);
}
}
$discrete = $tickFractions !== [];
$length = fn (array $at): float => $at[0] / 100 * $nominal + $at[1];
$plus = fn (array $at, float $pixels): array => [$at[0], $at[1] + $pixels];
$onEdge = fn (float $fraction): bool => $discrete && (abs($fraction - $tickFractions[0]) < 1e-6 || abs($fraction - end($tickFractions)) < 1e-6);
$place = fn (float $fraction): array => $discrete && ! $onEdge($fraction) ? [$fraction * 100, $corner * (1 - 2 * $fraction)] : [$fraction * 100, 0.0];
$valueStart = $place($range ? $fractions[0] : 0.0);
$valueEnd = $place($range ? $fractions[1] : $fractions[0]);
$startThumb = $range || ($centered && $fractions[0] <= 0.5) ? 4 : 0;
$endThumb = ! $centered || $fractions[0] >= 0.5 ? 4 : 0;
$startGap = $range || $centered ? $startThumb / 2 + 6 : 0;
$endGap = $endThumb / 2 + 6;
$threshold = ! $shrink || $discrete ? $corner : 0;
$centre = [50.0, 0.0];
$segments = [];
$stops = [];
$adjustedEnd = $centered ? ($length($valueEnd) <= $length($centre) ? $valueEnd : $centre) : $valueStart;
if (($centered || $range) && $length($adjustedEnd) > $startGap + $threshold) {
$segments['start'] = [[0.0, 0.0], $plus($adjustedEnd, -$startGap), $corner, 2];
$stops['start'] = [0.0, (float) $corner];
}
$adjustedStart = $centered ? ($length($valueEnd) >= $length($centre) ? $valueEnd : $centre) : $valueEnd;
if ($length($adjustedStart) < $nominal - $endGap - $threshold) {
$segments['end'] = [$plus($adjustedStart, $endGap), [100.0, 0.0], 2, $corner];
$stops['end'] = [100.0, (float) -$corner];
}
$activeFrom = match (true) {
$centered => $plus($adjustedEnd, $length($adjustedEnd) < $length($centre) ? $startGap : 0),
$range => $plus($valueStart, $startGap),
default => [0.0, 0.0],
};
$activeTo = $centered ? $plus($adjustedStart, $length($adjustedStart) > $length($centre) ? -$endGap : 0) : $plus($valueEnd, -$endGap);
$activeStartRadius = $centered || $range ? 2 : $corner;
if ($length($activeTo) - $length($activeFrom) > (! $shrink || $discrete ? $activeStartRadius : 0)) {
$segments['active'] = [$activeFrom, $activeTo, $activeStartRadius, 2];
}
$spacing = count($tickFractions) > 1 ? ($nominal - $corner * 2) * $tickFractions[1] : INF;
$centreGap = $length($valueEnd) > $nominal / 2 ? $startGap : $endGap;
$valueGap = $centered && $length($valueEnd) <= $nominal / 2 ? $startGap : $endGap;
$tickMarks = array_map(function (float $tick, int $index) use ($tickFractions, $corner, $length, $centered, $range, $nominal, $centreGap, $valueStart, $valueEnd, $startGap, $valueGap, $spacing, $segments, $activeFrom, $activeTo): array {
$at = [$tick * 100, $corner * (1 - 2 * $tick)];
$x = $length($at);
return [
'fraction' => $tick,
'at' => $at,
'hidden' => (($centered || $range) && $index === 0) || $index === count($tickFractions) - 1
|| ($centered && abs($x - $nominal / 2) <= $centreGap)
|| ($range && abs($x - $length($valueStart)) <= $startGap)
|| abs($x - $length($valueEnd)) <= $valueGap
|| $spacing < 8,
'active' => $x >= $length($activeFrom) && $x <= $length($activeTo),
];
}, $tickFractions, array_keys($tickFractions));
$iconInActive = $icon !== null && isset($segments['active']) && $length($activeTo) - $length($activeFrom) >= $iconSize + 20;
$iconInInactive = $icon !== null && ! $iconInActive && isset($segments['end']) && $nominal - $length($segments['end'][0]) >= $iconSize + 20;
$iconAt = $iconInInactive ? $plus($segments['end'][0], 10) : [0.0, 10.0];
$calc = fn (array $at): string => 'calc('.$format($at[0]).'% '.($at[1] < 0 ? '-' : '+').' '.$format(abs($at[1])).'px)';
$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
<div {{ $attributes->only(['class', 'style', 'wire:key'])->class(['min-w-0', 'w-full' => ! $sized && ! $vertical, 'flex w-fit flex-col' => $vertical]) }}>
@if (filled($label))
@if ($range)
<span id="{{ $id }}-label" class="mb-2 block type-label-lg text-on-surface-variant">{{ $label }}</span>
@else
<label for="{{ $id }}" id="{{ $id }}-label" class="mb-2 block type-label-lg text-on-surface-variant">{{ $label }}</label>
@endif
@endif
<div
x-data="materialSlider"
x-on:pointerdown="press($event)"
data-slider
data-size="{{ $size }}"
@if ($vertical) data-orientation="vertical" @endif
@if ($centered) data-centered @endif
@if ($range) role="group" @if (filled($label)) aria-labelledby="{{ $id }}-label" @endif @endif
@class([
'group/slider relative cursor-pointer select-none has-disabled:cursor-not-allowed',
$across,
'touch-pan-y' => ! $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',
])
>
<div data-slider-drawing wire:ignore aria-hidden="true" @class([
'pointer-events-none absolute noscript:hidden',
'inset-y-0 inset-x-0.5 rtl:-scale-x-100' => ! $vertical,
// The same drawing, laid out as long as the slider is tall and then turned a quarter
// anticlockwise so the value grows upwards. Everything inside it — the segments, the
// ticks, the handle and its label — is placed as if the slider were lying down.
'top-1/2 left-1/2 h-[100cqw] w-[calc(100cqh-0.25rem)] -translate-1/2 -rotate-90' => $vertical,
])>
<div @class(['absolute inset-x-0 top-1/2 -translate-y-1/2', $trackHeight])>
@foreach (['start' => $inactiveInk[$color], 'active' => $activeInk[$color], 'end' => $inactiveInk[$color]] as $part => $ink)
<span
data-segment="{{ $part }}"
@if (! isset($segments[$part])) hidden @endif
@class([
'absolute inset-y-0',
$ink,
'group-has-disabled/slider:bg-on-surface/38' => $part === 'active',
'group-has-disabled/slider:bg-on-surface/12' => $part !== 'active',
])
style="{{ $segmentStyle($segments[$part] ?? null) }}"
></span>
@endforeach
@foreach ($tickMarks as $mark)
<span
data-tick="{{ $format($mark['fraction']) }}"
@if ($mark['hidden']) hidden @endif
@if ($mark['active']) data-active @endif
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-corner-full', $tickInk[$color], 'group-has-disabled/slider:bg-on-surface/38 group-has-disabled/slider:data-active:bg-on-surface/12'])
style="left: {{ $calc($mark['at']) }}"
></span>
@endforeach
@foreach (['start', 'end'] as $part)
<span
data-stop="{{ $part }}"
@if (! isset($stops[$part])) hidden @endif
@class(['absolute top-1/2 size-1 -translate-1/2 rounded-corner-full', $stopInk[$color], 'group-has-disabled/slider:bg-on-surface/38'])
style="{{ isset($stops[$part]) ? 'left: '.$calc($stops[$part]) : '' }}"
></span>
@endforeach
@if ($icon !== null)
<span
data-track-icon
@if (! $iconInActive && ! $iconInInactive) hidden @endif
@if ($iconInActive) data-active @endif
@class([
'absolute top-1/2 flex -translate-y-1/2',
'rtl:-scale-x-100' => ! $vertical,
// Turned back the quarter the drawing turns, so the glyph stands up.
'rotate-90' => $vertical,
$iconInk[$color],
'group-has-disabled/slider:text-on-surface/38',
])
style="left: {{ $calc($iconAt) }}"
>
<x-livewire-material::icon :name="$icon" :class="$iconSize === 32 ? 'size-8' : 'size-6'" />
</span>
@endif
</div>
@foreach ($thumbs as $index => $thumb)
<span data-handle="{{ $thumb }}" class="group/thumb absolute inset-y-0 w-0" style="left: {{ $calc($place($fractions[$index])) }}">
<span @class([
'absolute top-1/2 left-0 w-1 -translate-1/2 rounded-corner-full',
'transition-[width,height] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
'group-data-pressed/thumb:w-0.5 group-data-focused/thumb:w-0.5',
'outline-offset-2 outline-secondary group-data-focused/thumb:outline-3',
$handleHeight,
$activeInk[$color],
'group-has-disabled/slider:bg-on-surface/38',
])></span>
@if ($valueLabel !== 'never')
<span @class(['absolute left-0 z-10 flex -translate-x-1/2 justify-center', 'rtl:-scale-x-100' => ! $vertical, $labelBottom])>
<span
data-value-label
@class([
'flex h-11 min-w-12 items-center justify-center whitespace-nowrap rounded-corner-full bg-inverse-surface px-2.5 text-inverse-on-surface type-label-lg',
'origin-bottom' => ! $vertical,
// Turned back the quarter the drawing turns: the pill lands
// beside the handle rather than above it, and reads across.
'rotate-90' => $vertical,
'transition-[opacity,scale] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast',
'opacity-0 scale-75 group-data-pressed/thumb:opacity-100 group-data-pressed/thumb:scale-100 group-data-focused/thumb:opacity-100 group-data-focused/thumb:scale-100' => $valueLabel === 'drag',
])
>{{ $format($values[$index]) }}</span>
</span>
@endif
</span>
@endforeach
</div>
@foreach ($thumbs as $index => $thumb)
<input
type="range"
id="{{ $index === 0 ? $id : $id.'-end' }}"
@if (filled($name)) name="{{ $range ? $name.'[]' : $name }}" @endif
min="{{ $format($minimum) }}"
max="{{ $format($maximum) }}"
step="{{ $continuous ? 'any' : $format($interval) }}"
value="{{ $format($values[$index]) }}"
data-thumb="{{ $thumb }}"
@if ($vertical) aria-orientation="vertical" @endif
@if ($range) aria-label="{{ $thumb === 'start' ? __('Range start') : __('Range end') }}" @endif
@if ($described) aria-describedby="{{ $described }}" @endif
@if ($messages !== []) aria-invalid="true" @endif
@disabled($disabled)
{{ new \Illuminate\View\ComponentAttributeBag($bindingsFor($index)) }}
{{ $inputAttributes }}
@class([
'pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent opacity-0',
'noscript:pointer-events-auto noscript:static noscript:block noscript:h-6 noscript:w-full noscript:appearance-auto noscript:opacity-100',
$accent[$color],
])
/>
@endforeach
</div>
@if ($messages !== [])
<div id="{{ $id }}-hint">
@foreach ($messages as $message)
<p class="mt-1 type-body-sm text-error">{{ $message }}</p>
@endforeach
</div>
@elseif (filled($hint))
<p id="{{ $id }}-hint" class="mt-1 type-body-sm text-on-surface-variant">{{ $hint }}</p>
@endif
</div>