Tailwind's sm…2xl are cleared for M3's window size classes (medium 600, expanded 840, large 1200, extra-large 1600; resources/js/breakpoints.js for scripts), and its radius, shadow, text-size, weight, leading, tracking and easing scales are cleared like its palette, so only M3's utilities compile. The semantic inks are roles rather than opacities (M3 reserves 38% for disabled). state.css declares M3's state opacities as tokens, adds the dragged layer and a touch-target utility. Plan: docs/plans/material-3-alignment.md, steps 1, 2, 3 and 8. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
392 lines
23 KiB
PHP
392 lines
23 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).
|
|
|
|
`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, ticks in the other part's colour, disabled parts in on-surface at 38%
|
|
and 12%. Compose leaves the value label to the app; this one is M3's as Flutter draws it
|
|
(RoundedRectSliderValueIndicatorShape): a 32px inverse-surface pill with label-large, 10px
|
|
padding, 4px above the handle, which is 6px shorter while its focus ring shows (Material
|
|
Components' m3_slider_focus_ring_thumb_height_decrease). The Expressive sizes are
|
|
Material Components for Android's md.comp.slider.xsmall … xlarge tokens: tracks of 16, 24,
|
|
40, 56 and 96px with 8, 8, 12, 16 and 28px corners, handles of 44, 44, 44, 68 and 108px,
|
|
icons of 24, 24 and 32px, 10px from their segment's end.
|
|
|
|
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',
|
|
'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;
|
|
|
|
$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, like the icon, takes the colour of the part of the track it is not on.
|
|
$tickInk = [
|
|
'primary' => 'bg-primary data-active:bg-secondary-container', 'secondary' => 'bg-secondary data-active:bg-secondary-container',
|
|
'tertiary' => 'bg-tertiary data-active:bg-tertiary-container', 'error' => 'bg-error data-active:bg-error-container',
|
|
'success' => 'bg-success data-active:bg-success-container', 'warning' => 'bg-warning data-active:bg-warning-container',
|
|
'info' => 'bg-info data-active:bg-info-container',
|
|
];
|
|
$iconInk = [
|
|
'primary' => 'text-primary data-active:text-secondary-container', 'secondary' => 'text-secondary data-active:text-secondary-container',
|
|
'tertiary' => 'text-tertiary data-active:text-tertiary-container', 'error' => 'text-error data-active:text-error-container',
|
|
'success' => 'text-success data-active:text-success-container', 'warning' => 'text-warning data-active:text-warning-container',
|
|
'info' => 'text-info data-active:text-info-container',
|
|
];
|
|
$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-11 group-data-focused/thumb:h-9.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.
|
|
$labelBottom = ['xs' => 'bottom-[calc(50%+1.625rem)]', 'sm' => 'bottom-[calc(50%+1.625rem)]', 'md' => 'bottom-[calc(50%+1.625rem)]', 'lg' => 'bottom-[calc(50%+2.375rem)]', 'xl' => 'bottom-[calc(50%+3.625rem)]'][$size];
|
|
$thumbs = $range ? ['start', 'end'] : ['start'];
|
|
@endphp
|
|
|
|
<div {{ $attributes->only(['class', 'style', 'wire:key'])->class(['min-w-0', 'w-full' => ! $sized]) }}>
|
|
@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 ($centered) data-centered @endif
|
|
@if ($range) role="group" @if (filled($label)) aria-labelledby="{{ $id }}-label" @endif @endif
|
|
@class([
|
|
'group/slider relative cursor-pointer touch-pan-y select-none has-disabled:cursor-not-allowed',
|
|
'h-12' => in_array($size, ['xs', 'sm', 'md'], true),
|
|
'h-17' => $size === 'lg',
|
|
'h-27' => $size === 'xl',
|
|
'mt-9' => $valueLabel === 'always',
|
|
'noscript:h-auto noscript:cursor-auto noscript:space-y-2',
|
|
])
|
|
>
|
|
<div data-slider-drawing wire:ignore aria-hidden="true" class="pointer-events-none absolute inset-y-0 inset-x-0.5 rtl:-scale-x-100 noscript:hidden">
|
|
<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', $activeInk[$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', $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', $labelBottom])>
|
|
<span
|
|
data-value-label
|
|
@class([
|
|
'flex h-8 min-w-9 origin-bottom items-center justify-center whitespace-nowrap rounded-corner-full bg-inverse-surface px-2.5 text-inverse-on-surface type-label-lg',
|
|
'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 ($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>
|