@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.
- package/package.json +1 -1
- package/registry/accordion.tsx +6 -2
- package/registry/badge.tsx +9 -25
- package/registry/breadcrumb.tsx +1 -1
- package/registry/button-group.tsx +9 -29
- package/registry/button.tsx +20 -46
- package/registry/card.tsx +21 -47
- package/registry/combobox.tsx +0 -3
- package/registry/command.tsx +6 -4
- package/registry/container.tsx +9 -25
- package/registry/drawer.tsx +36 -12
- package/registry/dropdown-menu.tsx +92 -44
- package/registry/hover-card.tsx +1 -1
- package/registry/image.tsx +2 -2
- package/registry/menubar.tsx +1 -1
- package/registry/resizable.tsx +164 -63
- package/registry/scroll-area.tsx +66 -7
- package/registry/sheet.tsx +62 -18
- package/registry/sidebar.tsx +7 -0
- package/registry/slider.tsx +101 -86
- package/registry/text.tsx +7 -16
package/registry/sheet.tsx
CHANGED
|
@@ -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
|
|
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 (!
|
|
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=
|
|
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(
|
|
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}
|
package/registry/sidebar.tsx
CHANGED
|
@@ -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
|
)}
|
package/registry/slider.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import { cn } from "@/lib/utils"
|
|
3
3
|
|
|
4
|
-
interface SliderProps
|
|
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
|
|
17
|
+
* Slider component with support for multiple thumbs (range selection)
|
|
16
18
|
*
|
|
17
19
|
* @example
|
|
18
|
-
*
|
|
19
|
-
* <Slider value={
|
|
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
|
|
42
|
+
const activeThumbIndex = React.useRef<number | null>(null)
|
|
38
43
|
|
|
39
44
|
const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
|
|
40
|
-
const setValue =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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 (
|
|
89
|
-
updateValue(e.touches[0].clientX)
|
|
90
|
-
e.preventDefault()
|
|
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
|
-
|
|
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="
|
|
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",
|
|
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-
|
|
151
|
+
className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary"
|
|
162
152
|
>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
)
|