Component: Vue Widget Slider (new) (#5516)

* feat: Initial shadcn configuration

* component: Add Slider component from shadcn-vue

* deps: Add tw-animate-css

* component: Align slider with Figma styles

* component: Set the step value for the slider, update styles

* fix: update component tests to work with Array of values

* vite: Don't reload dev server for test changes

* component: Swap text for a number input kept in sync with the slider

* cleanup: Don't need the override if the input isn't type="number"

* test: add step size tests

* cleanup: Don't need cn for these

* css: Update token names to match new Figma Variables

* lint: Fix camelCase vs train-case in passthrough

* feat: If the value is deleted, revert to the slider state cc: @PabloWiedemann

* feat: Improve cursor styles, grabbable thumb, clickable track

* lint: temporarily disable some warnings

* feat: Grabbing while sliding (most of the time)
This commit is contained in:
Alexander Brown
2025-09-12 18:52:18 -07:00
committed by GitHub
parent c588f2f457
commit 1845708ddb
12 changed files with 402 additions and 150 deletions

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { reactiveOmit } from '@vueuse/core'
import type { SliderRootEmits, SliderRootProps } from 'reka-ui'
import {
SliderRange,
SliderRoot,
SliderThumb,
SliderTrack,
useForwardPropsEmits
} from 'reka-ui'
import { type HTMLAttributes, ref } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const props = defineProps<
SliderRootProps & { class?: HTMLAttributes['class'] }
>()
const pressed = ref(false)
const setPressed = (val: boolean) => {
pressed.value = val
}
const emits = defineEmits<SliderRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SliderRoot
v-slot="{ modelValue }"
data-slot="slider"
:class="
cn(
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50',
'data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
props.class
)
"
v-bind="forwarded"
@slide-start="() => setPressed(true)"
@slide-move="() => setPressed(true)"
@slide-end="() => setPressed(false)"
>
<SliderTrack
data-slot="slider-track"
:class="
cn(
'bg-node-stroke relative grow overflow-hidden rounded-full',
'cursor-pointer',
'data-[orientation=horizontal]:h-0.5 data-[orientation=horizontal]:w-full',
'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-0.5'
)
"
>
<SliderRange
data-slot="slider-range"
class="bg-node-component-surface-highlight absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
/>
</SliderTrack>
<SliderThumb
v-for="(_, key) in modelValue"
:key="key"
data-slot="slider-thumb"
:class="
cn(
'bg-node-component-surface-highlight ring-node-component-surface-selected block size-3.5 shrink-0 rounded-full shadow-sm transition-[color,box-shadow]',
'cursor-grab',
'hover:ring-2 focus-visible:ring-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
{ 'cursor-grabbing': pressed }
)
"
/>
</SliderRoot>
</template>