@srcroot/ui 0.0.1 → 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.
- package/dist/index.js +91 -0
- package/package.json +9 -3
- 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/calendar.tsx +416 -142
- package/registry/card.tsx +21 -47
- package/registry/combobox.tsx +171 -0
- package/registry/command.tsx +300 -0
- package/registry/container.tsx +9 -25
- package/registry/context-menu.tsx +221 -0
- package/registry/date-picker.tsx +179 -0
- package/registry/drawer.tsx +241 -0
- package/registry/dropdown-menu.tsx +93 -74
- package/registry/file-upload.tsx +240 -0
- package/registry/hover-card.tsx +165 -0
- package/registry/image.tsx +2 -2
- package/registry/kbd.tsx +60 -0
- package/registry/menubar.tsx +246 -0
- package/registry/native-select.tsx +49 -0
- package/registry/pagination.tsx +3 -0
- package/registry/resizable.tsx +251 -0
- package/registry/scroll-area.tsx +119 -0
- package/registry/search.tsx +2 -1
- package/registry/sheet.tsx +63 -18
- package/registry/sidebar.tsx +512 -0
- package/registry/slider.tsx +133 -54
- package/registry/text.tsx +7 -16
- package/registry/toggle-group.tsx +129 -0
- package/registry/toggle.tsx +72 -0
- package/registry/tooltip.tsx +21 -3
package/registry/kbd.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
interface KbdProps extends React.HTMLAttributes<HTMLElement> {
|
|
5
|
+
/** Array of keys to display (e.g., ["Ctrl", "K"]) */
|
|
6
|
+
keys?: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Kbd - Keyboard key display component
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* <Kbd>⌘</Kbd>
|
|
14
|
+
* <Kbd keys={["Ctrl", "Shift", "P"]} />
|
|
15
|
+
*/
|
|
16
|
+
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
17
|
+
({ className, children, keys, ...props }, ref) => {
|
|
18
|
+
// If keys array is provided, render each key
|
|
19
|
+
if (keys && keys.length > 0) {
|
|
20
|
+
return (
|
|
21
|
+
<span className="inline-flex items-center gap-1">
|
|
22
|
+
{keys.map((key, index) => (
|
|
23
|
+
<React.Fragment key={index}>
|
|
24
|
+
<kbd
|
|
25
|
+
ref={index === 0 ? ref : undefined}
|
|
26
|
+
className={cn(
|
|
27
|
+
"pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{key}
|
|
33
|
+
</kbd>
|
|
34
|
+
{index < keys.length - 1 && (
|
|
35
|
+
<span className="text-muted-foreground text-xs">+</span>
|
|
36
|
+
)}
|
|
37
|
+
</React.Fragment>
|
|
38
|
+
))}
|
|
39
|
+
</span>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Single key rendering
|
|
44
|
+
return (
|
|
45
|
+
<kbd
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
"pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground",
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</kbd>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
Kbd.displayName = "Kbd"
|
|
59
|
+
|
|
60
|
+
export { Kbd }
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
// Menubar Context
|
|
7
|
+
interface MenubarContextValue {
|
|
8
|
+
activeMenu: string | null
|
|
9
|
+
setActiveMenu: (menu: string | null) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MenubarContext = React.createContext<MenubarContextValue | null>(null)
|
|
13
|
+
|
|
14
|
+
function useMenubar() {
|
|
15
|
+
const context = React.useContext(MenubarContext)
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error("useMenubar must be used within a Menubar")
|
|
18
|
+
}
|
|
19
|
+
return context
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MenubarMenu Context
|
|
23
|
+
interface MenubarMenuContextValue {
|
|
24
|
+
menuId: string
|
|
25
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MenubarMenuContext = React.createContext<MenubarMenuContextValue | null>(null)
|
|
29
|
+
|
|
30
|
+
function useMenubarMenu() {
|
|
31
|
+
const context = React.useContext(MenubarMenuContext)
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error("useMenubarMenu must be used within a MenubarMenu")
|
|
34
|
+
}
|
|
35
|
+
return context
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Menubar Root
|
|
39
|
+
const Menubar = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
40
|
+
({ className, children, ...props }, ref) => {
|
|
41
|
+
const [activeMenu, setActiveMenu] = React.useState<string | null>(null)
|
|
42
|
+
|
|
43
|
+
// Close on outside click
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (!activeMenu) return
|
|
46
|
+
const handleClick = (e: MouseEvent) => {
|
|
47
|
+
const target = e.target as Element
|
|
48
|
+
if (!target.closest('[data-menubar]')) {
|
|
49
|
+
setActiveMenu(null)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
document.addEventListener("click", handleClick)
|
|
53
|
+
return () => document.removeEventListener("click", handleClick)
|
|
54
|
+
}, [activeMenu])
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<MenubarContext.Provider value={{ activeMenu, setActiveMenu }}>
|
|
58
|
+
<div
|
|
59
|
+
ref={ref}
|
|
60
|
+
data-menubar
|
|
61
|
+
className={cn(
|
|
62
|
+
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</div>
|
|
69
|
+
</MenubarContext.Provider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
Menubar.displayName = "Menubar"
|
|
74
|
+
|
|
75
|
+
// MenubarMenu
|
|
76
|
+
interface MenubarMenuProps {
|
|
77
|
+
children: React.ReactNode
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const MenubarMenu = ({ children }: MenubarMenuProps) => {
|
|
81
|
+
const menuId = React.useId()
|
|
82
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<MenubarMenuContext.Provider value={{ menuId, triggerRef }}>
|
|
86
|
+
<div className="relative">
|
|
87
|
+
{children}
|
|
88
|
+
</div>
|
|
89
|
+
</MenubarMenuContext.Provider>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
MenubarMenu.displayName = "MenubarMenu"
|
|
93
|
+
|
|
94
|
+
// MenubarTrigger
|
|
95
|
+
const MenubarTrigger = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
96
|
+
({ className, children, ...props }, ref) => {
|
|
97
|
+
const { activeMenu, setActiveMenu } = useMenubar()
|
|
98
|
+
const { menuId, triggerRef } = useMenubarMenu()
|
|
99
|
+
const isOpen = activeMenu === menuId
|
|
100
|
+
|
|
101
|
+
const handleClick = () => {
|
|
102
|
+
setActiveMenu(isOpen ? null : menuId)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleMouseEnter = () => {
|
|
106
|
+
if (activeMenu && activeMenu !== menuId) {
|
|
107
|
+
setActiveMenu(menuId)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<button
|
|
113
|
+
ref={triggerRef}
|
|
114
|
+
className={cn(
|
|
115
|
+
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none",
|
|
116
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
117
|
+
isOpen && "bg-accent text-accent-foreground",
|
|
118
|
+
className
|
|
119
|
+
)}
|
|
120
|
+
onClick={handleClick}
|
|
121
|
+
onMouseEnter={handleMouseEnter}
|
|
122
|
+
{...props}
|
|
123
|
+
>
|
|
124
|
+
{children}
|
|
125
|
+
</button>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
MenubarTrigger.displayName = "MenubarTrigger"
|
|
130
|
+
|
|
131
|
+
// MenubarContent
|
|
132
|
+
const MenubarContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
133
|
+
({ className, children, ...props }, ref) => {
|
|
134
|
+
const { activeMenu, setActiveMenu } = useMenubar()
|
|
135
|
+
const { menuId, triggerRef } = useMenubarMenu()
|
|
136
|
+
const isOpen = activeMenu === menuId
|
|
137
|
+
const [position, setPosition] = React.useState({ top: 0, left: 0 })
|
|
138
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
139
|
+
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
if (!isOpen || !triggerRef.current) return
|
|
142
|
+
const rect = triggerRef.current.getBoundingClientRect()
|
|
143
|
+
setPosition({
|
|
144
|
+
top: rect.bottom + 4,
|
|
145
|
+
left: rect.left,
|
|
146
|
+
})
|
|
147
|
+
}, [isOpen, triggerRef])
|
|
148
|
+
|
|
149
|
+
// Close on Escape
|
|
150
|
+
React.useEffect(() => {
|
|
151
|
+
if (!isOpen) return
|
|
152
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
153
|
+
if (e.key === "Escape") setActiveMenu(null)
|
|
154
|
+
}
|
|
155
|
+
document.addEventListener("keydown", handleEscape)
|
|
156
|
+
return () => document.removeEventListener("keydown", handleEscape)
|
|
157
|
+
}, [isOpen, setActiveMenu])
|
|
158
|
+
|
|
159
|
+
if (!isOpen) return null
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
ref={contentRef}
|
|
164
|
+
className={cn(
|
|
165
|
+
"fixed z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
166
|
+
"animate-in fade-in-0 slide-in-from-top-2",
|
|
167
|
+
className
|
|
168
|
+
)}
|
|
169
|
+
style={{
|
|
170
|
+
top: position.top,
|
|
171
|
+
left: position.left,
|
|
172
|
+
}}
|
|
173
|
+
{...props}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
MenubarContent.displayName = "MenubarContent"
|
|
181
|
+
|
|
182
|
+
// MenubarItem
|
|
183
|
+
interface MenubarItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
184
|
+
inset?: boolean
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const MenubarItem = React.forwardRef<HTMLButtonElement, MenubarItemProps>(
|
|
188
|
+
({ className, inset, children, ...props }, ref) => {
|
|
189
|
+
const { setActiveMenu } = useMenubar()
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<button
|
|
193
|
+
ref={ref}
|
|
194
|
+
className={cn(
|
|
195
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
|
196
|
+
"focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
197
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
198
|
+
inset && "pl-8",
|
|
199
|
+
className
|
|
200
|
+
)}
|
|
201
|
+
onClick={() => setActiveMenu(null)}
|
|
202
|
+
{...props}
|
|
203
|
+
>
|
|
204
|
+
{children}
|
|
205
|
+
</button>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
MenubarItem.displayName = "MenubarItem"
|
|
210
|
+
|
|
211
|
+
// MenubarSeparator
|
|
212
|
+
const MenubarSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
213
|
+
({ className, ...props }, ref) => (
|
|
214
|
+
<div ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
MenubarSeparator.displayName = "MenubarSeparator"
|
|
218
|
+
|
|
219
|
+
// MenubarLabel
|
|
220
|
+
const MenubarLabel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }>(
|
|
221
|
+
({ className, inset, ...props }, ref) => (
|
|
222
|
+
<div
|
|
223
|
+
ref={ref}
|
|
224
|
+
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
225
|
+
{...props}
|
|
226
|
+
/>
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
MenubarLabel.displayName = "MenubarLabel"
|
|
230
|
+
|
|
231
|
+
// MenubarShortcut
|
|
232
|
+
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
233
|
+
<span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
|
|
234
|
+
)
|
|
235
|
+
MenubarShortcut.displayName = "MenubarShortcut"
|
|
236
|
+
|
|
237
|
+
export {
|
|
238
|
+
Menubar,
|
|
239
|
+
MenubarMenu,
|
|
240
|
+
MenubarTrigger,
|
|
241
|
+
MenubarContent,
|
|
242
|
+
MenubarItem,
|
|
243
|
+
MenubarSeparator,
|
|
244
|
+
MenubarLabel,
|
|
245
|
+
MenubarShortcut,
|
|
246
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface NativeSelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> { }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* NativeSelect - Styled browser-native select element
|
|
8
|
+
*
|
|
9
|
+
* Uses the browser's native <select> for accessibility and mobile UX,
|
|
10
|
+
* with custom styling to match the design system.
|
|
11
|
+
*/
|
|
12
|
+
const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
13
|
+
({ className, children, ...props }, ref) => {
|
|
14
|
+
return (
|
|
15
|
+
<div className="relative">
|
|
16
|
+
<select
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"flex h-10 w-full appearance-none rounded-md border border-input bg-background px-3 py-2 pr-8 text-sm ring-offset-background",
|
|
20
|
+
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
21
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</select>
|
|
28
|
+
{/* Custom chevron icon */}
|
|
29
|
+
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
|
30
|
+
<svg
|
|
31
|
+
className="h-4 w-4 opacity-50"
|
|
32
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
33
|
+
viewBox="0 0 24 24"
|
|
34
|
+
fill="none"
|
|
35
|
+
stroke="currentColor"
|
|
36
|
+
strokeWidth="2"
|
|
37
|
+
strokeLinecap="round"
|
|
38
|
+
strokeLinejoin="round"
|
|
39
|
+
>
|
|
40
|
+
<path d="m6 9 6 6 6-6" />
|
|
41
|
+
</svg>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
NativeSelect.displayName = "NativeSelect"
|
|
48
|
+
|
|
49
|
+
export { NativeSelect }
|
package/registry/pagination.tsx
CHANGED
|
@@ -58,6 +58,9 @@ PaginationItem.displayName = "PaginationItem"
|
|
|
58
58
|
interface PaginationLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
59
59
|
isActive?: boolean
|
|
60
60
|
size?: "default" | "sm" | "lg" | "icon"
|
|
61
|
+
children?: React.ReactNode
|
|
62
|
+
className?: string
|
|
63
|
+
href?: string
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
const PaginationLink = ({
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
// Resizable Context
|
|
7
|
+
interface ResizablePanelGroupContextValue {
|
|
8
|
+
direction: "horizontal" | "vertical"
|
|
9
|
+
sizes: number[]
|
|
10
|
+
setSizes: React.Dispatch<React.SetStateAction<number[]>>
|
|
11
|
+
registerPanel: () => number
|
|
12
|
+
getPanelCount: () => number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
|
|
16
|
+
|
|
17
|
+
function useResizablePanelGroup() {
|
|
18
|
+
const context = React.useContext(ResizablePanelGroupContext)
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error("useResizablePanelGroup must be used within a ResizablePanelGroup")
|
|
21
|
+
}
|
|
22
|
+
return context
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ResizablePanelGroup
|
|
26
|
+
interface ResizablePanelGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
27
|
+
direction?: "horizontal" | "vertical"
|
|
28
|
+
onLayout?: (sizes: number[]) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
|
|
32
|
+
({ className, direction = "horizontal", children, onLayout, ...props }, ref) => {
|
|
33
|
+
const [sizes, setSizes] = React.useState<number[]>([])
|
|
34
|
+
const panelCountRef = React.useRef(0)
|
|
35
|
+
|
|
36
|
+
// Reset panel count on each render to handle re-renders properly
|
|
37
|
+
React.useLayoutEffect(() => {
|
|
38
|
+
panelCountRef.current = 0
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const registerPanel = React.useCallback(() => {
|
|
42
|
+
const index = panelCountRef.current
|
|
43
|
+
panelCountRef.current += 1
|
|
44
|
+
return index
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
const getPanelCount = React.useCallback(() => {
|
|
48
|
+
return panelCountRef.current
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
// Notify layout changes
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (sizes.length > 0) {
|
|
54
|
+
onLayout?.(sizes)
|
|
55
|
+
}
|
|
56
|
+
}, [sizes, onLayout])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<ResizablePanelGroupContext.Provider value={{ direction, sizes, setSizes, registerPanel, getPanelCount }}>
|
|
60
|
+
<div
|
|
61
|
+
ref={ref}
|
|
62
|
+
data-panel-group
|
|
63
|
+
data-direction={direction}
|
|
64
|
+
className={cn(
|
|
65
|
+
"flex h-full w-full",
|
|
66
|
+
direction === "horizontal" ? "flex-row" : "flex-col",
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
</ResizablePanelGroupContext.Provider>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
ResizablePanelGroup.displayName = "ResizablePanelGroup"
|
|
78
|
+
|
|
79
|
+
// ResizablePanel
|
|
80
|
+
interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
81
|
+
defaultSize?: number
|
|
82
|
+
minSize?: number
|
|
83
|
+
maxSize?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
|
|
87
|
+
({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
|
|
88
|
+
const { direction, sizes, setSizes, registerPanel } = useResizablePanelGroup()
|
|
89
|
+
const [index] = React.useState(() => registerPanel())
|
|
90
|
+
|
|
91
|
+
// Initialize size
|
|
92
|
+
React.useLayoutEffect(() => {
|
|
93
|
+
setSizes(prev => {
|
|
94
|
+
const newSizes = [...prev]
|
|
95
|
+
if (newSizes[index] === undefined) {
|
|
96
|
+
newSizes[index] = defaultSize
|
|
97
|
+
}
|
|
98
|
+
return newSizes
|
|
99
|
+
})
|
|
100
|
+
}, [index, defaultSize, setSizes])
|
|
101
|
+
|
|
102
|
+
const size = sizes[index] ?? defaultSize
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
ref={ref}
|
|
107
|
+
data-panel
|
|
108
|
+
data-panel-index={index}
|
|
109
|
+
className={cn("overflow-hidden", className)}
|
|
110
|
+
style={{
|
|
111
|
+
...style,
|
|
112
|
+
flex: `0 0 ${size}%`,
|
|
113
|
+
}}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
ResizablePanel.displayName = "ResizablePanel"
|
|
122
|
+
|
|
123
|
+
// ResizableHandle
|
|
124
|
+
interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
125
|
+
withHandle?: boolean
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
|
|
129
|
+
({ className, withHandle = false, ...props }, ref) => {
|
|
130
|
+
const { direction, setSizes } = useResizablePanelGroup()
|
|
131
|
+
const [isDragging, setIsDragging] = React.useState(false)
|
|
132
|
+
const handleRef = React.useRef<HTMLDivElement>(null)
|
|
133
|
+
|
|
134
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
135
|
+
e.preventDefault()
|
|
136
|
+
setIsDragging(true)
|
|
137
|
+
|
|
138
|
+
const startPos = direction === "horizontal" ? e.clientX : e.clientY
|
|
139
|
+
const handle = handleRef.current
|
|
140
|
+
if (!handle) return
|
|
141
|
+
|
|
142
|
+
// Find adjacent panels
|
|
143
|
+
const prevPanel = handle.previousElementSibling as HTMLElement
|
|
144
|
+
const nextPanel = handle.nextElementSibling as HTMLElement
|
|
145
|
+
if (!prevPanel || !nextPanel) return
|
|
146
|
+
|
|
147
|
+
const prevIndex = parseInt(prevPanel.dataset.panelIndex || "0")
|
|
148
|
+
const nextIndex = parseInt(nextPanel.dataset.panelIndex || "1")
|
|
149
|
+
|
|
150
|
+
// Get current sizes
|
|
151
|
+
const prevRect = prevPanel.getBoundingClientRect()
|
|
152
|
+
const nextRect = nextPanel.getBoundingClientRect()
|
|
153
|
+
const totalSize = direction === "horizontal"
|
|
154
|
+
? prevRect.width + nextRect.width
|
|
155
|
+
: prevRect.height + nextRect.height
|
|
156
|
+
|
|
157
|
+
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
158
|
+
const currentPos = direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
|
|
159
|
+
const delta = currentPos - startPos
|
|
160
|
+
const deltaPercent = (delta / totalSize) * 100
|
|
161
|
+
|
|
162
|
+
setSizes(prev => {
|
|
163
|
+
const newSizes = [...prev]
|
|
164
|
+
const prevSize = prev[prevIndex] ?? 50
|
|
165
|
+
const nextSize = prev[nextIndex] ?? 50
|
|
166
|
+
|
|
167
|
+
// Calculate new sizes with constraints
|
|
168
|
+
let newPrevSize = prevSize + deltaPercent
|
|
169
|
+
let newNextSize = nextSize - deltaPercent
|
|
170
|
+
|
|
171
|
+
// Apply min/max constraints
|
|
172
|
+
if (newPrevSize < 10) {
|
|
173
|
+
newPrevSize = 10
|
|
174
|
+
newNextSize = prevSize + nextSize - 10
|
|
175
|
+
}
|
|
176
|
+
if (newNextSize < 10) {
|
|
177
|
+
newNextSize = 10
|
|
178
|
+
newPrevSize = prevSize + nextSize - 10
|
|
179
|
+
}
|
|
180
|
+
if (newPrevSize > 90) {
|
|
181
|
+
newPrevSize = 90
|
|
182
|
+
newNextSize = prevSize + nextSize - 90
|
|
183
|
+
}
|
|
184
|
+
if (newNextSize > 90) {
|
|
185
|
+
newNextSize = 90
|
|
186
|
+
newPrevSize = prevSize + nextSize - 90
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
newSizes[prevIndex] = newPrevSize
|
|
190
|
+
newSizes[nextIndex] = newNextSize
|
|
191
|
+
return newSizes
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const handleMouseUp = () => {
|
|
196
|
+
setIsDragging(false)
|
|
197
|
+
document.removeEventListener("mousemove", handleMouseMove)
|
|
198
|
+
document.removeEventListener("mouseup", handleMouseUp)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
document.addEventListener("mousemove", handleMouseMove)
|
|
202
|
+
document.addEventListener("mouseup", handleMouseUp)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div
|
|
207
|
+
ref={handleRef}
|
|
208
|
+
data-panel-resize-handle
|
|
209
|
+
className={cn(
|
|
210
|
+
"relative flex items-center justify-center bg-border",
|
|
211
|
+
direction === "horizontal"
|
|
212
|
+
? "w-px cursor-col-resize hover:w-1 hover:bg-primary/50"
|
|
213
|
+
: "h-px cursor-row-resize hover:h-1 hover:bg-primary/50",
|
|
214
|
+
isDragging && (direction === "horizontal" ? "w-1 bg-primary" : "h-1 bg-primary"),
|
|
215
|
+
"transition-all",
|
|
216
|
+
className
|
|
217
|
+
)}
|
|
218
|
+
onMouseDown={handleMouseDown}
|
|
219
|
+
{...props}
|
|
220
|
+
>
|
|
221
|
+
{withHandle && (
|
|
222
|
+
<div
|
|
223
|
+
className={cn(
|
|
224
|
+
"z-10 flex items-center justify-center rounded-sm border bg-border",
|
|
225
|
+
direction === "horizontal" ? "h-4 w-3" : "h-3 w-4"
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
<svg
|
|
229
|
+
className={cn(
|
|
230
|
+
"h-2.5 w-2.5 text-muted-foreground",
|
|
231
|
+
direction === "vertical" && "rotate-90"
|
|
232
|
+
)}
|
|
233
|
+
viewBox="0 0 6 10"
|
|
234
|
+
fill="currentColor"
|
|
235
|
+
>
|
|
236
|
+
<circle cx="1" cy="2" r="1" />
|
|
237
|
+
<circle cx="1" cy="5" r="1" />
|
|
238
|
+
<circle cx="1" cy="8" r="1" />
|
|
239
|
+
<circle cx="5" cy="2" r="1" />
|
|
240
|
+
<circle cx="5" cy="5" r="1" />
|
|
241
|
+
<circle cx="5" cy="8" r="1" />
|
|
242
|
+
</svg>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
ResizableHandle.displayName = "ResizableHandle"
|
|
250
|
+
|
|
251
|
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|