@srcroot/ui 0.0.2 → 0.0.3

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.
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { cn } from "@/lib/utils"
3
3
 
4
- interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
4
+ interface SliderProps {
5
5
  value?: number[]
6
6
  onValueChange?: (value: number[]) => void
7
7
  defaultValue?: number[]
@@ -9,17 +9,21 @@ interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChan
9
9
  max?: number
10
10
  step?: number
11
11
  disabled?: boolean
12
+ minStepsBetweenThumbs?: number
13
+ className?: string
12
14
  }
13
15
 
14
16
  /**
15
- * Slider component with keyboard support
17
+ * Slider component with support for multiple thumbs (range selection)
16
18
  *
17
19
  * @example
18
- * const [value, setValue] = useState([50])
19
- * <Slider value={value} onValueChange={setValue} max={100} step={1} />
20
+ * // Single value
21
+ * <Slider value={[50]} onValueChange={setValue} max={100} step={1} />
22
+ *
23
+ * @example
24
+ * // Range
25
+ * <Slider value={[25, 75]} onValueChange={setValue} max={100} step={1} />
20
26
  */
21
- export { Slider }
22
-
23
27
  const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
24
28
  ({
25
29
  className,
@@ -30,19 +34,22 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
30
34
  max = 100,
31
35
  step = 1,
32
36
  disabled,
37
+ minStepsBetweenThumbs = 0,
33
38
  ...props
34
39
  }, ref) => {
35
40
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
36
41
  const trackRef = React.useRef<HTMLDivElement>(null)
37
- const isDragging = React.useRef(false)
42
+ const activeThumbIndex = React.useRef<number | null>(null)
38
43
 
39
44
  const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
40
- const setValue = onValueChange || setUncontrolledValue
41
- const currentValue = value[0] || 0
42
-
43
- const percentage = ((currentValue - min) / (max - min)) * 100
45
+ const setValue = (newValue: number[]) => {
46
+ if (controlledValue === undefined) {
47
+ setUncontrolledValue(newValue)
48
+ }
49
+ onValueChange?.(newValue)
50
+ }
44
51
 
45
- const updateValue = (clientX: number) => {
52
+ const updateValue = (clientX: number, thumbIndex: number) => {
46
53
  if (!trackRef.current) return
47
54
 
48
55
  const rect = trackRef.current.getBoundingClientRect()
@@ -51,129 +58,137 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
51
58
  const steppedValue = Math.round(rawValue / step) * step
52
59
  const clampedValue = Math.min(Math.max(steppedValue, min), max)
53
60
 
54
- setValue([clampedValue])
61
+ const newValue = [...value]
62
+ newValue[thumbIndex] = clampedValue
63
+
64
+ // Sort logic to prevent crossover if preferred, or just allow it but sorted
65
+ newValue.sort((a, b) => a - b)
66
+
67
+ setValue(newValue)
68
+ }
69
+
70
+ // Find closest thumb to a point
71
+ const getClosestThumbIndex = (clientX: number) => {
72
+ if (!trackRef.current) return 0
73
+ const rect = trackRef.current.getBoundingClientRect()
74
+ const percentage = (clientX - rect.left) / rect.width
75
+ const clickedValue = min + percentage * (max - min)
76
+
77
+ let closestIndex = 0
78
+ let minDiff = Infinity
79
+
80
+ value.forEach((val, index) => {
81
+ const diff = Math.abs(val - clickedValue)
82
+ if (diff < minDiff) {
83
+ minDiff = diff
84
+ closestIndex = index
85
+ }
86
+ })
87
+
88
+ return closestIndex
55
89
  }
56
90
 
57
91
  const handleMouseDown = (e: React.MouseEvent) => {
58
92
  if (disabled) return
59
- isDragging.current = true
60
- updateValue(e.clientX)
93
+ const thumbIndex = getClosestThumbIndex(e.clientX)
94
+ activeThumbIndex.current = thumbIndex
95
+
96
+ updateValue(e.clientX, thumbIndex)
61
97
 
62
98
  document.addEventListener('mousemove', handleMouseMove)
63
99
  document.addEventListener('mouseup', handleMouseUp)
64
100
  }
65
101
 
66
102
  const handleMouseMove = (e: MouseEvent) => {
67
- if (!isDragging.current) return
68
- updateValue(e.clientX)
103
+ if (activeThumbIndex.current === null) return
104
+ updateValue(e.clientX, activeThumbIndex.current)
69
105
  }
70
106
 
71
107
  const handleMouseUp = () => {
72
- isDragging.current = false
108
+ activeThumbIndex.current = null
73
109
  document.removeEventListener('mousemove', handleMouseMove)
74
110
  document.removeEventListener('mouseup', handleMouseUp)
75
111
  }
76
112
 
77
- // Touch support
78
113
  const handleTouchStart = (e: React.TouchEvent) => {
79
114
  if (disabled) return
80
- isDragging.current = true
81
- updateValue(e.touches[0].clientX)
115
+ const thumbIndex = getClosestThumbIndex(e.touches[0].clientX)
116
+ activeThumbIndex.current = thumbIndex
117
+
118
+ updateValue(e.touches[0].clientX, thumbIndex)
82
119
 
83
120
  document.addEventListener('touchmove', handleTouchMove)
84
121
  document.addEventListener('touchend', handleTouchEnd)
85
122
  }
86
123
 
87
124
  const handleTouchMove = (e: TouchEvent) => {
88
- if (!isDragging.current) return
89
- updateValue(e.touches[0].clientX)
90
- e.preventDefault() // Prevent scrolling while dragging
125
+ if (activeThumbIndex.current === null) return
126
+ updateValue(e.touches[0].clientX, activeThumbIndex.current)
127
+ e.preventDefault()
91
128
  }
92
129
 
93
130
  const handleTouchEnd = () => {
94
- isDragging.current = false
131
+ activeThumbIndex.current = null
95
132
  document.removeEventListener('touchmove', handleTouchMove)
96
133
  document.removeEventListener('touchend', handleTouchEnd)
97
134
  }
98
135
 
99
-
100
- const handleKeyDown = (e: React.KeyboardEvent) => {
101
- if (disabled) return
102
-
103
- let newValue = currentValue
104
-
105
- switch (e.key) {
106
- case "ArrowRight":
107
- case "ArrowUp":
108
- newValue = Math.min(currentValue + step, max)
109
- break
110
- case "ArrowLeft":
111
- case "ArrowDown":
112
- newValue = Math.max(currentValue - step, min)
113
- break
114
- case "Home":
115
- newValue = min
116
- break
117
- case "End":
118
- newValue = max
119
- break
120
- default:
121
- return
122
- }
123
-
124
- if (newValue !== currentValue) {
125
- e.preventDefault()
126
- setValue([newValue])
127
- }
128
- }
129
-
130
- // Cleanup on unmount
131
- React.useEffect(() => {
132
- return () => {
133
- document.removeEventListener('mousemove', handleMouseMove)
134
- document.removeEventListener('mouseup', handleMouseUp)
135
- document.removeEventListener('touchmove', handleTouchMove)
136
- document.removeEventListener('touchend', handleTouchEnd)
137
- }
138
- }, [])
139
-
140
136
  return (
141
137
  <div
142
138
  ref={ref}
143
- role="slider"
144
- aria-valuenow={currentValue}
145
- aria-valuemin={min}
146
- aria-valuemax={max}
147
- aria-disabled={disabled}
148
- tabIndex={disabled ? -1 : 0}
139
+ role="group"
149
140
  className={cn(
150
- "relative flex w-full touch-none select-none items-center py-4 cursor-pointer", // Added vertical padding for easier touch area
141
+ "relative flex w-full touch-none select-none items-center py-4 cursor-pointer",
151
142
  disabled && "opacity-50 cursor-not-allowed",
152
143
  className
153
144
  )}
154
- onKeyDown={handleKeyDown}
155
145
  onMouseDown={handleMouseDown}
156
146
  onTouchStart={handleTouchStart}
157
147
  {...props}
158
148
  >
159
149
  <div
160
150
  ref={trackRef}
161
- className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20"
151
+ className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary"
162
152
  >
163
- <div
164
- className="absolute h-full bg-primary transition-all duration-75 ease-out" // Added transition but very fast to feel responsive
165
- style={{ width: `${percentage}%` }}
166
- />
167
- </div>
168
- <div
169
- className={cn(
170
- "absolute block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
171
- "hover:bg-accent hover:border-primary" // Visual feedback
153
+ {/* Render active tracks between ranges if multiple, or from 0 if single */}
154
+ {value.length > 1 ? (
155
+ <div
156
+ className="absolute h-full bg-primary transition-all duration-75"
157
+ style={{
158
+ left: `${((value[0] - min) / (max - min)) * 100}%`,
159
+ right: `${100 - ((value[value.length - 1] - min) / (max - min)) * 100}%`,
160
+ }}
161
+ />
162
+ ) : (
163
+ <div
164
+ className="absolute h-full bg-primary transition-all duration-75"
165
+ style={{ width: `${((value[0] - min) / (max - min)) * 100}%` }}
166
+ />
172
167
  )}
173
- style={{ left: `calc(${percentage}% - 8px)` }}
174
- />
168
+ </div>
169
+
170
+ {value.map((val, index) => {
171
+ const percentage = ((val - min) / (max - min)) * 100
172
+ return (
173
+ <div
174
+ key={index}
175
+ role="slider"
176
+ aria-valuemin={min}
177
+ aria-valuemax={max}
178
+ aria-valuenow={val}
179
+ tabIndex={disabled ? -1 : 0}
180
+ className={cn(
181
+ "absolute block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
182
+ "hover:bg-accent hover:border-primary"
183
+ )}
184
+ style={{ left: `calc(${percentage}% - 8px)` }}
185
+ />
186
+ )
187
+ })}
175
188
  </div>
176
189
  )
177
190
  }
178
191
  )
179
192
  Slider.displayName = "Slider"
193
+
194
+ export { Slider }
package/registry/text.tsx CHANGED
@@ -26,13 +26,14 @@ const textVariants = cva("", {
26
26
 
27
27
  type TextVariants = VariantProps<typeof textVariants>
28
28
 
29
- interface TextBaseProps extends TextVariants {
29
+ interface TextProps extends TextVariants {
30
30
  className?: string
31
31
  children?: React.ReactNode
32
+ as?: "p" | "span" | "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "label"
32
33
  }
33
34
 
34
35
  /**
35
- * Polymorphic Text component for typography
36
+ * Text component for typography
36
37
  *
37
38
  * @example
38
39
  * // As a heading
@@ -44,22 +45,12 @@ interface TextBaseProps extends TextVariants {
44
45
  * // Muted text
45
46
  * <Text variant="muted">Secondary information</Text>
46
47
  */
47
- const Text = React.forwardRef(
48
- <T extends React.ElementType = "p">(
49
- {
50
- as,
51
- className,
52
- variant,
53
- ...props
54
- }: TextBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof TextBaseProps | "as">,
55
- ref: React.ForwardedRef<React.ElementRef<T>>
56
- ) => {
57
- const Comp = as || "p"
58
-
48
+ const Text = React.forwardRef<HTMLElement, TextProps>(
49
+ ({ as: Component = "p", className, variant, ...props }, ref) => {
59
50
  return (
60
- <Comp
51
+ <Component
61
52
  ref={ref as any}
62
- className={cn(textVariants({ variant, className }))}
53
+ className={cn(textVariants({ variant }), className)}
63
54
  {...props}
64
55
  />
65
56
  )