@srcroot/ui 0.0.2 → 0.0.4

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.
@@ -18,20 +18,7 @@ interface SheetProps {
18
18
  }
19
19
 
20
20
  /**
21
- * Sheet (slide-in panel) component
22
- *
23
- * @example
24
- * <Sheet>
25
- * <SheetTrigger asChild>
26
- * <Button>Open Sheet</Button>
27
- * </SheetTrigger>
28
- * <SheetContent side="right">
29
- * <SheetHeader>
30
- * <SheetTitle>Title</SheetTitle>
31
- * <SheetDescription>Description</SheetDescription>
32
- * </SheetHeader>
33
- * </SheetContent>
34
- * </Sheet>
21
+ * Sheet (slide-in panel) component with smooth animations
35
22
  */
36
23
  function Sheet({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: SheetProps) {
37
24
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
@@ -77,7 +64,7 @@ const SheetTrigger = React.forwardRef<HTMLButtonElement, SheetTriggerProps>(
77
64
  SheetTrigger.displayName = "SheetTrigger"
78
65
 
79
66
  const sheetVariants = cva(
80
- "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out",
67
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg",
81
68
  {
82
69
  variants: {
83
70
  side: {
@@ -93,6 +80,26 @@ const sheetVariants = cva(
93
80
  }
94
81
  )
95
82
 
83
+ // Animation classes for each side
84
+ const animationClasses = {
85
+ top: {
86
+ open: "translate-y-0",
87
+ closed: "-translate-y-full",
88
+ },
89
+ bottom: {
90
+ open: "translate-y-0",
91
+ closed: "translate-y-full",
92
+ },
93
+ left: {
94
+ open: "translate-x-0",
95
+ closed: "-translate-x-full",
96
+ },
97
+ right: {
98
+ open: "translate-x-0",
99
+ closed: "translate-x-full",
100
+ },
101
+ }
102
+
96
103
  interface SheetContentProps
97
104
  extends React.HTMLAttributes<HTMLDivElement>,
98
105
  VariantProps<typeof sheetVariants> { }
@@ -102,6 +109,29 @@ const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
102
109
  const context = React.useContext(SheetContext)
103
110
  if (!context) throw new Error("SheetContent must be used within Sheet")
104
111
 
112
+ const [isVisible, setIsVisible] = React.useState(false)
113
+ const [isAnimating, setIsAnimating] = React.useState(false)
114
+
115
+ React.useEffect(() => {
116
+ if (context.open) {
117
+ // First make visible (off-screen)
118
+ setIsVisible(true)
119
+ // Use a small timeout to ensure the browser has painted the initial state
120
+ const timer = setTimeout(() => {
121
+ setIsAnimating(true)
122
+ }, 10)
123
+ return () => clearTimeout(timer)
124
+ } else {
125
+ // Start close animation
126
+ setIsAnimating(false)
127
+ // Wait for animation to complete before hiding
128
+ const timer = setTimeout(() => {
129
+ setIsVisible(false)
130
+ }, 300)
131
+ return () => clearTimeout(timer)
132
+ }
133
+ }, [context.open])
134
+
105
135
  React.useEffect(() => {
106
136
  const handleEscape = (e: KeyboardEvent) => {
107
137
  if (e.key === "Escape") {
@@ -120,19 +150,33 @@ const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
120
150
  }
121
151
  }, [context.open, context])
122
152
 
123
- if (!context.open) return null
153
+ if (!isVisible) return null
154
+
155
+ const sideKey = side || "right"
124
156
 
125
157
  return (
126
158
  <>
159
+ {/* Overlay with fade animation */}
127
160
  <div
128
- className="fixed inset-0 z-50 bg-black/80"
161
+ className={cn(
162
+ "fixed inset-0 z-50 bg-black/80 transition-opacity duration-300",
163
+ isAnimating ? "opacity-100" : "opacity-0"
164
+ )}
129
165
  onClick={() => context.onOpenChange(false)}
130
166
  />
167
+ {/* Sheet content with slide animation */}
131
168
  <div
132
169
  ref={ref}
133
170
  role="dialog"
134
171
  aria-modal="true"
135
- className={cn(sheetVariants({ side }), className)}
172
+ className={cn(
173
+ sheetVariants({ side }),
174
+ "transition-transform duration-300 ease-out",
175
+ isAnimating
176
+ ? animationClasses[sideKey].open
177
+ : animationClasses[sideKey].closed,
178
+ className
179
+ )}
136
180
  {...props}
137
181
  >
138
182
  {children}
@@ -281,6 +281,13 @@ const SidebarInset = React.forwardRef<
281
281
  ref={ref}
282
282
  className={cn(
283
283
  "relative flex min-h-svh flex-1 flex-col bg-background",
284
+ // Width calculation based on sidebar state
285
+ "w-full md:w-[calc(100%-var(--sidebar-width))]",
286
+ "md:peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon))]",
287
+ "md:peer-data-[collapsible=offcanvas]:peer-data-[state=collapsed]:w-full",
288
+ // Transition for smooth resize
289
+ "transition-[width] duration-200 ease-linear",
290
+ // Inset variant styles
284
291
  "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
285
292
  className
286
293
  )}
@@ -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
  )