Files
frontend-svelte/src/shared/ui/Slider/Slider.svelte
T

138 lines
4.5 KiB
Svelte
Raw Normal View History

<!--
Component: Slider
Styled bits-ui slider component with single value.
-->
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
Slider,
type SliderRootProps,
} from 'bits-ui';
type Props =
& Omit<
SliderRootProps,
'type' | 'onValueChange' | 'onValueCommit'
>
& {
/**
* Slider value, numeric.
*/
value: number;
/**
* Optional label displayed inline on the track before the filled range.
*/
label?: string;
/**
* A callback function called when the value changes.
* @param newValue - number
*/
onValueChange?: (newValue: number) => void;
/**
* A callback function called when the user stops dragging the thumb and the value is committed.
* @param newValue - number
*/
onValueCommit?: (newValue: number) => void;
};
let {
value = $bindable(),
orientation = 'horizontal',
class: className,
label,
...rest
}: Props = $props();
</script>
<Slider.Root
bind:value
class={cn(
'relative flex h-full w-6 touch-none select-none items-center justify-center',
orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48',
className,
)}
type="single"
{orientation}
{...rest}
>
{#snippet children(props)}
{#if label && orientation === 'horizontal'}
<span class="absolute top-0 left-0 -translate-y-1/2 text-[0.5rem] uppercase text-gray-400">
{label}
</span>
{/if}
<span
{...props}
class={cn(
'relative bg-background-muted rounded-full',
orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px',
)}
>
<Slider.Range
class={cn(
'absolute bg-foreground rounded-full',
orientation === 'horizontal' ? 'h-full' : 'w-full',
)}
/>
<Slider.Thumb
index={0}
class={cn(
'group/thumb relative block',
2026-02-15 22:57:29 +03:00
'size-2',
orientation === 'horizontal' ? '-top-1' : '-left-1',
'rounded-full',
'bg-foreground',
// Glow shadow
'shadow-[0_0_6px_rgba(0,0,0,0.4)]',
// Smooth transitions only for size/position
'duration-200 ease-out',
orientation === 'horizontal'
? 'transition-[height,top,left,box-shadow]'
: 'transition-[width,top,left,box-shadow]',
// Hover: bigger glow
'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]',
orientation === 'horizontal'
? 'hover:size-3 hover:-top-[5.5px]'
: 'hover:size-3 hover:-left-[5.5px]',
// Active: smaller glow
'active:shadow-[0_0_4px_rgba(0,0,0,0.3)]',
orientation === 'horizontal'
? 'active:h-2.5 active:-top-[4.5px]'
: 'active:w-2.5 active:-left-[4.5px]',
'focus:outline-none',
'cursor-grab active:cursor-grabbing',
)}
>
<div
class="
absolute inset-0 rounded-full
bg-background-20
opacity-0 group-hover/thumb:opacity-100
transition-opacity duration-200
"
>
</div>
<span
class={cn(
'absolute',
orientation === 'horizontal'
? '-top-8 left-1/2 -translate-x-1/2'
: 'left-5 top-1/2 -translate-y-1/2',
'px-1.5 py-0.5 rounded-md',
'bg-foreground/90 backdrop-blur-sm',
'font-mono text-[0.625rem] font-medium text-background',
'opacity-0 group-hover/thumb:opacity-100',
'transition-all duration-300',
'pointer-events-none',
'shadow-sm',
)}
>
{value}
</span>
</Slider.Thumb>
</span>
{/snippet}
</Slider.Root>