@srcroot/ui 0.0.55 → 0.0.58
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/README.md +151 -151
- package/dist/index.d.ts +0 -0
- package/dist/index.js +120 -93
- package/package.json +7 -2
- package/src/registry/analytics/google-analytics.tsx +36 -39
- package/src/registry/analytics/google-tag-manager.tsx +62 -65
- package/src/registry/analytics/meta-pixel.tsx +44 -47
- package/src/registry/analytics/microsoft-clarity.tsx +31 -34
- package/src/registry/analytics/tiktok-pixel.tsx +34 -37
- package/src/registry/lib/utils.ts +0 -0
- package/src/registry/themes/v3/blue.css +157 -157
- package/src/registry/themes/v3/glass.css +153 -153
- package/src/registry/themes/v3/gray.css +157 -157
- package/src/registry/themes/v3/green.css +157 -157
- package/src/registry/themes/v3/neutral.css +157 -157
- package/src/registry/themes/v3/orange.css +157 -157
- package/src/registry/themes/v3/rose.css +157 -157
- package/src/registry/themes/v3/slate.css +157 -157
- package/src/registry/themes/v3/stone.css +157 -157
- package/src/registry/themes/v3/violet.css +186 -186
- package/src/registry/themes/v3/zinc.css +157 -157
- package/src/registry/themes/v4/blue.css +184 -184
- package/src/registry/themes/v4/glass.css +180 -180
- package/src/registry/themes/v4/gray.css +184 -184
- package/src/registry/themes/v4/green.css +184 -184
- package/src/registry/themes/v4/neutral.css +184 -184
- package/src/registry/themes/v4/orange.css +184 -184
- package/src/registry/themes/v4/rose.css +184 -184
- package/src/registry/themes/v4/slate.css +184 -184
- package/src/registry/themes/v4/stone.css +184 -184
- package/src/registry/themes/v4/violet.css +184 -184
- package/src/registry/themes/v4/zinc.css +184 -184
- package/src/registry/ui/accordion.tsx +164 -165
- package/src/registry/ui/alert-dialog.tsx +213 -214
- package/src/registry/ui/alert.tsx +73 -76
- package/src/registry/ui/aspect-ratio.tsx +44 -47
- package/src/registry/ui/avatar.tsx +96 -97
- package/src/registry/ui/badge.tsx +52 -55
- package/src/registry/ui/breadcrumb.tsx +147 -150
- package/src/registry/ui/button-group.tsx +64 -67
- package/src/registry/ui/button.tsx +71 -72
- package/src/registry/ui/calendar.tsx +514 -515
- package/src/registry/ui/card.tsx +88 -91
- package/src/registry/ui/carousel.tsx +214 -214
- package/src/registry/ui/chart.tsx +373 -373
- package/src/registry/ui/chatbot.tsx +86 -13
- package/src/registry/ui/checkbox.tsx +93 -94
- package/src/registry/ui/collapsible.tsx +107 -108
- package/src/registry/ui/combobox.tsx +171 -171
- package/src/registry/ui/command.tsx +300 -300
- package/src/registry/ui/container.tsx +44 -47
- package/src/registry/ui/context-menu.tsx +221 -221
- package/src/registry/ui/date-picker.tsx +228 -228
- package/src/registry/ui/dialog.tsx +269 -270
- package/src/registry/ui/drawer.tsx +10 -4
- package/src/registry/ui/dropdown-menu.tsx +529 -530
- package/src/registry/ui/empty-state.tsx +0 -2
- package/src/registry/ui/file-upload.tsx +0 -0
- package/src/registry/ui/floating-dock.tsx +0 -0
- package/src/registry/ui/form-field.tsx +91 -94
- package/src/registry/ui/google-analytics.tsx +38 -0
- package/src/registry/ui/google-tag-manager.tsx +64 -0
- package/src/registry/ui/hover-card.tsx +223 -223
- package/src/registry/ui/image.tsx +144 -147
- package/src/registry/ui/input-group.tsx +82 -85
- package/src/registry/ui/input.tsx +125 -125
- package/src/registry/ui/kbd.tsx +60 -63
- package/src/registry/ui/label.tsx +36 -37
- package/src/registry/ui/loading-spinner.tsx +108 -111
- package/src/registry/ui/map.tsx +0 -0
- package/src/registry/ui/marquee.tsx +2 -0
- package/src/registry/ui/menubar.tsx +246 -246
- package/src/registry/ui/meta-pixel.tsx +46 -0
- package/src/registry/ui/microsoft-clarity.tsx +33 -0
- package/src/registry/ui/native-select.tsx +49 -52
- package/src/registry/ui/otp-input.tsx +163 -155
- package/src/registry/ui/pagination.tsx +149 -152
- package/src/registry/ui/patterns.tsx +28 -0
- package/src/registry/ui/popover.tsx +226 -227
- package/src/registry/ui/progress.tsx +51 -52
- package/src/registry/ui/radio.tsx +99 -102
- package/src/registry/ui/resizable.tsx +314 -314
- package/src/registry/ui/scroll-animation.tsx +45 -0
- package/src/registry/ui/scroll-area.tsx +121 -122
- package/src/registry/ui/scroll-to-top.tsx +0 -0
- package/src/registry/ui/search.tsx +162 -150
- package/src/registry/ui/select.tsx +292 -293
- package/src/registry/ui/separator.tsx +46 -47
- package/src/registry/ui/sheet.tsx +6 -3
- package/src/registry/ui/sidebar.tsx +628 -628
- package/src/registry/ui/skeleton.tsx +26 -29
- package/src/registry/ui/slider.tsx +196 -197
- package/src/registry/ui/slot.tsx +69 -72
- package/src/registry/ui/star-rating.tsx +146 -134
- package/src/registry/ui/switch.tsx +72 -73
- package/src/registry/ui/table-of-contents.tsx +96 -96
- package/src/registry/ui/table.tsx +138 -139
- package/src/registry/ui/tabs.tsx +124 -125
- package/src/registry/ui/text.tsx +61 -64
- package/src/registry/ui/textarea.tsx +41 -42
- package/src/registry/ui/theme-switcher.tsx +66 -66
- package/src/registry/ui/tiktok-pixel.tsx +36 -0
- package/src/registry/ui/toast.tsx +97 -98
- package/src/registry/ui/toggle-group.tsx +129 -129
- package/src/registry/ui/toggle.tsx +72 -72
- package/src/registry/ui/tooltip.tsx +143 -144
- package/src/registry/ui/whatsapp.tsx +0 -0
|
@@ -1,314 +1,314 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
// Resizable Context
|
|
7
|
-
interface ResizablePanelGroupContextValue {
|
|
8
|
-
groupId: string
|
|
9
|
-
direction: "horizontal" | "vertical"
|
|
10
|
-
registerPanel: (defaultSize: number, minSize: number, maxSize: number) => number
|
|
11
|
-
getSize: (index: number) => number
|
|
12
|
-
getTotalSize: () => number
|
|
13
|
-
onResize: (handleIndex: number, delta: number) => void
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
|
|
17
|
-
|
|
18
|
-
function useResizablePanelGroup() {
|
|
19
|
-
const context = React.useContext(ResizablePanelGroupContext)
|
|
20
|
-
if (!context) {
|
|
21
|
-
throw new Error("useResizablePanelGroup must be used within a ResizablePanelGroup")
|
|
22
|
-
}
|
|
23
|
-
return context
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ResizablePanelGroup
|
|
27
|
-
interface ResizablePanelGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
28
|
-
direction?: "horizontal" | "vertical"
|
|
29
|
-
onLayout?: (sizes: number[]) => void
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface PanelConfig {
|
|
33
|
-
defaultSize: number
|
|
34
|
-
minSize: number
|
|
35
|
-
maxSize: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
|
|
39
|
-
({ className, direction = "horizontal", children, onLayout, ...props }, ref) => {
|
|
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[]>([])
|
|
47
|
-
const panelCountRef = React.useRef(0)
|
|
48
|
-
|
|
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
|
-
|
|
61
|
-
return index
|
|
62
|
-
}, [])
|
|
63
|
-
|
|
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
|
|
107
|
-
}
|
|
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])
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<ResizablePanelGroupContext.Provider value={{ groupId, direction, registerPanel, getSize, getTotalSize, onResize }}>
|
|
133
|
-
<div
|
|
134
|
-
ref={(node) => {
|
|
135
|
-
containerRef.current = node
|
|
136
|
-
if (typeof ref === "function") ref(node)
|
|
137
|
-
else if (ref) ref.current = node
|
|
138
|
-
}}
|
|
139
|
-
data-panel-group
|
|
140
|
-
data-panel-group-id={groupId}
|
|
141
|
-
data-direction={direction}
|
|
142
|
-
className={cn(
|
|
143
|
-
"flex h-full w-full",
|
|
144
|
-
direction === "horizontal" ? "flex-row" : "flex-col",
|
|
145
|
-
className
|
|
146
|
-
)}
|
|
147
|
-
{...props}
|
|
148
|
-
>
|
|
149
|
-
{children}
|
|
150
|
-
</div>
|
|
151
|
-
</ResizablePanelGroupContext.Provider>
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
)
|
|
155
|
-
ResizablePanelGroup.displayName = "ResizablePanelGroup"
|
|
156
|
-
|
|
157
|
-
// ResizablePanel
|
|
158
|
-
interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
159
|
-
defaultSize?: number
|
|
160
|
-
minSize?: number
|
|
161
|
-
maxSize?: number
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
|
|
165
|
-
({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
|
|
166
|
-
const { groupId, direction, registerPanel, getSize, getTotalSize } = useResizablePanelGroup()
|
|
167
|
-
const indexRef = React.useRef<number>(-1)
|
|
168
|
-
|
|
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
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<div
|
|
181
|
-
ref={ref}
|
|
182
|
-
data-panel
|
|
183
|
-
data-panel-group-id={groupId}
|
|
184
|
-
data-panel-index={indexRef.current}
|
|
185
|
-
className={cn("overflow-hidden", className)}
|
|
186
|
-
style={{
|
|
187
|
-
...style,
|
|
188
|
-
// Use flex-grow with the size as the ratio for smoother resizing
|
|
189
|
-
flex: `${size} 1 0`,
|
|
190
|
-
}}
|
|
191
|
-
{...props}
|
|
192
|
-
>
|
|
193
|
-
{children}
|
|
194
|
-
</div>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
)
|
|
198
|
-
ResizablePanel.displayName = "ResizablePanel"
|
|
199
|
-
|
|
200
|
-
// ResizableHandle
|
|
201
|
-
interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
202
|
-
withHandle?: boolean
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
|
|
206
|
-
({ className, withHandle = false, ...props }, ref) => {
|
|
207
|
-
const { direction, onResize } = useResizablePanelGroup()
|
|
208
|
-
const [isDragging, setIsDragging] = React.useState(false)
|
|
209
|
-
const handleIndexRef = React.useRef<number>(-1)
|
|
210
|
-
const lastPosRef = React.useRef<number>(0)
|
|
211
|
-
const handleRef = React.useRef<HTMLDivElement>(null)
|
|
212
|
-
|
|
213
|
-
// Determine handle index from DOM position
|
|
214
|
-
const getHandleIndex = React.useCallback(() => {
|
|
215
|
-
const handle = handleRef.current
|
|
216
|
-
if (!handle) return 0
|
|
217
|
-
|
|
218
|
-
const parent = handle.parentElement
|
|
219
|
-
if (!parent) return 0
|
|
220
|
-
|
|
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)
|
|
235
|
-
|
|
236
|
-
const handleIndex = getHandleIndex()
|
|
237
|
-
const startPos = direction === "horizontal" ? e.clientX : e.clientY
|
|
238
|
-
lastPosRef.current = startPos
|
|
239
|
-
|
|
240
|
-
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
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
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const handleMouseUp = () => {
|
|
251
|
-
setIsDragging(false)
|
|
252
|
-
document.removeEventListener("mousemove", handleMouseMove)
|
|
253
|
-
document.removeEventListener("mouseup", handleMouseUp)
|
|
254
|
-
document.body.style.cursor = ""
|
|
255
|
-
document.body.style.userSelect = ""
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
document.body.style.cursor = direction === "horizontal" ? "col-resize" : "row-resize"
|
|
259
|
-
document.body.style.userSelect = "none"
|
|
260
|
-
document.addEventListener("mousemove", handleMouseMove)
|
|
261
|
-
document.addEventListener("mouseup", handleMouseUp)
|
|
262
|
-
}, [direction, onResize, getHandleIndex])
|
|
263
|
-
|
|
264
|
-
return (
|
|
265
|
-
<div
|
|
266
|
-
ref={(node) => {
|
|
267
|
-
handleRef.current = node
|
|
268
|
-
if (typeof ref === "function") ref(node)
|
|
269
|
-
else if (ref) ref.current = node
|
|
270
|
-
}}
|
|
271
|
-
data-panel-resize-handle
|
|
272
|
-
data-dragging={isDragging}
|
|
273
|
-
className={cn(
|
|
274
|
-
"relative flex shrink-0 items-center justify-center bg-border",
|
|
275
|
-
direction === "horizontal"
|
|
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",
|
|
279
|
-
className
|
|
280
|
-
)}
|
|
281
|
-
onMouseDown={handleMouseDown}
|
|
282
|
-
{...props}
|
|
283
|
-
>
|
|
284
|
-
{withHandle && (
|
|
285
|
-
<div
|
|
286
|
-
className={cn(
|
|
287
|
-
"z-10 flex items-center justify-center rounded-sm border bg-border",
|
|
288
|
-
direction === "horizontal" ? "h-4 w-3" : "h-3 w-4"
|
|
289
|
-
)}
|
|
290
|
-
>
|
|
291
|
-
<svg
|
|
292
|
-
className={cn(
|
|
293
|
-
"h-2.5 w-2.5 text-muted-foreground",
|
|
294
|
-
direction === "vertical" && "rotate-90"
|
|
295
|
-
)}
|
|
296
|
-
viewBox="0 0 6 10"
|
|
297
|
-
fill="currentColor"
|
|
298
|
-
>
|
|
299
|
-
<circle cx="1" cy="2" r="1" />
|
|
300
|
-
<circle cx="1" cy="5" r="1" />
|
|
301
|
-
<circle cx="1" cy="8" r="1" />
|
|
302
|
-
<circle cx="5" cy="2" r="1" />
|
|
303
|
-
<circle cx="5" cy="5" r="1" />
|
|
304
|
-
<circle cx="5" cy="8" r="1" />
|
|
305
|
-
</svg>
|
|
306
|
-
</div>
|
|
307
|
-
)}
|
|
308
|
-
</div>
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
)
|
|
312
|
-
ResizableHandle.displayName = "ResizableHandle"
|
|
313
|
-
|
|
314
|
-
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
// Resizable Context
|
|
7
|
+
interface ResizablePanelGroupContextValue {
|
|
8
|
+
groupId: string
|
|
9
|
+
direction: "horizontal" | "vertical"
|
|
10
|
+
registerPanel: (defaultSize: number, minSize: number, maxSize: number) => number
|
|
11
|
+
getSize: (index: number) => number
|
|
12
|
+
getTotalSize: () => number
|
|
13
|
+
onResize: (handleIndex: number, delta: number) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
|
|
17
|
+
|
|
18
|
+
function useResizablePanelGroup() {
|
|
19
|
+
const context = React.useContext(ResizablePanelGroupContext)
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error("useResizablePanelGroup must be used within a ResizablePanelGroup")
|
|
22
|
+
}
|
|
23
|
+
return context
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ResizablePanelGroup
|
|
27
|
+
interface ResizablePanelGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
28
|
+
direction?: "horizontal" | "vertical"
|
|
29
|
+
onLayout?: (sizes: number[]) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PanelConfig {
|
|
33
|
+
defaultSize: number
|
|
34
|
+
minSize: number
|
|
35
|
+
maxSize: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
|
|
39
|
+
({ className, direction = "horizontal", children, onLayout, ...props }, ref) => {
|
|
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[]>([])
|
|
47
|
+
const panelCountRef = React.useRef(0)
|
|
48
|
+
|
|
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
|
+
|
|
61
|
+
return index
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
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
|
|
107
|
+
}
|
|
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])
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<ResizablePanelGroupContext.Provider value={{ groupId, direction, registerPanel, getSize, getTotalSize, onResize }}>
|
|
133
|
+
<div
|
|
134
|
+
ref={(node) => {
|
|
135
|
+
containerRef.current = node
|
|
136
|
+
if (typeof ref === "function") ref(node)
|
|
137
|
+
else if (ref) ref.current = node
|
|
138
|
+
}}
|
|
139
|
+
data-panel-group
|
|
140
|
+
data-panel-group-id={groupId}
|
|
141
|
+
data-direction={direction}
|
|
142
|
+
className={cn(
|
|
143
|
+
"flex h-full w-full",
|
|
144
|
+
direction === "horizontal" ? "flex-row" : "flex-col",
|
|
145
|
+
className
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</div>
|
|
151
|
+
</ResizablePanelGroupContext.Provider>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
ResizablePanelGroup.displayName = "ResizablePanelGroup"
|
|
156
|
+
|
|
157
|
+
// ResizablePanel
|
|
158
|
+
interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
159
|
+
defaultSize?: number
|
|
160
|
+
minSize?: number
|
|
161
|
+
maxSize?: number
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
|
|
165
|
+
({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
|
|
166
|
+
const { groupId, direction, registerPanel, getSize, getTotalSize } = useResizablePanelGroup()
|
|
167
|
+
const indexRef = React.useRef<number>(-1)
|
|
168
|
+
|
|
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
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
ref={ref}
|
|
182
|
+
data-panel
|
|
183
|
+
data-panel-group-id={groupId}
|
|
184
|
+
data-panel-index={indexRef.current}
|
|
185
|
+
className={cn("overflow-hidden", className)}
|
|
186
|
+
style={{
|
|
187
|
+
...style,
|
|
188
|
+
// Use flex-grow with the size as the ratio for smoother resizing
|
|
189
|
+
flex: `${size} 1 0`,
|
|
190
|
+
}}
|
|
191
|
+
{...props}
|
|
192
|
+
>
|
|
193
|
+
{children}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
ResizablePanel.displayName = "ResizablePanel"
|
|
199
|
+
|
|
200
|
+
// ResizableHandle
|
|
201
|
+
interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
202
|
+
withHandle?: boolean
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
|
|
206
|
+
({ className, withHandle = false, ...props }, ref) => {
|
|
207
|
+
const { direction, onResize } = useResizablePanelGroup()
|
|
208
|
+
const [isDragging, setIsDragging] = React.useState(false)
|
|
209
|
+
const handleIndexRef = React.useRef<number>(-1)
|
|
210
|
+
const lastPosRef = React.useRef<number>(0)
|
|
211
|
+
const handleRef = React.useRef<HTMLDivElement>(null)
|
|
212
|
+
|
|
213
|
+
// Determine handle index from DOM position
|
|
214
|
+
const getHandleIndex = React.useCallback(() => {
|
|
215
|
+
const handle = handleRef.current
|
|
216
|
+
if (!handle) return 0
|
|
217
|
+
|
|
218
|
+
const parent = handle.parentElement
|
|
219
|
+
if (!parent) return 0
|
|
220
|
+
|
|
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)
|
|
235
|
+
|
|
236
|
+
const handleIndex = getHandleIndex()
|
|
237
|
+
const startPos = direction === "horizontal" ? e.clientX : e.clientY
|
|
238
|
+
lastPosRef.current = startPos
|
|
239
|
+
|
|
240
|
+
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
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
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const handleMouseUp = () => {
|
|
251
|
+
setIsDragging(false)
|
|
252
|
+
document.removeEventListener("mousemove", handleMouseMove)
|
|
253
|
+
document.removeEventListener("mouseup", handleMouseUp)
|
|
254
|
+
document.body.style.cursor = ""
|
|
255
|
+
document.body.style.userSelect = ""
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
document.body.style.cursor = direction === "horizontal" ? "col-resize" : "row-resize"
|
|
259
|
+
document.body.style.userSelect = "none"
|
|
260
|
+
document.addEventListener("mousemove", handleMouseMove)
|
|
261
|
+
document.addEventListener("mouseup", handleMouseUp)
|
|
262
|
+
}, [direction, onResize, getHandleIndex])
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<div
|
|
266
|
+
ref={(node) => {
|
|
267
|
+
handleRef.current = node
|
|
268
|
+
if (typeof ref === "function") ref(node)
|
|
269
|
+
else if (ref) ref.current = node
|
|
270
|
+
}}
|
|
271
|
+
data-panel-resize-handle
|
|
272
|
+
data-dragging={isDragging}
|
|
273
|
+
className={cn(
|
|
274
|
+
"relative flex shrink-0 items-center justify-center bg-border",
|
|
275
|
+
direction === "horizontal"
|
|
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",
|
|
279
|
+
className
|
|
280
|
+
)}
|
|
281
|
+
onMouseDown={handleMouseDown}
|
|
282
|
+
{...props}
|
|
283
|
+
>
|
|
284
|
+
{withHandle && (
|
|
285
|
+
<div
|
|
286
|
+
className={cn(
|
|
287
|
+
"z-10 flex items-center justify-center rounded-sm border bg-border",
|
|
288
|
+
direction === "horizontal" ? "h-4 w-3" : "h-3 w-4"
|
|
289
|
+
)}
|
|
290
|
+
>
|
|
291
|
+
<svg
|
|
292
|
+
className={cn(
|
|
293
|
+
"h-2.5 w-2.5 text-muted-foreground",
|
|
294
|
+
direction === "vertical" && "rotate-90"
|
|
295
|
+
)}
|
|
296
|
+
viewBox="0 0 6 10"
|
|
297
|
+
fill="currentColor"
|
|
298
|
+
>
|
|
299
|
+
<circle cx="1" cy="2" r="1" />
|
|
300
|
+
<circle cx="1" cy="5" r="1" />
|
|
301
|
+
<circle cx="1" cy="8" r="1" />
|
|
302
|
+
<circle cx="5" cy="2" r="1" />
|
|
303
|
+
<circle cx="5" cy="5" r="1" />
|
|
304
|
+
<circle cx="5" cy="8" r="1" />
|
|
305
|
+
</svg>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
ResizableHandle.displayName = "ResizableHandle"
|
|
313
|
+
|
|
314
|
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|