diff --git a/playground/app.vue b/playground/app.vue index 3bcad594..3b056780 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -33,6 +33,7 @@ const components = [ 'separator', 'skeleton', 'slideover', + 'slider', 'switch', 'tabs', 'textarea', diff --git a/playground/components/FormElementsExample.vue b/playground/components/FormElementsExample.vue index e3fd8d40..17fcadac 100644 --- a/playground/components/FormElementsExample.vue +++ b/playground/components/FormElementsExample.vue @@ -22,8 +22,8 @@ const schema = z.object({ }), radioGroup: z.string().refine(value => value === 'option-2', { message: 'Select Option 2' - }) - // range: z.number().max(20, { message: 'Must be less than 20' }) + }), + slider: z.number().max(20, { message: 'Must be less than 20' }) }) type Schema = z.output @@ -70,6 +70,10 @@ function onSubmit(event: FormSubmitEvent) { + + + +
Submit diff --git a/playground/pages/slider.vue b/playground/pages/slider.vue new file mode 100644 index 00000000..c70dd6dc --- /dev/null +++ b/playground/pages/slider.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/runtime/components/RadioGroup.vue b/src/runtime/components/RadioGroup.vue index 9ee6d390..3c42b430 100644 --- a/src/runtime/components/RadioGroup.vue +++ b/src/runtime/components/RadioGroup.vue @@ -39,9 +39,9 @@ export interface RadioGroupSlots { + + + + diff --git a/src/runtime/types/index.d.ts b/src/runtime/types/index.d.ts index 07c6b65c..d216ce9f 100644 --- a/src/runtime/types/index.d.ts +++ b/src/runtime/types/index.d.ts @@ -24,6 +24,7 @@ export * from '../components/RadioGroup.vue' export * from '../components/Separator.vue' export * from '../components/Skeleton.vue' export * from '../components/Slideover.vue' +export * from '../components/Slider.vue' export * from '../components/Switch.vue' export * from '../components/Tabs.vue' export * from '../components/Textarea.vue' diff --git a/src/theme/index.ts b/src/theme/index.ts index 44bf620a..8a7281a1 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -22,6 +22,7 @@ export { default as radioGroup } from './radio-group' export { default as separator } from './separator' export { default as skeleton } from './skeleton' export { default as slideover } from './slideover' +export { default as slider } from './slider' export { default as switch } from './switch' export { default as tabs } from './tabs' export { default as textarea } from './textarea' diff --git a/src/theme/slider.ts b/src/theme/slider.ts new file mode 100644 index 00000000..515e28ae --- /dev/null +++ b/src/theme/slider.ts @@ -0,0 +1,138 @@ +export default (config: { colors: string[] }) => ({ + slots: { + root: 'relative flex items-center select-none touch-none', + track: 'relative bg-gray-200 dark:bg-gray-700 overflow-hidden rounded-full grow', + range: 'absolute rounded-full', + thumb: 'rounded-full bg-white dark:bg-gray-900 ring-2 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2' + }, + variants: { + color: Object.fromEntries(config.colors.map((color: string) => [color, { + range: `bg-${color}-500 dark:bg-${color}-400`, + thumb: `ring-${color}-500 dark:ring-${color}-400 focus-visible:outline-${color}-500/50 dark:focus-visible:outline-${color}-400/50` + }])), + size: { + '2xs': { + thumb: 'size-1.5' + }, + 'xs': { + thumb: 'size-2' + }, + 'sm': { + thumb: 'size-3' + }, + 'md': { + thumb: 'size-4' + }, + 'lg': { + thumb: 'size-5' + }, + 'xl': { + thumb: 'size-6' + } + }, + orientation: { + horizontal: { + root: 'w-full', + range: 'h-full' + }, + vertical: { + root: 'flex-col h-full', + range: 'w-full' + } + }, + disabled: { + true: { + root: 'opacity-75 cursor-not-allowed' + } + } + }, + compoundVariants: [{ + orientation: 'horizontal', + size: '2xs', + class: { + root: 'h-1.5', + track: 'h-px' + } + }, { + orientation: 'horizontal', + size: 'xs', + class: { + root: 'h-2', + track: 'h-0.5' + } + }, { + orientation: 'horizontal', + size: 'sm', + class: { + root: 'h-3', + track: 'h-1' + } + }, { + orientation: 'horizontal', + size: 'md', + class: { + root: 'h-4', + track: 'h-2' + } + }, { + orientation: 'horizontal', + size: 'lg', + class: { + root: 'h-5', + track: 'h-3' + } + }, { + orientation: 'horizontal', + size: 'xl', + class: { + root: 'h-6', + track: 'h-4' + } + }, { + orientation: 'vertical', + size: '2xs', + class: { + root: 'w-1.5', + track: 'w-px' + } + }, { + orientation: 'vertical', + size: 'xs', + class: { + root: 'w-2', + track: 'w-0.5' + } + }, { + orientation: 'vertical', + size: 'sm', + class: { + root: 'w-3', + track: 'w-1' + } + }, { + orientation: 'vertical', + size: 'md', + class: { + root: 'w-4', + track: 'w-2' + } + }, { + orientation: 'vertical', + size: 'lg', + class: { + root: 'w-5', + track: 'w-3' + } + }, { + orientation: 'vertical', + size: 'xl', + class: { + root: 'w-6', + track: 'w-4' + } + }], + defaultVariants: { + size: 'sm', + color: 'primary' + } +}) diff --git a/test/components/Slider.spec.ts b/test/components/Slider.spec.ts new file mode 100644 index 00000000..59c29d32 --- /dev/null +++ b/test/components/Slider.spec.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest' +import Slider, { type SliderProps } from '../../src/runtime/components/Slider.vue' +import ComponentRender from '../component-render' + +describe('Slider', () => { + it.each([ + ['basic case', {}], + ['with class', { props: { class: 'w-48' } }], + ['with ui', { props: { ui: { track: 'bg-gray-100 dark:bg-gray-800' } } }], + ['with defaultValue', { props: { defaultValue: 10 } }], + ['with multiple thumbs', { props: { defaultValue: [0, 10] } }], + ['with name', { props: { name: 'custom-name' } }], + ['with disabled', { props: { disabled: true } }], + ['with inverted', { props: { inverted: true } }], + ['with orientation', { props: { orientation: 'vertical' as const } }], + ['with min max step', { props: { min: 4, max: 12, step: 2 } }], + ['with min steps between thumbs', { props: { defaultValue: [0, 30], minStepsBetweenThumbs: 30 } }], + ['with color', { props: { color: 'red' as const } }], + ['with size 2xs', { props: { size: '2xs' as const } }], + ['with size xs', { props: { size: 'xs' as const } }], + ['with size sm', { props: { size: 'sm' as const } }], + ['with size md', { props: { size: 'md' as const } }], + ['with size lg', { props: { size: 'lg' as const } }], + ['with size xl', { props: { size: 'xl' as const } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: SliderProps, slots?: any }) => { + const html = await ComponentRender(nameOrHtml, options, Slider) + expect(html).toMatchSnapshot() + }) +}) diff --git a/test/components/__snapshots__/Slider.spec.ts.snap b/test/components/__snapshots__/Slider.spec.ts.snap new file mode 100644 index 00000000..712694c5 --- /dev/null +++ b/test/components/__snapshots__/Slider.spec.ts.snap @@ -0,0 +1,111 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Slider > renders basic case correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with class correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with color correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with defaultValue correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with disabled correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with inverted correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with min max step correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with min steps between thumbs correctly 1`] = ` +" +
+
+" +`; + +exports[`Slider > renders with multiple thumbs correctly 1`] = ` +" +
+
+" +`; + +exports[`Slider > renders with name correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with orientation correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with size 2xs correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with size lg correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with size md correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with size sm correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with size xl correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with size xs correctly 1`] = ` +" +
+" +`; + +exports[`Slider > renders with ui correctly 1`] = ` +" +
+" +`;