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
455 lines
27 KiB
PHP
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>
|