2026-02-08 14:18:17 +03:00
|
|
|
<!--
|
|
|
|
|
Component: Slider
|
2026-02-24 17:57:40 +03:00
|
|
|
Single-value slider using bits-ui Slider primitive.
|
|
|
|
|
Swiss design: 1px track, diamond thumb (rotate-45), brand accent.
|
2026-02-08 14:18:17 +03:00
|
|
|
-->
|
|
|
|
|
<script lang="ts">
|
2026-02-25 10:01:26 +03:00
|
|
|
import {
|
|
|
|
|
type Orientation,
|
|
|
|
|
Slider,
|
|
|
|
|
} from 'bits-ui';
|
2026-02-08 14:18:17 +03:00
|
|
|
|
2026-02-24 17:57:40 +03:00
|
|
|
interface Props {
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* Slider value
|
|
|
|
|
* @default 0
|
|
|
|
|
*/
|
2026-02-24 17:57:40 +03:00
|
|
|
value?: number;
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* Minimum value
|
|
|
|
|
* @default 0
|
|
|
|
|
*/
|
2026-02-24 17:57:40 +03:00
|
|
|
min?: number;
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* Maximum value
|
|
|
|
|
* @default 100
|
|
|
|
|
*/
|
2026-02-24 17:57:40 +03:00
|
|
|
max?: number;
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* Step increment
|
|
|
|
|
* @default 1
|
|
|
|
|
*/
|
2026-02-24 17:57:40 +03:00
|
|
|
step?: number;
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* Disabled state
|
|
|
|
|
* @default false
|
|
|
|
|
*/
|
2026-02-24 17:57:40 +03:00
|
|
|
disabled?: boolean;
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* Slider orientation
|
|
|
|
|
* @default 'horizontal'
|
|
|
|
|
*/
|
2026-02-25 10:01:26 +03:00
|
|
|
orientation?: Orientation;
|
2026-02-24 17:57:40 +03:00
|
|
|
/**
|
2026-03-02 22:19:35 +03:00
|
|
|
* Value formatter
|
2026-02-24 17:57:40 +03:00
|
|
|
* @default (v) => v
|
|
|
|
|
*/
|
|
|
|
|
format?: (v: number) => string | number;
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* Value change callback
|
|
|
|
|
*/
|
2026-02-24 17:57:40 +03:00
|
|
|
onValueChange?: (v: number) => void;
|
2026-03-02 22:19:35 +03:00
|
|
|
/**
|
|
|
|
|
* CSS classes
|
|
|
|
|
*/
|
2026-02-24 17:57:40 +03:00
|
|
|
class?: string;
|
|
|
|
|
}
|
2026-02-08 14:18:17 +03:00
|
|
|
|
2026-02-18 16:55:11 +03:00
|
|
|
let {
|
2026-02-24 17:57:40 +03:00
|
|
|
value = $bindable(0),
|
|
|
|
|
min = 0,
|
|
|
|
|
max = 100,
|
|
|
|
|
step = 1,
|
|
|
|
|
disabled = false,
|
2026-02-18 16:55:11 +03:00
|
|
|
orientation = 'horizontal',
|
2026-02-24 17:57:40 +03:00
|
|
|
format = (v: number) => v,
|
|
|
|
|
onValueChange,
|
2026-02-18 16:55:11 +03:00
|
|
|
class: className,
|
|
|
|
|
}: Props = $props();
|
2026-02-24 17:57:40 +03:00
|
|
|
|
|
|
|
|
const isVertical = $derived(orientation === 'vertical');
|
|
|
|
|
|
2026-04-17 09:41:55 +03:00
|
|
|
const labelClasses = `font-mono text-2xs tabular-nums shrink-0
|
2026-05-25 10:20:40 +03:00
|
|
|
text-subtle
|
2026-02-24 17:57:40 +03:00
|
|
|
group-hover:text-neutral-700 dark:group-hover:text-neutral-300
|
|
|
|
|
transition-colors`;
|
|
|
|
|
|
|
|
|
|
const thumbClasses = `block w-2.5 h-2.5 bg-brand
|
2026-05-25 10:20:40 +03:00
|
|
|
rotate-45 shadow-rest
|
2026-02-24 17:57:40 +03:00
|
|
|
hover:scale-125
|
|
|
|
|
focus-visible:outline-none
|
|
|
|
|
focus-visible:ring-2 focus-visible:ring-brand/20
|
|
|
|
|
data-active:scale-90
|
2026-05-25 10:20:40 +03:00
|
|
|
transition-transform duration-fast
|
2026-02-24 17:57:40 +03:00
|
|
|
disabled:pointer-events-none disabled:opacity-50
|
|
|
|
|
cursor-grab active:cursor-grabbing`;
|
2026-02-08 14:18:17 +03:00
|
|
|
</script>
|
|
|
|
|
|
2026-02-24 17:57:40 +03:00
|
|
|
{#if isVertical}
|
|
|
|
|
<div class="inline-flex flex-col items-center gap-3 group h-full {className ?? ''}">
|
|
|
|
|
<span class="{labelClasses} text-center">
|
|
|
|
|
{format(value)}
|
|
|
|
|
</span>
|
2026-02-08 14:18:17 +03:00
|
|
|
|
2026-02-24 17:57:40 +03:00
|
|
|
<Slider.Root
|
|
|
|
|
type="single"
|
|
|
|
|
orientation="vertical"
|
|
|
|
|
bind:value
|
|
|
|
|
{min}
|
|
|
|
|
{max}
|
|
|
|
|
{step}
|
|
|
|
|
{disabled}
|
|
|
|
|
onValueChange={(v => onValueChange?.(v))}
|
|
|
|
|
class="
|
|
|
|
|
relative flex flex-col items-center select-none touch-none
|
|
|
|
|
w-5 h-full grow cursor-row-resize
|
|
|
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
{#snippet children({ thumbItems })}
|
|
|
|
|
<span
|
2026-02-08 14:18:17 +03:00
|
|
|
class="
|
2026-02-24 17:57:40 +03:00
|
|
|
bg-neutral-200 dark:bg-neutral-800
|
|
|
|
|
relative grow w-px overflow-visible
|
|
|
|
|
group-hover:bg-neutral-300 dark:group-hover:bg-neutral-700
|
|
|
|
|
transition-colors
|
2026-02-08 14:18:17 +03:00
|
|
|
"
|
|
|
|
|
>
|
2026-02-24 17:57:40 +03:00
|
|
|
<Slider.Range class="absolute bg-brand w-full" />
|
|
|
|
|
</span>
|
2026-02-08 14:18:17 +03:00
|
|
|
|
2026-02-24 17:57:40 +03:00
|
|
|
{#each thumbItems as thumb (thumb)}
|
|
|
|
|
<Slider.Thumb
|
|
|
|
|
index={thumb.index}
|
|
|
|
|
class={thumbClasses}
|
|
|
|
|
aria-label="Value"
|
|
|
|
|
/>
|
|
|
|
|
{/each}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Slider.Root>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="flex items-center gap-4 group w-full {className ?? ''}">
|
|
|
|
|
<Slider.Root
|
|
|
|
|
type="single"
|
|
|
|
|
orientation="horizontal"
|
|
|
|
|
bind:value
|
|
|
|
|
{min}
|
|
|
|
|
{max}
|
|
|
|
|
{step}
|
|
|
|
|
{disabled}
|
|
|
|
|
onValueChange={(v => onValueChange?.(v))}
|
|
|
|
|
class="
|
|
|
|
|
relative flex items-center select-none touch-none
|
|
|
|
|
w-full h-5 cursor-col-resize
|
|
|
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
{#snippet children({ thumbItems })}
|
2026-02-08 14:18:17 +03:00
|
|
|
<span
|
2026-02-24 17:57:40 +03:00
|
|
|
class="
|
|
|
|
|
bg-neutral-200 dark:bg-neutral-800
|
|
|
|
|
relative grow h-px overflow-visible
|
|
|
|
|
group-hover:bg-neutral-300 dark:group-hover:bg-neutral-700
|
|
|
|
|
transition-colors
|
|
|
|
|
"
|
2026-02-08 14:18:17 +03:00
|
|
|
>
|
2026-02-24 17:57:40 +03:00
|
|
|
<Slider.Range class="absolute bg-brand h-full" />
|
2026-02-08 14:18:17 +03:00
|
|
|
</span>
|
2026-02-24 17:57:40 +03:00
|
|
|
|
|
|
|
|
{#each thumbItems as thumb (thumb)}
|
|
|
|
|
<Slider.Thumb
|
|
|
|
|
index={thumb.index}
|
|
|
|
|
class={thumbClasses}
|
|
|
|
|
aria-label="Value"
|
|
|
|
|
/>
|
|
|
|
|
{/each}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Slider.Root>
|
|
|
|
|
|
|
|
|
|
<!-- Label: right of slider -->
|
|
|
|
|
<span class="{labelClasses} w-12 text-right">
|
|
|
|
|
{format(value)}
|
2026-02-08 14:18:17 +03:00
|
|
|
</span>
|
2026-02-24 17:57:40 +03:00
|
|
|
</div>
|
|
|
|
|
{/if}
|