@stasho/ds 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +88 -0
  2. package/src/components/alert/alert.tsx +204 -0
  3. package/src/components/badge/badge.tsx +139 -0
  4. package/src/components/breadcrumb/breadcrumb.tsx +130 -0
  5. package/src/components/button/button.tsx +153 -0
  6. package/src/components/card/card.tsx +48 -0
  7. package/src/components/checkbox/checkbox.tsx +82 -0
  8. package/src/components/combobox/combobox.tsx +166 -0
  9. package/src/components/copyable-text/copyable-text.tsx +194 -0
  10. package/src/components/dialog/dialog.tsx +145 -0
  11. package/src/components/form-field/form-field.tsx +71 -0
  12. package/src/components/input/input.tsx +52 -0
  13. package/src/components/logo/logo.tsx +68 -0
  14. package/src/components/multi-select/multi-select.tsx +312 -0
  15. package/src/components/pagination/pagination.tsx +218 -0
  16. package/src/components/progress-bar/progress-bar.tsx +134 -0
  17. package/src/components/radio-group/radio-group.tsx +87 -0
  18. package/src/components/select/select.tsx +124 -0
  19. package/src/components/slider/slider.tsx +127 -0
  20. package/src/components/status-dot/status-dot.tsx +53 -0
  21. package/src/components/stepper/stepper.tsx +229 -0
  22. package/src/components/switch/switch.tsx +74 -0
  23. package/src/components/table/table.tsx +260 -0
  24. package/src/components/tabs/tabs.tsx +493 -0
  25. package/src/components/textarea/textarea.tsx +56 -0
  26. package/src/components/tooltip/tooltip.tsx +35 -0
  27. package/src/components/ui/skeleton.tsx +21 -0
  28. package/src/components/ui/spinner.tsx +27 -0
  29. package/src/lib/cn.ts +6 -0
  30. package/src/styles/tokens.css +459 -0
@@ -0,0 +1,218 @@
1
+ import { forwardRef, type HTMLAttributes } from "react";
2
+ import {
3
+ CaretDoubleLeft,
4
+ CaretDoubleRight,
5
+ CaretLeft,
6
+ CaretRight,
7
+ } from "@phosphor-icons/react";
8
+ import { cn } from "@ac/lib/cn";
9
+
10
+ type PageItem = number | "ellipsis";
11
+
12
+ type BuildPageRangeArgs = {
13
+ page: number;
14
+ totalPages: number;
15
+ siblingCount: number;
16
+ showFirstLast: boolean;
17
+ };
18
+
19
+ function buildPageRange({
20
+ page,
21
+ totalPages,
22
+ siblingCount,
23
+ showFirstLast,
24
+ }: BuildPageRangeArgs): PageItem[] {
25
+ if (!showFirstLast) {
26
+ const left = Math.max(page - siblingCount, 1);
27
+ const right = Math.min(page + siblingCount, totalPages);
28
+ const items: PageItem[] = [];
29
+ for (let i = left; i <= right; i++) {
30
+ items.push(i);
31
+ }
32
+ return items;
33
+ }
34
+
35
+ const maxSlots = 2 * siblingCount + 5;
36
+
37
+ if (totalPages <= maxSlots) {
38
+ const items: PageItem[] = [];
39
+ for (let i = 1; i <= totalPages; i++) {
40
+ items.push(i);
41
+ }
42
+ return items;
43
+ }
44
+
45
+ const nearStart = page <= siblingCount + 3;
46
+ const nearEnd = page >= totalPages - siblingCount - 2;
47
+
48
+ if (nearStart) {
49
+ const items: PageItem[] = [];
50
+ for (let i = 1; i <= maxSlots - 2; i++) {
51
+ items.push(i);
52
+ }
53
+ items.push("ellipsis", totalPages);
54
+ return items;
55
+ }
56
+
57
+ if (nearEnd) {
58
+ const items: PageItem[] = [1, "ellipsis"];
59
+ for (let i = totalPages - (maxSlots - 3); i <= totalPages; i++) {
60
+ items.push(i);
61
+ }
62
+ return items;
63
+ }
64
+
65
+ const items: PageItem[] = [1, "ellipsis"];
66
+ for (let i = page - siblingCount; i <= page + siblingCount; i++) {
67
+ items.push(i);
68
+ }
69
+ items.push("ellipsis", totalPages);
70
+ return items;
71
+ }
72
+
73
+ type PaginationProps = Omit<HTMLAttributes<HTMLElement>, "onChange"> & {
74
+ page: number;
75
+ totalPages: number;
76
+ onPageChange: (page: number) => void;
77
+ siblingCount?: number;
78
+ showFirstLast?: boolean;
79
+ };
80
+
81
+ const NAV_BUTTON = [
82
+ "inline-flex items-center justify-center",
83
+ "size-8 rounded-full",
84
+ "text-primary-600 dark:text-primary-400",
85
+ "hover:bg-primary-100 dark:hover:bg-primary-200/10",
86
+ "transition-colors cursor-pointer",
87
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500",
88
+ ].join(" ");
89
+
90
+ const NAV_DISABLED = "opacity-50 pointer-events-none";
91
+
92
+ const PAGE_BUTTON = [
93
+ "inline-flex items-center justify-center",
94
+ "size-8 rounded-full",
95
+ "font-heading font-bold text-lg",
96
+ "text-primary-600 dark:text-primary-400",
97
+ "hover:bg-primary-100 dark:hover:bg-primary-200/10",
98
+ "transition-colors cursor-pointer",
99
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500",
100
+ ].join(" ");
101
+
102
+ const PAGE_ACTIVE = [
103
+ "bg-primary-400 text-white dark:bg-primary-600 dark:text-white",
104
+ "hover:bg-primary-400 dark:hover:bg-primary-600",
105
+ ].join(" ");
106
+
107
+ const Pagination = forwardRef<HTMLElement, PaginationProps>(
108
+ (
109
+ {
110
+ page,
111
+ totalPages,
112
+ onPageChange,
113
+ siblingCount = 1,
114
+ showFirstLast = true,
115
+ className,
116
+ ...rest
117
+ },
118
+ ref,
119
+ ) => {
120
+ const items = buildPageRange({
121
+ page,
122
+ totalPages,
123
+ siblingCount,
124
+ showFirstLast,
125
+ });
126
+
127
+ const isFirst = page <= 1;
128
+ const isLast = page >= totalPages;
129
+
130
+ return (
131
+ <nav
132
+ ref={ref}
133
+ aria-label="Pagination"
134
+ className={cn("flex items-center gap-4", className)}
135
+ {...rest}
136
+ >
137
+ {showFirstLast && (
138
+ <button
139
+ type="button"
140
+ className={cn(NAV_BUTTON, isFirst && NAV_DISABLED)}
141
+ aria-label="First page"
142
+ aria-disabled={isFirst || undefined}
143
+ onClick={isFirst ? undefined : () => onPageChange(1)}
144
+ >
145
+ <CaretDoubleLeft
146
+ weight="bold"
147
+ className="size-4"
148
+ aria-hidden="true"
149
+ />
150
+ </button>
151
+ )}
152
+
153
+ <button
154
+ type="button"
155
+ className={cn(NAV_BUTTON, isFirst && NAV_DISABLED)}
156
+ aria-label="Previous page"
157
+ aria-disabled={isFirst || undefined}
158
+ onClick={isFirst ? undefined : () => onPageChange(page - 1)}
159
+ >
160
+ <CaretLeft weight="bold" className="size-4" aria-hidden="true" />
161
+ </button>
162
+
163
+ {items.map((item, i) =>
164
+ item === "ellipsis" ? (
165
+ <span
166
+ key={i}
167
+ className="inline-flex items-center justify-center size-8 font-heading font-bold text-lg text-primary-600 dark:text-primary-400 select-none"
168
+ aria-hidden="true"
169
+ >
170
+ {"\u2026"}
171
+ </span>
172
+ ) : (
173
+ <button
174
+ key={i}
175
+ type="button"
176
+ className={cn(PAGE_BUTTON, item === page && PAGE_ACTIVE)}
177
+ aria-label={`Page ${item}`}
178
+ aria-current={item === page ? "page" : undefined}
179
+ onClick={item === page ? undefined : () => onPageChange(item)}
180
+ >
181
+ {item}
182
+ </button>
183
+ ),
184
+ )}
185
+
186
+ <button
187
+ type="button"
188
+ className={cn(NAV_BUTTON, isLast && NAV_DISABLED)}
189
+ aria-label="Next page"
190
+ aria-disabled={isLast || undefined}
191
+ onClick={isLast ? undefined : () => onPageChange(page + 1)}
192
+ >
193
+ <CaretRight weight="bold" className="size-4" aria-hidden="true" />
194
+ </button>
195
+
196
+ {showFirstLast && (
197
+ <button
198
+ type="button"
199
+ className={cn(NAV_BUTTON, isLast && NAV_DISABLED)}
200
+ aria-label="Last page"
201
+ aria-disabled={isLast || undefined}
202
+ onClick={isLast ? undefined : () => onPageChange(totalPages)}
203
+ >
204
+ <CaretDoubleRight
205
+ weight="bold"
206
+ className="size-4"
207
+ aria-hidden="true"
208
+ />
209
+ </button>
210
+ )}
211
+ </nav>
212
+ );
213
+ },
214
+ );
215
+
216
+ Pagination.displayName = "Pagination";
217
+
218
+ export { buildPageRange, Pagination, type PageItem, type PaginationProps };
@@ -0,0 +1,134 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ forwardRef,
5
+ isValidElement,
6
+ useId,
7
+ type HTMLAttributes,
8
+ type ReactNode,
9
+ } from "react";
10
+ import { cva, type VariantProps } from "class-variance-authority";
11
+ import { cn } from "@ac/lib/cn";
12
+
13
+ /* ── Variants ──────────────────────────────────── */
14
+
15
+ const progressBarVariants = cva(
16
+ "relative rounded-full bg-surface overflow-hidden",
17
+ {
18
+ variants: {
19
+ size: {
20
+ sm: "h-1",
21
+ md: "h-1.5",
22
+ lg: "h-2.5",
23
+ },
24
+ },
25
+ defaultVariants: { size: "md" },
26
+ },
27
+ );
28
+
29
+ /* ── ProgressBarDescription ────────────────────── */
30
+
31
+ type ProgressBarDescriptionProps = HTMLAttributes<HTMLSpanElement>;
32
+
33
+ const ProgressBarDescription = forwardRef<
34
+ HTMLSpanElement,
35
+ ProgressBarDescriptionProps
36
+ >(({ className, ...rest }, ref) => (
37
+ <span
38
+ ref={ref}
39
+ data-description=""
40
+ className={cn("text-xs text-muted-foreground", className)}
41
+ {...rest}
42
+ />
43
+ ));
44
+
45
+ ProgressBarDescription.displayName = "ProgressBarDescription";
46
+
47
+ /* ── ProgressBar ───────────────────────────────── */
48
+
49
+ type ProgressBarProps = HTMLAttributes<HTMLDivElement> &
50
+ VariantProps<typeof progressBarVariants> & {
51
+ /** 0–max. Omit for indeterminate mode. */
52
+ value?: number;
53
+ /** Upper bound. Default 100. */
54
+ max?: number;
55
+ /** Accessible label (required). Becomes aria-label. */
56
+ label: string;
57
+ children?: ReactNode;
58
+ };
59
+
60
+ const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
61
+ ({ value, max = 100, label, size, className, children, ...rest }, ref) => {
62
+ const descId = useId();
63
+ const indeterminate = value === undefined;
64
+
65
+ const clampedPercent = indeterminate
66
+ ? 0
67
+ : (Math.min(Math.max(value, 0), max) / max) * 100;
68
+
69
+ let hasDescription = false;
70
+ Children.forEach(children, (child) => {
71
+ if (isValidElement(child) && child.type === ProgressBarDescription) {
72
+ hasDescription = true;
73
+ }
74
+ });
75
+
76
+ const track = (
77
+ <div
78
+ ref={hasDescription ? undefined : ref}
79
+ role="progressbar"
80
+ aria-label={label}
81
+ aria-valuemin={indeterminate ? undefined : 0}
82
+ aria-valuemax={indeterminate ? undefined : max}
83
+ aria-valuenow={indeterminate ? undefined : Math.round(clampedPercent)}
84
+ aria-describedby={hasDescription ? descId : undefined}
85
+ className={cn(progressBarVariants({ size }), className)}
86
+ {...(hasDescription ? {} : rest)}
87
+ >
88
+ <div
89
+ data-fill=""
90
+ data-indeterminate={indeterminate ? "" : undefined}
91
+ className={cn(
92
+ "h-full rounded-full bg-primary",
93
+ indeterminate
94
+ ? "animate-progress-indeterminate"
95
+ : "transition-all",
96
+ "motion-reduce:animate-none",
97
+ )}
98
+ style={indeterminate ? undefined : { width: `${clampedPercent}%` }}
99
+ />
100
+ </div>
101
+ );
102
+
103
+ if (!hasDescription) return track;
104
+
105
+ const descChildren = Children.map(children, (child) => {
106
+ if (
107
+ isValidElement<ProgressBarDescriptionProps>(child) &&
108
+ child.type === ProgressBarDescription
109
+ ) {
110
+ return cloneElement(child, { id: descId });
111
+ }
112
+ return child;
113
+ });
114
+
115
+ return (
116
+ <div ref={ref} className="flex flex-col gap-1.5" {...rest}>
117
+ {track}
118
+ {descChildren}
119
+ </div>
120
+ );
121
+ },
122
+ );
123
+
124
+ ProgressBar.displayName = "ProgressBar";
125
+
126
+ /* ── Exports ───────────────────────────────────── */
127
+
128
+ export {
129
+ ProgressBar,
130
+ ProgressBarDescription,
131
+ progressBarVariants,
132
+ type ProgressBarProps,
133
+ type ProgressBarDescriptionProps,
134
+ };
@@ -0,0 +1,87 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef } from "react";
2
+ import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@ac/lib/cn";
5
+
6
+ const radioItemVariants = cva(
7
+ [
8
+ "peer shrink-0 rounded-full bg-surface",
9
+ "border-3 border-edge",
10
+ "hover:border-edge-hover",
11
+ "focus-visible:outline-none focus-visible:ring-3",
12
+ "focus-visible:ring-primary-500",
13
+ "disabled:opacity-50 disabled:pointer-events-none",
14
+ "data-[state=checked]:border-primary",
15
+ "transition-colors",
16
+ ].join(" "),
17
+ {
18
+ variants: {
19
+ size: {
20
+ xs: "size-4",
21
+ sm: "size-5",
22
+ md: "size-6",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ size: "md",
27
+ },
28
+ },
29
+ );
30
+
31
+ type RadioGroupProps = ComponentPropsWithoutRef<
32
+ typeof RadioGroupPrimitive.Root
33
+ > & {
34
+ size?: "xs" | "sm" | "md";
35
+ };
36
+
37
+ const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
38
+ ({ className, ...rest }, ref) => {
39
+ return (
40
+ <RadioGroupPrimitive.Root
41
+ ref={ref}
42
+ className={cn("flex flex-col gap-2", className)}
43
+ {...rest}
44
+ />
45
+ );
46
+ },
47
+ );
48
+ RadioGroup.displayName = "RadioGroup";
49
+
50
+ type RadioGroupItemProps = Omit<
51
+ ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
52
+ "size"
53
+ > &
54
+ VariantProps<typeof radioItemVariants>;
55
+
56
+ const RadioGroupItem = forwardRef<HTMLButtonElement, RadioGroupItemProps>(
57
+ ({ size, className, ...rest }, ref) => {
58
+ return (
59
+ <RadioGroupPrimitive.Item
60
+ ref={ref}
61
+ className={cn(radioItemVariants({ size }), className)}
62
+ {...rest}
63
+ >
64
+ <RadioGroupPrimitive.Indicator
65
+ forceMount
66
+ className={cn(
67
+ "flex size-full items-center justify-center",
68
+ "[clip-path:circle(0%_at_50%_50%)]",
69
+ "data-[state=checked]:[clip-path:circle(100%_at_50%_50%)]",
70
+ "transition-[clip-path] duration-200 ease-in-out motion-reduce:transition-none",
71
+ )}
72
+ >
73
+ <span className="block size-[80%] rounded-full bg-primary" />
74
+ </RadioGroupPrimitive.Indicator>
75
+ </RadioGroupPrimitive.Item>
76
+ );
77
+ },
78
+ );
79
+ RadioGroupItem.displayName = "RadioGroupItem";
80
+
81
+ export {
82
+ RadioGroup,
83
+ RadioGroupItem,
84
+ radioItemVariants,
85
+ type RadioGroupProps,
86
+ type RadioGroupItemProps,
87
+ };
@@ -0,0 +1,124 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef } from "react";
2
+ import { Select as SelectPrimitive } from "radix-ui";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { CaretDown, Check } from "@phosphor-icons/react";
5
+ import { cn } from "@ac/lib/cn";
6
+
7
+ const triggerVariants = cva(
8
+ [
9
+ "inline-flex items-center justify-between",
10
+ "w-full font-sans text-foreground bg-primary-100 dark:bg-base-700",
11
+ "border-0 rounded-full",
12
+ "focus-visible:outline-none focus-visible:ring-3",
13
+ "focus-visible:ring-primary-500",
14
+ "disabled:opacity-50 disabled:pointer-events-none",
15
+ "ring-0 transition-colors",
16
+ "data-[placeholder]:text-muted-foreground",
17
+ ].join(" "),
18
+ {
19
+ variants: {
20
+ size: {
21
+ sm: "py-1.5 px-4 text-sm",
22
+ md: "py-2 px-5 text-base",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ size: "md",
27
+ },
28
+ },
29
+ );
30
+
31
+ type SelectOption = {
32
+ value: string;
33
+ label: string;
34
+ disabled?: boolean;
35
+ };
36
+
37
+ type SelectProps = Omit<
38
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Root>,
39
+ "children"
40
+ > &
41
+ VariantProps<typeof triggerVariants> & {
42
+ options: SelectOption[];
43
+ placeholder?: string;
44
+ error?: boolean;
45
+ className?: string;
46
+ id?: string;
47
+ "aria-describedby"?: string;
48
+ };
49
+
50
+ const Select = forwardRef<HTMLButtonElement, SelectProps>(
51
+ (
52
+ {
53
+ options,
54
+ placeholder,
55
+ size,
56
+ error = false,
57
+ className,
58
+ id,
59
+ "aria-describedby": ariaDescribedBy,
60
+ ...rest
61
+ },
62
+ ref,
63
+ ) => {
64
+ return (
65
+ <SelectPrimitive.Root {...rest}>
66
+ <SelectPrimitive.Trigger
67
+ ref={ref}
68
+ id={id}
69
+ aria-describedby={ariaDescribedBy}
70
+ aria-invalid={error || undefined}
71
+ className={cn(
72
+ triggerVariants({ size }),
73
+ error && "border-3 border-error-400 hover:border-error-500",
74
+ className,
75
+ )}
76
+ >
77
+ <SelectPrimitive.Value placeholder={placeholder} />
78
+ <SelectPrimitive.Icon className="ml-2 shrink-0 text-muted-foreground">
79
+ <CaretDown weight="bold" className="size-4" />
80
+ </SelectPrimitive.Icon>
81
+ </SelectPrimitive.Trigger>
82
+ <SelectPrimitive.Portal>
83
+ <SelectPrimitive.Content
84
+ className={cn(
85
+ "z-50 overflow-hidden rounded-2xl",
86
+ "bg-surface border border-edge shadow-brand",
87
+ )}
88
+ position="popper"
89
+ sideOffset={4}
90
+ >
91
+ <SelectPrimitive.Viewport className="p-1 min-w-[var(--radix-select-trigger-width)]">
92
+ {options.map((option) => (
93
+ <SelectPrimitive.Item
94
+ key={option.value}
95
+ value={option.value}
96
+ disabled={option.disabled ?? false}
97
+ className={cn(
98
+ "relative flex items-center rounded-xl px-4 py-2",
99
+ "text-sm text-foreground cursor-pointer select-none",
100
+ "outline-none",
101
+ "data-[highlighted]:bg-muted",
102
+ "data-[disabled]:opacity-50",
103
+ "data-[disabled]:pointer-events-none",
104
+ )}
105
+ >
106
+ <SelectPrimitive.ItemText>
107
+ {option.label}
108
+ </SelectPrimitive.ItemText>
109
+ <SelectPrimitive.ItemIndicator className="ml-auto pl-4">
110
+ <Check weight="bold" className="size-4" />
111
+ </SelectPrimitive.ItemIndicator>
112
+ </SelectPrimitive.Item>
113
+ ))}
114
+ </SelectPrimitive.Viewport>
115
+ </SelectPrimitive.Content>
116
+ </SelectPrimitive.Portal>
117
+ </SelectPrimitive.Root>
118
+ );
119
+ },
120
+ );
121
+
122
+ Select.displayName = "Select";
123
+
124
+ export { Select, triggerVariants, type SelectProps, type SelectOption };
@@ -0,0 +1,127 @@
1
+ import { forwardRef, useState, type ComponentPropsWithoutRef } from "react";
2
+ import { Slider as SliderPrimitive } from "radix-ui";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@ac/lib/cn";
5
+
6
+ const trackVariants = cva(
7
+ [
8
+ "relative w-full grow overflow-hidden rounded-full",
9
+ "bg-neutral-200 dark:bg-base-700",
10
+ ].join(" "),
11
+ {
12
+ variants: {
13
+ size: {
14
+ sm: "h-1.5",
15
+ md: "h-2",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ size: "md",
20
+ },
21
+ },
22
+ );
23
+
24
+ const thumbVariants = cva(
25
+ [
26
+ "block rounded-full bg-white",
27
+ "border-2 border-primary-500",
28
+ "focus-visible:outline-none focus-visible:ring-3",
29
+ "focus-visible:ring-primary-500",
30
+ "disabled:pointer-events-none",
31
+ ].join(" "),
32
+ {
33
+ variants: {
34
+ size: {
35
+ sm: "size-4",
36
+ md: "size-5",
37
+ },
38
+ },
39
+ defaultVariants: {
40
+ size: "md",
41
+ },
42
+ },
43
+ );
44
+
45
+ type SliderProps = Omit<
46
+ ComponentPropsWithoutRef<typeof SliderPrimitive.Root>,
47
+ "size"
48
+ > &
49
+ VariantProps<typeof trackVariants> & {
50
+ error?: boolean;
51
+ showTooltip?: boolean;
52
+ };
53
+
54
+ const Slider = forwardRef<
55
+ React.ComponentRef<typeof SliderPrimitive.Root>,
56
+ SliderProps
57
+ >(
58
+ (
59
+ {
60
+ size,
61
+ error = false,
62
+ showTooltip = false,
63
+ className,
64
+ disabled,
65
+ onValueChange: onValueChangeProp,
66
+ ...rootProps
67
+ },
68
+ ref,
69
+ ) => {
70
+ const [hovering, setHovering] = useState(false);
71
+ const [internalValue, setInternalValue] = useState(
72
+ rootProps.defaultValue ?? rootProps.value ?? [0],
73
+ );
74
+
75
+ const displayValue = rootProps.value ?? internalValue;
76
+
77
+ return (
78
+ <SliderPrimitive.Root
79
+ ref={ref}
80
+ {...(disabled ? { disabled: true } : {})}
81
+ className={cn(
82
+ "relative flex w-full touch-none select-none items-center",
83
+ disabled && "opacity-50 pointer-events-none",
84
+ className,
85
+ )}
86
+ onValueChange={(val) => {
87
+ setInternalValue(val);
88
+ onValueChangeProp?.(val);
89
+ }}
90
+ onPointerEnter={() => setHovering(true)}
91
+ onPointerLeave={() => setHovering(false)}
92
+ {...rootProps}
93
+ >
94
+ <SliderPrimitive.Track
95
+ className={cn(
96
+ trackVariants({ size }),
97
+ error && "ring-2 ring-error-400",
98
+ )}
99
+ >
100
+ <SliderPrimitive.Range className="absolute h-full bg-primary-500 rounded-full" />
101
+ </SliderPrimitive.Track>
102
+ {displayValue.map((val, i) => (
103
+ <SliderPrimitive.Thumb
104
+ key={i}
105
+ className={cn(thumbVariants({ size }), "relative")}
106
+ >
107
+ {showTooltip && hovering && (
108
+ <span
109
+ className={cn(
110
+ "absolute bottom-full left-1/2 -translate-x-1/2 mb-2",
111
+ "rounded-md bg-neutral-900 dark:bg-base-700 px-2 py-1",
112
+ "text-xs text-white whitespace-nowrap pointer-events-none",
113
+ )}
114
+ >
115
+ {val}
116
+ </span>
117
+ )}
118
+ </SliderPrimitive.Thumb>
119
+ ))}
120
+ </SliderPrimitive.Root>
121
+ );
122
+ },
123
+ );
124
+
125
+ Slider.displayName = "Slider";
126
+
127
+ export { Slider, trackVariants, thumbVariants, type SliderProps };