@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
|
@@ -4,6 +4,7 @@ import { cn } from "@/lib/utils"
|
|
|
4
4
|
interface DropdownMenuContextValue {
|
|
5
5
|
open: boolean
|
|
6
6
|
onOpenChange: (open: boolean) => void
|
|
7
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null)
|
|
@@ -16,7 +17,7 @@ interface DropdownMenuProps {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* DropdownMenu component with keyboard navigation
|
|
20
|
+
* DropdownMenu component with keyboard navigation and proper positioning
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
22
23
|
* <DropdownMenu>
|
|
@@ -33,12 +34,13 @@ interface DropdownMenuProps {
|
|
|
33
34
|
*/
|
|
34
35
|
function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DropdownMenuProps) {
|
|
35
36
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
37
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
36
38
|
|
|
37
39
|
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
38
40
|
const setOpen = onOpenChange || setUncontrolledOpen
|
|
39
41
|
|
|
40
42
|
return (
|
|
41
|
-
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
43
|
+
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
|
|
42
44
|
<div className="relative inline-block text-left">
|
|
43
45
|
{children}
|
|
44
46
|
</div>
|
|
@@ -60,18 +62,25 @@ const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTrig
|
|
|
60
62
|
context.onOpenChange(!context.open)
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
// Combine refs
|
|
66
|
+
const combinedRef = (node: HTMLButtonElement | null) => {
|
|
67
|
+
(context.triggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = node
|
|
68
|
+
if (typeof ref === 'function') ref(node)
|
|
69
|
+
else if (ref) ref.current = node
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
if (asChild && React.isValidElement(children)) {
|
|
64
73
|
return React.cloneElement(children as React.ReactElement<any>, {
|
|
65
74
|
onClick: handleClick,
|
|
66
75
|
"aria-expanded": context.open,
|
|
67
76
|
"aria-haspopup": "menu",
|
|
68
|
-
ref,
|
|
77
|
+
ref: combinedRef,
|
|
69
78
|
})
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
return (
|
|
73
82
|
<button
|
|
74
|
-
ref={
|
|
83
|
+
ref={combinedRef}
|
|
75
84
|
aria-expanded={context.open}
|
|
76
85
|
aria-haspopup="menu"
|
|
77
86
|
onClick={handleClick}
|
|
@@ -84,53 +93,94 @@ const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTrig
|
|
|
84
93
|
)
|
|
85
94
|
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
97
|
+
/** Alignment relative to trigger: 'start' | 'center' | 'end' */
|
|
98
|
+
align?: 'start' | 'center' | 'end'
|
|
99
|
+
/** Side of trigger to open: 'bottom' | 'top' */
|
|
100
|
+
side?: 'bottom' | 'top'
|
|
101
|
+
/** Offset from trigger in pixels */
|
|
102
|
+
sideOffset?: number
|
|
103
|
+
}
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
|
106
|
+
({ className, align = 'start', side = 'bottom', sideOffset = 4, ...props }, ref) => {
|
|
107
|
+
const context = React.useContext(DropdownMenuContext)
|
|
108
|
+
if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
|
|
109
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
113
|
+
if (context.open) {
|
|
114
|
+
const target = e.target as Node
|
|
115
|
+
const content = contentRef.current
|
|
116
|
+
const trigger = context.triggerRef.current
|
|
117
|
+
|
|
118
|
+
// Don't close if clicking inside content or trigger
|
|
119
|
+
if (content?.contains(target) || trigger?.contains(target)) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
context.onOpenChange(false)
|
|
123
|
+
}
|
|
98
124
|
}
|
|
99
|
-
}
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
127
|
+
if (e.key === "Escape" && context.open) {
|
|
128
|
+
context.onOpenChange(false)
|
|
129
|
+
}
|
|
104
130
|
}
|
|
105
|
-
}
|
|
106
131
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
document.addEventListener("click", handleClickOutside)
|
|
134
|
+
}, 0)
|
|
135
|
+
document.addEventListener("keydown", handleEscape)
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
clearTimeout(timer)
|
|
139
|
+
document.removeEventListener("click", handleClickOutside)
|
|
140
|
+
document.removeEventListener("keydown", handleEscape)
|
|
141
|
+
}
|
|
142
|
+
}, [context.open, context])
|
|
143
|
+
|
|
144
|
+
if (!context.open) return null
|
|
111
145
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
// Calculate alignment classes
|
|
147
|
+
const alignmentClasses = {
|
|
148
|
+
start: 'left-0',
|
|
149
|
+
center: 'left-1/2 -translate-x-1/2',
|
|
150
|
+
end: 'right-0',
|
|
116
151
|
}
|
|
117
|
-
}, [context.open, context])
|
|
118
152
|
|
|
119
|
-
|
|
153
|
+
// Calculate side classes
|
|
154
|
+
const sideClasses = {
|
|
155
|
+
bottom: `top-full mt-${sideOffset}`,
|
|
156
|
+
top: `bottom-full mb-${sideOffset}`,
|
|
157
|
+
}
|
|
120
158
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
ref={(node) => {
|
|
162
|
+
(contentRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
163
|
+
if (typeof ref === 'function') ref(node)
|
|
164
|
+
else if (ref) ref.current = node
|
|
165
|
+
}}
|
|
166
|
+
role="menu"
|
|
167
|
+
aria-orientation="vertical"
|
|
168
|
+
className={cn(
|
|
169
|
+
"absolute z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
170
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
171
|
+
alignmentClasses[align],
|
|
172
|
+
side === 'bottom' ? 'top-full' : 'bottom-full',
|
|
173
|
+
className
|
|
174
|
+
)}
|
|
175
|
+
style={{
|
|
176
|
+
marginTop: side === 'bottom' ? sideOffset : undefined,
|
|
177
|
+
marginBottom: side === 'top' ? sideOffset : undefined,
|
|
178
|
+
}}
|
|
179
|
+
{...props}
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
)
|
|
134
184
|
DropdownMenuContent.displayName = "DropdownMenuContent"
|
|
135
185
|
|
|
136
186
|
const DropdownMenuItem = React.forwardRef<
|
|
@@ -188,8 +238,6 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|
|
188
238
|
HTMLDivElement,
|
|
189
239
|
React.HTMLAttributes<HTMLDivElement> & { checked?: boolean; disabled?: boolean }
|
|
190
240
|
>(({ className, children, checked, disabled, onClick, ...props }, ref) => {
|
|
191
|
-
const context = React.useContext(DropdownMenuContext)
|
|
192
|
-
|
|
193
241
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
194
242
|
if (disabled) return
|
|
195
243
|
onClick?.(e)
|
package/registry/hover-card.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"
|
|
|
6
6
|
// HoverCard Context
|
|
7
7
|
interface HoverCardContextValue {
|
|
8
8
|
open: boolean
|
|
9
|
-
triggerRef: React.RefObject<HTMLDivElement>
|
|
9
|
+
triggerRef: React.RefObject<HTMLDivElement | null>
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const HoverCardContext = React.createContext<HoverCardContextValue | null>(null)
|
package/registry/image.tsx
CHANGED
|
@@ -69,8 +69,8 @@ const Image = React.forwardRef<HTMLImageElement, ImageProps>(
|
|
|
69
69
|
}, [src])
|
|
70
70
|
|
|
71
71
|
const containerStyle: React.CSSProperties = aspectRatio
|
|
72
|
-
? { paddingBottom: `${100 / aspectRatio}%`, ...style }
|
|
73
|
-
: style
|
|
72
|
+
? { paddingBottom: `${100 / aspectRatio}%`, ...(style || {}) }
|
|
73
|
+
: (style || {})
|
|
74
74
|
|
|
75
75
|
// Render fallback
|
|
76
76
|
if (status === "error" && fallback) {
|
package/registry/menubar.tsx
CHANGED
|
@@ -22,7 +22,7 @@ function useMenubar() {
|
|
|
22
22
|
// MenubarMenu Context
|
|
23
23
|
interface MenubarMenuContextValue {
|
|
24
24
|
menuId: string
|
|
25
|
-
triggerRef: React.RefObject<HTMLButtonElement>
|
|
25
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const MenubarMenuContext = React.createContext<MenubarMenuContextValue | null>(null)
|
package/registry/resizable.tsx
CHANGED
|
@@ -5,10 +5,12 @@ import { cn } from "@/lib/utils"
|
|
|
5
5
|
|
|
6
6
|
// Resizable Context
|
|
7
7
|
interface ResizablePanelGroupContextValue {
|
|
8
|
+
groupId: string
|
|
8
9
|
direction: "horizontal" | "vertical"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
registerPanel: (defaultSize: number, minSize: number, maxSize: number) => number
|
|
11
|
+
getSize: (index: number) => number
|
|
12
|
+
getTotalSize: () => number
|
|
13
|
+
onResize: (handleIndex: number, delta: number) => void
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
|
|
@@ -27,29 +29,115 @@ interface ResizablePanelGroupProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
27
29
|
onLayout?: (sizes: number[]) => void
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
interface PanelConfig {
|
|
33
|
+
defaultSize: number
|
|
34
|
+
minSize: number
|
|
35
|
+
maxSize: number
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
|
|
31
39
|
({ className, direction = "horizontal", children, onLayout, ...props }, ref) => {
|
|
32
|
-
const
|
|
40
|
+
const groupId = React.useId()
|
|
41
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
42
|
+
const [, forceUpdate] = React.useReducer(x => x + 1, 0)
|
|
43
|
+
|
|
44
|
+
// Store panel configs and sizes in refs for stable access
|
|
45
|
+
const panelConfigsRef = React.useRef<PanelConfig[]>([])
|
|
46
|
+
const sizesRef = React.useRef<number[]>([])
|
|
33
47
|
const panelCountRef = React.useRef(0)
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
// Reset panel count at start of each render
|
|
50
|
+
panelCountRef.current = 0
|
|
51
|
+
|
|
52
|
+
const registerPanel = React.useCallback((defaultSize: number, minSize: number, maxSize: number) => {
|
|
53
|
+
const index = panelCountRef.current++
|
|
54
|
+
|
|
55
|
+
// Only initialize if not already set
|
|
56
|
+
if (sizesRef.current[index] === undefined) {
|
|
57
|
+
sizesRef.current[index] = defaultSize
|
|
58
|
+
panelConfigsRef.current[index] = { defaultSize, minSize, maxSize }
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
return index
|
|
39
62
|
}, [])
|
|
40
63
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
const getSize = React.useCallback((index: number) => {
|
|
65
|
+
return sizesRef.current[index] ?? 50
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const getTotalSize = React.useCallback(() => {
|
|
69
|
+
return sizesRef.current.reduce((sum, s) => sum + s, 0)
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
const onResize = React.useCallback((handleIndex: number, delta: number) => {
|
|
73
|
+
const container = containerRef.current
|
|
74
|
+
if (!container) return
|
|
75
|
+
|
|
76
|
+
const containerSize = direction === "horizontal"
|
|
77
|
+
? container.offsetWidth
|
|
78
|
+
: container.offsetHeight
|
|
79
|
+
|
|
80
|
+
if (containerSize === 0) return
|
|
81
|
+
|
|
82
|
+
const deltaPercent = (delta / containerSize) * 100
|
|
83
|
+
|
|
84
|
+
const leftIndex = handleIndex
|
|
85
|
+
const rightIndex = handleIndex + 1
|
|
86
|
+
const sizes = sizesRef.current
|
|
87
|
+
const configs = panelConfigsRef.current
|
|
88
|
+
|
|
89
|
+
if (leftIndex >= sizes.length || rightIndex >= sizes.length) return
|
|
90
|
+
|
|
91
|
+
const leftSize = sizes[leftIndex]
|
|
92
|
+
const rightSize = sizes[rightIndex]
|
|
93
|
+
const totalSize = leftSize + rightSize
|
|
94
|
+
|
|
95
|
+
let newLeftSize = leftSize + deltaPercent
|
|
96
|
+
let newRightSize = rightSize - deltaPercent
|
|
97
|
+
|
|
98
|
+
const leftMin = configs[leftIndex]?.minSize ?? 10
|
|
99
|
+
const leftMax = configs[leftIndex]?.maxSize ?? 90
|
|
100
|
+
const rightMin = configs[rightIndex]?.minSize ?? 10
|
|
101
|
+
const rightMax = configs[rightIndex]?.maxSize ?? 90
|
|
102
|
+
|
|
103
|
+
// Apply constraints
|
|
104
|
+
if (newLeftSize < leftMin) {
|
|
105
|
+
newLeftSize = leftMin
|
|
106
|
+
newRightSize = totalSize - leftMin
|
|
45
107
|
}
|
|
46
|
-
|
|
108
|
+
if (newRightSize < rightMin) {
|
|
109
|
+
newRightSize = rightMin
|
|
110
|
+
newLeftSize = totalSize - rightMin
|
|
111
|
+
}
|
|
112
|
+
if (newLeftSize > leftMax) {
|
|
113
|
+
newLeftSize = leftMax
|
|
114
|
+
newRightSize = totalSize - leftMax
|
|
115
|
+
}
|
|
116
|
+
if (newRightSize > rightMax) {
|
|
117
|
+
newRightSize = rightMax
|
|
118
|
+
newLeftSize = totalSize - rightMax
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
sizesRef.current[leftIndex] = newLeftSize
|
|
122
|
+
sizesRef.current[rightIndex] = newRightSize
|
|
123
|
+
|
|
124
|
+
// Force re-render to update panel sizes
|
|
125
|
+
forceUpdate()
|
|
126
|
+
|
|
127
|
+
// Notify layout changes
|
|
128
|
+
onLayout?.([...sizesRef.current])
|
|
129
|
+
}, [direction, onLayout])
|
|
47
130
|
|
|
48
131
|
return (
|
|
49
|
-
<ResizablePanelGroupContext.Provider value={{ direction,
|
|
132
|
+
<ResizablePanelGroupContext.Provider value={{ groupId, direction, registerPanel, getSize, getTotalSize, onResize }}>
|
|
50
133
|
<div
|
|
51
|
-
ref={
|
|
134
|
+
ref={(node) => {
|
|
135
|
+
containerRef.current = node
|
|
136
|
+
if (typeof ref === "function") ref(node)
|
|
137
|
+
else if (ref) ref.current = node
|
|
138
|
+
}}
|
|
52
139
|
data-panel-group
|
|
140
|
+
data-panel-group-id={groupId}
|
|
53
141
|
data-direction={direction}
|
|
54
142
|
className={cn(
|
|
55
143
|
"flex h-full w-full",
|
|
@@ -75,33 +163,30 @@ interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
75
163
|
|
|
76
164
|
const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
|
|
77
165
|
({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
|
|
78
|
-
const { direction,
|
|
79
|
-
const indexRef = React.useRef<number
|
|
80
|
-
|
|
81
|
-
React.useEffect(() => {
|
|
82
|
-
if (indexRef.current === null) {
|
|
83
|
-
indexRef.current = registerPanel()
|
|
84
|
-
setSizes(prev => {
|
|
85
|
-
const newSizes = [...prev]
|
|
86
|
-
newSizes[indexRef.current!] = defaultSize
|
|
87
|
-
return newSizes
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
}, [registerPanel, setSizes, defaultSize])
|
|
166
|
+
const { groupId, direction, registerPanel, getSize, getTotalSize } = useResizablePanelGroup()
|
|
167
|
+
const indexRef = React.useRef<number>(-1)
|
|
91
168
|
|
|
92
|
-
|
|
169
|
+
// Register panel on first render only
|
|
170
|
+
if (indexRef.current === -1) {
|
|
171
|
+
indexRef.current = registerPanel(defaultSize, minSize, maxSize)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const size = getSize(indexRef.current)
|
|
175
|
+
const totalSize = getTotalSize()
|
|
176
|
+
// Convert to actual percentage of total
|
|
177
|
+
// const actualPercent = totalSize > 0 ? (size / totalSize) * 100 : size
|
|
93
178
|
|
|
94
179
|
return (
|
|
95
180
|
<div
|
|
96
181
|
ref={ref}
|
|
97
182
|
data-panel
|
|
183
|
+
data-panel-group-id={groupId}
|
|
98
184
|
data-panel-index={indexRef.current}
|
|
99
185
|
className={cn("overflow-hidden", className)}
|
|
100
186
|
style={{
|
|
101
187
|
...style,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
flexGrow: 0,
|
|
188
|
+
// Use flex-grow with the size as the ratio for smoother resizing
|
|
189
|
+
flex: `${size} 1 0`,
|
|
105
190
|
}}
|
|
106
191
|
{...props}
|
|
107
192
|
>
|
|
@@ -119,62 +204,78 @@ interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
119
204
|
|
|
120
205
|
const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
|
|
121
206
|
({ className, withHandle = false, ...props }, ref) => {
|
|
122
|
-
const { direction,
|
|
207
|
+
const { direction, onResize } = useResizablePanelGroup()
|
|
123
208
|
const [isDragging, setIsDragging] = React.useState(false)
|
|
209
|
+
const handleIndexRef = React.useRef<number>(-1)
|
|
210
|
+
const lastPosRef = React.useRef<number>(0)
|
|
124
211
|
const handleRef = React.useRef<HTMLDivElement>(null)
|
|
125
212
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
213
|
+
// Determine handle index from DOM position
|
|
214
|
+
const getHandleIndex = React.useCallback(() => {
|
|
215
|
+
const handle = handleRef.current
|
|
216
|
+
if (!handle) return 0
|
|
129
217
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const startSizes = [...sizes]
|
|
133
|
-
const container = handleRef.current?.parentElement
|
|
218
|
+
const parent = handle.parentElement
|
|
219
|
+
if (!parent) return 0
|
|
134
220
|
|
|
135
|
-
|
|
221
|
+
let handleCount = 0
|
|
222
|
+
for (const child of Array.from(parent.children)) {
|
|
223
|
+
if (child === handle) break
|
|
224
|
+
if (child.hasAttribute('data-panel-resize-handle')) {
|
|
225
|
+
handleCount++
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return handleCount
|
|
229
|
+
}, [])
|
|
230
|
+
|
|
231
|
+
const handleMouseDown = React.useCallback((e: React.MouseEvent) => {
|
|
232
|
+
e.preventDefault()
|
|
233
|
+
e.stopPropagation()
|
|
234
|
+
setIsDragging(true)
|
|
136
235
|
|
|
137
|
-
const
|
|
138
|
-
const
|
|
236
|
+
const handleIndex = getHandleIndex()
|
|
237
|
+
const startPos = direction === "horizontal" ? e.clientX : e.clientY
|
|
238
|
+
lastPosRef.current = startPos
|
|
139
239
|
|
|
140
240
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Adjust first two panels (simple implementation)
|
|
149
|
-
if (newSizes.length >= 2) {
|
|
150
|
-
newSizes[0] = Math.max(10, Math.min(90, startSizes[0] + deltaPercent))
|
|
151
|
-
newSizes[1] = Math.max(10, Math.min(90, startSizes[1] - deltaPercent))
|
|
152
|
-
}
|
|
153
|
-
return newSizes
|
|
154
|
-
})
|
|
241
|
+
const currentPos = direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
|
|
242
|
+
const delta = currentPos - lastPosRef.current
|
|
243
|
+
lastPosRef.current = currentPos
|
|
244
|
+
|
|
245
|
+
if (delta !== 0) {
|
|
246
|
+
onResize(handleIndex, delta)
|
|
247
|
+
}
|
|
155
248
|
}
|
|
156
249
|
|
|
157
250
|
const handleMouseUp = () => {
|
|
158
251
|
setIsDragging(false)
|
|
159
252
|
document.removeEventListener("mousemove", handleMouseMove)
|
|
160
253
|
document.removeEventListener("mouseup", handleMouseUp)
|
|
254
|
+
document.body.style.cursor = ""
|
|
255
|
+
document.body.style.userSelect = ""
|
|
161
256
|
}
|
|
162
257
|
|
|
258
|
+
document.body.style.cursor = direction === "horizontal" ? "col-resize" : "row-resize"
|
|
259
|
+
document.body.style.userSelect = "none"
|
|
163
260
|
document.addEventListener("mousemove", handleMouseMove)
|
|
164
261
|
document.addEventListener("mouseup", handleMouseUp)
|
|
165
|
-
}
|
|
262
|
+
}, [direction, onResize, getHandleIndex])
|
|
166
263
|
|
|
167
264
|
return (
|
|
168
265
|
<div
|
|
169
|
-
ref={
|
|
266
|
+
ref={(node) => {
|
|
267
|
+
handleRef.current = node
|
|
268
|
+
if (typeof ref === "function") ref(node)
|
|
269
|
+
else if (ref) ref.current = node
|
|
270
|
+
}}
|
|
170
271
|
data-panel-resize-handle
|
|
272
|
+
data-dragging={isDragging}
|
|
171
273
|
className={cn(
|
|
172
|
-
"relative flex items-center justify-center bg-border",
|
|
274
|
+
"relative flex shrink-0 items-center justify-center bg-border",
|
|
173
275
|
direction === "horizontal"
|
|
174
|
-
? "w-
|
|
175
|
-
: "h-
|
|
176
|
-
isDragging &&
|
|
177
|
-
"transition-all",
|
|
276
|
+
? "w-1 cursor-col-resize hover:bg-primary/50 active:bg-primary"
|
|
277
|
+
: "h-1 cursor-row-resize hover:bg-primary/50 active:bg-primary",
|
|
278
|
+
isDragging && "bg-primary",
|
|
178
279
|
className
|
|
179
280
|
)}
|
|
180
281
|
onMouseDown={handleMouseDown}
|
package/registry/scroll-area.tsx
CHANGED
|
@@ -3,17 +3,47 @@ import { cn } from "@/lib/utils"
|
|
|
3
3
|
|
|
4
4
|
interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
5
|
/** Orientation of scrollbar */
|
|
6
|
-
orientation?: "vertical" | "horizontal"
|
|
6
|
+
orientation?: "vertical" | "horizontal"
|
|
7
|
+
/** Scrollbar size: "thin" (4px), "default" (8px), "thick" (12px) */
|
|
8
|
+
scrollbarSize?: "thin" | "default" | "thick"
|
|
9
|
+
/** Hide scrollbar until hover */
|
|
10
|
+
hideScrollbar?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// CSS for custom scrollbar styling
|
|
14
|
+
const scrollbarStyles = {
|
|
15
|
+
thin: {
|
|
16
|
+
width: "4px",
|
|
17
|
+
height: "4px",
|
|
18
|
+
},
|
|
19
|
+
default: {
|
|
20
|
+
width: "8px",
|
|
21
|
+
height: "8px",
|
|
22
|
+
},
|
|
23
|
+
thick: {
|
|
24
|
+
width: "12px",
|
|
25
|
+
height: "12px",
|
|
26
|
+
},
|
|
7
27
|
}
|
|
8
28
|
|
|
9
29
|
/**
|
|
10
30
|
* ScrollArea - Custom scrollbar container
|
|
11
31
|
*
|
|
12
32
|
* Provides a styled scrollbar that is thin and consistent across browsers.
|
|
13
|
-
*
|
|
33
|
+
* Supports customizable scrollbar size and orientation.
|
|
14
34
|
*/
|
|
15
35
|
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
16
|
-
({ className, children, orientation = "vertical", ...props }, ref) => {
|
|
36
|
+
({ className, children, orientation = "vertical", scrollbarSize = "thin", hideScrollbar = false, style, ...props }, ref) => {
|
|
37
|
+
const sizes = scrollbarStyles[scrollbarSize]
|
|
38
|
+
|
|
39
|
+
const scrollbarCSS: React.CSSProperties = {
|
|
40
|
+
...style,
|
|
41
|
+
// Webkit browsers (Chrome, Safari, Edge)
|
|
42
|
+
// @ts-ignore - CSS custom properties for scrollbar
|
|
43
|
+
"--scrollbar-width": sizes.width,
|
|
44
|
+
"--scrollbar-height": sizes.height,
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
return (
|
|
18
48
|
<div
|
|
19
49
|
ref={ref}
|
|
@@ -22,13 +52,42 @@ const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
|
22
52
|
// Container overflow based on orientation
|
|
23
53
|
orientation === "vertical" && "overflow-y-auto overflow-x-hidden",
|
|
24
54
|
orientation === "horizontal" && "overflow-x-auto overflow-y-hidden",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"scrollbar-
|
|
55
|
+
// Custom scrollbar classes
|
|
56
|
+
"scrollbar-custom",
|
|
57
|
+
hideScrollbar && "scrollbar-hide hover:scrollbar-show",
|
|
28
58
|
className
|
|
29
59
|
)}
|
|
60
|
+
style={scrollbarCSS}
|
|
30
61
|
{...props}
|
|
31
62
|
>
|
|
63
|
+
<style>{`
|
|
64
|
+
.scrollbar-custom::-webkit-scrollbar {
|
|
65
|
+
width: var(--scrollbar-width, 4px);
|
|
66
|
+
height: var(--scrollbar-height, 4px);
|
|
67
|
+
}
|
|
68
|
+
.scrollbar-custom::-webkit-scrollbar-track {
|
|
69
|
+
background: transparent;
|
|
70
|
+
border-radius: 9999px;
|
|
71
|
+
}
|
|
72
|
+
.scrollbar-custom::-webkit-scrollbar-thumb {
|
|
73
|
+
background: hsl(var(--muted-foreground) / 0.3);
|
|
74
|
+
border-radius: 9999px;
|
|
75
|
+
}
|
|
76
|
+
.scrollbar-custom::-webkit-scrollbar-thumb:hover {
|
|
77
|
+
background: hsl(var(--muted-foreground) / 0.5);
|
|
78
|
+
}
|
|
79
|
+
.scrollbar-custom {
|
|
80
|
+
scrollbar-width: thin;
|
|
81
|
+
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
|
|
82
|
+
}
|
|
83
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
84
|
+
opacity: 0;
|
|
85
|
+
}
|
|
86
|
+
.scrollbar-hide:hover::-webkit-scrollbar,
|
|
87
|
+
.scrollbar-show::-webkit-scrollbar {
|
|
88
|
+
opacity: 1;
|
|
89
|
+
}
|
|
90
|
+
`}</style>
|
|
32
91
|
{children}
|
|
33
92
|
</div>
|
|
34
93
|
)
|
|
@@ -36,7 +95,7 @@ const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
|
36
95
|
)
|
|
37
96
|
ScrollArea.displayName = "ScrollArea"
|
|
38
97
|
|
|
39
|
-
// ScrollBar component for explicit scrollbar styling reference
|
|
98
|
+
// ScrollBar component for explicit scrollbar styling reference (optional usage)
|
|
40
99
|
interface ScrollBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
41
100
|
orientation?: "vertical" | "horizontal"
|
|
42
101
|
}
|