@srcroot/ui 0.0.49 → 0.0.52
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 +24 -16
- package/package.json +2 -1
- package/src/registry/themes/v3/glass.css +154 -0
- package/src/registry/themes/v4/glass.css +181 -0
- package/src/registry/ui/alert-dialog.tsx +15 -10
- package/src/registry/ui/breadcrumb.tsx +4 -8
- package/src/registry/ui/button.tsx +7 -14
- package/src/registry/ui/collapsible.tsx +4 -9
- package/src/registry/ui/dialog.tsx +13 -10
- package/src/registry/ui/drawer.tsx +34 -10
- package/src/registry/ui/dropdown-menu.tsx +75 -31
- package/src/registry/ui/hover-card.tsx +92 -34
- package/src/registry/ui/popover.tsx +108 -20
- package/src/registry/ui/select.tsx +139 -39
- package/src/registry/ui/sheet.tsx +27 -10
- package/src/registry/ui/sidebar.tsx +28 -78
- package/src/registry/ui/slot.tsx +69 -0
- package/src/registry/ui/tooltip.tsx +15 -14
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
+
import { createPortal } from "react-dom"
|
|
2
3
|
import { cn } from "@/lib/utils"
|
|
4
|
+
import { Slot } from "@/components/ui/slot"
|
|
3
5
|
|
|
4
6
|
interface PopoverContextValue {
|
|
5
7
|
open: boolean
|
|
6
8
|
onOpenChange: (open: boolean) => void
|
|
9
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
const PopoverContext = React.createContext<PopoverContextValue | null>(null)
|
|
@@ -30,12 +33,13 @@ interface PopoverProps {
|
|
|
30
33
|
*/
|
|
31
34
|
function Popover({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: PopoverProps) {
|
|
32
35
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
36
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
33
37
|
|
|
34
38
|
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
35
39
|
const setOpen = onOpenChange || setUncontrolledOpen
|
|
36
40
|
|
|
37
41
|
return (
|
|
38
|
-
<PopoverContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
42
|
+
<PopoverContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
|
|
39
43
|
<div className="relative inline-block">
|
|
40
44
|
{children}
|
|
41
45
|
</div>
|
|
@@ -57,25 +61,25 @@ const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(
|
|
|
57
61
|
context.onOpenChange(!context.open)
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
ref,
|
|
66
|
-
})
|
|
64
|
+
// Combine refs
|
|
65
|
+
const combinedRef = (node: HTMLButtonElement | null) => {
|
|
66
|
+
(context.triggerRef as any).current = node
|
|
67
|
+
if (typeof ref === 'function') ref(node)
|
|
68
|
+
else if (ref) ref.current = node
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
const Comp = asChild ? Slot : "button"
|
|
72
|
+
|
|
69
73
|
return (
|
|
70
|
-
<
|
|
71
|
-
ref={
|
|
74
|
+
<Comp
|
|
75
|
+
ref={combinedRef}
|
|
72
76
|
aria-expanded={context.open}
|
|
73
77
|
aria-haspopup={true}
|
|
74
78
|
onClick={handleClick}
|
|
75
79
|
{...props}
|
|
76
80
|
>
|
|
77
81
|
{children}
|
|
78
|
-
</
|
|
82
|
+
</Comp>
|
|
79
83
|
)
|
|
80
84
|
}
|
|
81
85
|
)
|
|
@@ -83,14 +87,30 @@ PopoverTrigger.displayName = "PopoverTrigger"
|
|
|
83
87
|
|
|
84
88
|
const PopoverContent = React.forwardRef<
|
|
85
89
|
HTMLDivElement,
|
|
86
|
-
React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "center" | "end" }
|
|
87
|
-
>(({ className, children, align = "center", ...props }, ref) => {
|
|
90
|
+
React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "center" | "end"; sideOffset?: number; portal?: boolean }
|
|
91
|
+
>(({ className, children, align = "center", sideOffset = 4, portal = true, ...props }, ref) => {
|
|
88
92
|
const context = React.useContext(PopoverContext)
|
|
89
93
|
if (!context) throw new Error("PopoverContent must be used within Popover")
|
|
94
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
95
|
+
const [position, setPosition] = React.useState({ top: 0, left: 0 })
|
|
96
|
+
|
|
97
|
+
// Reset scroll on mount/unmount if needed, but mainly we just need a portal container
|
|
98
|
+
const [mounted, setMounted] = React.useState(false)
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
setMounted(true)
|
|
101
|
+
}, [])
|
|
90
102
|
|
|
91
103
|
React.useEffect(() => {
|
|
92
|
-
const handleClickOutside = () => {
|
|
104
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
93
105
|
if (context.open) {
|
|
106
|
+
const target = e.target as Node
|
|
107
|
+
const content = contentRef.current
|
|
108
|
+
const trigger = context.triggerRef.current
|
|
109
|
+
|
|
110
|
+
// Don't close if clicking inside content or trigger
|
|
111
|
+
if (content?.contains(target) || (trigger && trigger.contains(target))) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
94
114
|
context.onOpenChange(false)
|
|
95
115
|
}
|
|
96
116
|
}
|
|
@@ -101,35 +121,103 @@ const PopoverContent = React.forwardRef<
|
|
|
101
121
|
}
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
const checkPosition = () => {
|
|
125
|
+
if (context.open && contentRef.current && context.triggerRef.current) {
|
|
126
|
+
const triggerRect = context.triggerRef.current.getBoundingClientRect()
|
|
127
|
+
const contentRect = contentRef.current.getBoundingClientRect()
|
|
128
|
+
const viewportHeight = window.innerHeight
|
|
129
|
+
const viewportWidth = window.innerWidth
|
|
130
|
+
|
|
131
|
+
let top = 0
|
|
132
|
+
let left = 0
|
|
133
|
+
|
|
134
|
+
// Vertical
|
|
135
|
+
const spaceBelow = viewportHeight - triggerRect.bottom
|
|
136
|
+
const spaceAbove = triggerRect.top
|
|
137
|
+
const neededHeight = contentRect.height + sideOffset
|
|
138
|
+
const onBottom = spaceBelow >= neededHeight || spaceBelow > spaceAbove
|
|
139
|
+
|
|
140
|
+
if (onBottom) {
|
|
141
|
+
top = triggerRect.bottom + sideOffset
|
|
142
|
+
} else {
|
|
143
|
+
top = triggerRect.top - contentRect.height - sideOffset
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Horizontal (Alignment)
|
|
147
|
+
if (align === 'start') {
|
|
148
|
+
left = triggerRect.left
|
|
149
|
+
} else if (align === 'end') {
|
|
150
|
+
left = triggerRect.right - contentRect.width
|
|
151
|
+
} else {
|
|
152
|
+
// center
|
|
153
|
+
left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clamping
|
|
157
|
+
if (left < 4) left = 4
|
|
158
|
+
if (left + contentRect.width > viewportWidth - 4) {
|
|
159
|
+
left = viewportWidth - contentRect.width - 4
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setPosition({ top, left })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (context.open) {
|
|
167
|
+
requestAnimationFrame(checkPosition)
|
|
168
|
+
}
|
|
169
|
+
|
|
105
170
|
const timer = setTimeout(() => {
|
|
106
171
|
document.addEventListener("click", handleClickOutside)
|
|
107
172
|
}, 0)
|
|
108
173
|
document.addEventListener("keydown", handleEscape)
|
|
174
|
+
window.addEventListener("resize", checkPosition)
|
|
175
|
+
window.addEventListener("scroll", checkPosition, true)
|
|
109
176
|
|
|
110
177
|
return () => {
|
|
111
178
|
clearTimeout(timer)
|
|
112
179
|
document.removeEventListener("click", handleClickOutside)
|
|
113
180
|
document.removeEventListener("keydown", handleEscape)
|
|
181
|
+
window.removeEventListener("resize", checkPosition)
|
|
182
|
+
window.removeEventListener("scroll", checkPosition, true)
|
|
114
183
|
}
|
|
115
|
-
}, [context.open, context])
|
|
184
|
+
}, [context.open, context, align, sideOffset])
|
|
116
185
|
|
|
117
186
|
if (!context.open) return null
|
|
187
|
+
if (portal && !mounted) return null
|
|
118
188
|
|
|
119
|
-
|
|
189
|
+
const content = (
|
|
120
190
|
<div
|
|
121
|
-
ref={
|
|
191
|
+
ref={(node) => {
|
|
192
|
+
(contentRef as any).current = node
|
|
193
|
+
if (typeof ref === 'function') ref(node)
|
|
194
|
+
else if (ref) ref.current = node
|
|
195
|
+
}}
|
|
122
196
|
className={cn(
|
|
123
|
-
"
|
|
124
|
-
|
|
197
|
+
"z-50 min-w-[10rem] rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
|
|
198
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
199
|
+
!portal && "absolute",
|
|
200
|
+
!portal && "mt-2",
|
|
201
|
+
portal && "fixed",
|
|
125
202
|
className
|
|
126
203
|
)}
|
|
204
|
+
style={{
|
|
205
|
+
top: portal ? position.top : undefined,
|
|
206
|
+
left: portal ? position.left : undefined,
|
|
207
|
+
...props.style
|
|
208
|
+
}}
|
|
127
209
|
onClick={(e) => e.stopPropagation()}
|
|
128
210
|
{...props}
|
|
129
211
|
>
|
|
130
212
|
{children}
|
|
131
213
|
</div>
|
|
132
214
|
)
|
|
215
|
+
|
|
216
|
+
if (portal) {
|
|
217
|
+
return createPortal(content, document.body)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return content
|
|
133
221
|
})
|
|
134
222
|
PopoverContent.displayName = "PopoverContent"
|
|
135
223
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
+
import { createPortal } from "react-dom"
|
|
2
3
|
import { cn } from "@/lib/utils"
|
|
3
4
|
|
|
4
5
|
interface SelectContextValue {
|
|
@@ -6,6 +7,7 @@ interface SelectContextValue {
|
|
|
6
7
|
onValueChange: (value: string) => void
|
|
7
8
|
open: boolean
|
|
8
9
|
setOpen: (open: boolean) => void
|
|
10
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const SelectContext = React.createContext<SelectContextValue | null>(null)
|
|
@@ -34,12 +36,13 @@ interface SelectProps {
|
|
|
34
36
|
function Select({ children, value: controlledValue, onValueChange, defaultValue = "" }: SelectProps) {
|
|
35
37
|
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
|
|
36
38
|
const [open, setOpen] = React.useState(false)
|
|
39
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
37
40
|
|
|
38
41
|
const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
|
|
39
42
|
const setValue = onValueChange || setUncontrolledValue
|
|
40
43
|
|
|
41
44
|
return (
|
|
42
|
-
<SelectContext.Provider value={{ value, onValueChange: setValue, open, setOpen }}>
|
|
45
|
+
<SelectContext.Provider value={{ value, onValueChange: setValue, open, setOpen, triggerRef }}>
|
|
43
46
|
<div className="relative">
|
|
44
47
|
{children}
|
|
45
48
|
</div>
|
|
@@ -54,9 +57,16 @@ const SelectTrigger = React.forwardRef<
|
|
|
54
57
|
const context = React.useContext(SelectContext)
|
|
55
58
|
if (!context) throw new Error("SelectTrigger must be used within Select")
|
|
56
59
|
|
|
60
|
+
// Combine refs
|
|
61
|
+
const combinedRef = (node: HTMLButtonElement | null) => {
|
|
62
|
+
(context.triggerRef as any).current = node
|
|
63
|
+
if (typeof ref === 'function') ref(node)
|
|
64
|
+
else if (ref) ref.current = node
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
return (
|
|
58
68
|
<button
|
|
59
|
-
ref={
|
|
69
|
+
ref={combinedRef}
|
|
60
70
|
type="button"
|
|
61
71
|
role="combobox"
|
|
62
72
|
aria-expanded={context.open}
|
|
@@ -98,52 +108,142 @@ function SelectValue({ placeholder }: SelectValueProps) {
|
|
|
98
108
|
)
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
112
|
+
sideOffset?: number
|
|
113
|
+
portal?: boolean
|
|
114
|
+
align?: "start" | "center" | "end"
|
|
115
|
+
side?: "top" | "bottom"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(
|
|
119
|
+
({ className, children, sideOffset = 4, portal = true, align = "start", side = "bottom", ...props }, ref) => {
|
|
120
|
+
const context = React.useContext(SelectContext)
|
|
121
|
+
if (!context) throw new Error("SelectContent must be used within Select")
|
|
122
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
123
|
+
const [position, setPosition] = React.useState({ top: 0, left: 0 })
|
|
124
|
+
const [mounted, setMounted] = React.useState(false)
|
|
125
|
+
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
setMounted(true)
|
|
128
|
+
}, [])
|
|
129
|
+
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
132
|
+
if (context.open) {
|
|
133
|
+
const target = e.target as Node
|
|
134
|
+
const content = contentRef.current
|
|
135
|
+
const trigger = context.triggerRef.current
|
|
136
|
+
|
|
137
|
+
if (content?.contains(target) || (trigger && trigger.contains(target))) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
context.setOpen(false)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
145
|
+
if (e.key === "Escape" && context.open) {
|
|
146
|
+
context.setOpen(false)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const checkPosition = () => {
|
|
151
|
+
if (context.open && contentRef.current && context.triggerRef.current) {
|
|
152
|
+
const triggerRect = context.triggerRef.current.getBoundingClientRect()
|
|
153
|
+
const contentRect = contentRef.current.getBoundingClientRect()
|
|
154
|
+
const viewportHeight = window.innerHeight
|
|
155
|
+
const viewportWidth = window.innerWidth
|
|
156
|
+
|
|
157
|
+
let top = 0
|
|
158
|
+
let left = 0
|
|
159
|
+
|
|
160
|
+
// Vertical
|
|
161
|
+
const spaceBelow = viewportHeight - triggerRect.bottom
|
|
162
|
+
const spaceAbove = triggerRect.top
|
|
163
|
+
const neededHeight = contentRect.height + sideOffset
|
|
164
|
+
const onBottom = spaceBelow >= neededHeight || spaceBelow > spaceAbove
|
|
165
|
+
|
|
166
|
+
if (onBottom) {
|
|
167
|
+
top = triggerRect.bottom + sideOffset
|
|
168
|
+
} else {
|
|
169
|
+
top = triggerRect.top - contentRect.height - sideOffset
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Horizontal - Select usually matches trigger width or starts aligned
|
|
173
|
+
left = triggerRect.left
|
|
174
|
+
|
|
175
|
+
// Clamping
|
|
176
|
+
if (left < 4) left = 4
|
|
177
|
+
if (left + contentRect.width > viewportWidth - 4) {
|
|
178
|
+
left = viewportWidth - contentRect.width - 4
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Match width if it fits, or at least min-width of trigger
|
|
182
|
+
// For now keeping simple positioning
|
|
183
|
+
|
|
184
|
+
setPosition({ top, left })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
107
187
|
|
|
108
|
-
React.useEffect(() => {
|
|
109
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
110
188
|
if (context.open) {
|
|
111
|
-
|
|
189
|
+
requestAnimationFrame(checkPosition)
|
|
112
190
|
}
|
|
113
|
-
}
|
|
114
191
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
document.addEventListener("mousedown", handleClickOutside)
|
|
194
|
+
}, 0)
|
|
195
|
+
document.addEventListener("keydown", handleEscape)
|
|
196
|
+
window.addEventListener("resize", checkPosition)
|
|
197
|
+
window.addEventListener("scroll", checkPosition, true)
|
|
198
|
+
|
|
199
|
+
return () => {
|
|
200
|
+
clearTimeout(timer)
|
|
201
|
+
document.removeEventListener("mousedown", handleClickOutside)
|
|
202
|
+
document.removeEventListener("keydown", handleEscape)
|
|
203
|
+
window.removeEventListener("resize", checkPosition)
|
|
204
|
+
window.removeEventListener("scroll", checkPosition, true)
|
|
118
205
|
}
|
|
119
|
-
}
|
|
206
|
+
}, [context.open, context, sideOffset])
|
|
120
207
|
|
|
121
|
-
|
|
122
|
-
|
|
208
|
+
if (!context.open) return null
|
|
209
|
+
if (portal && !mounted) return null
|
|
123
210
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
211
|
+
const content = (
|
|
212
|
+
<div
|
|
213
|
+
ref={(node) => {
|
|
214
|
+
(contentRef as any).current = node
|
|
215
|
+
if (typeof ref === 'function') ref(node)
|
|
216
|
+
else if (ref) ref.current = node
|
|
217
|
+
}}
|
|
218
|
+
role="listbox"
|
|
219
|
+
className={cn(
|
|
220
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
221
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
222
|
+
!portal && "absolute",
|
|
223
|
+
!portal && "mt-1",
|
|
224
|
+
portal && "fixed",
|
|
225
|
+
className
|
|
226
|
+
)}
|
|
227
|
+
style={{
|
|
228
|
+
top: portal ? position.top : undefined,
|
|
229
|
+
left: portal ? position.left : undefined,
|
|
230
|
+
width: context.triggerRef.current ? context.triggerRef.current.offsetWidth : undefined,
|
|
231
|
+
...props.style
|
|
232
|
+
}}
|
|
233
|
+
onClick={(e) => e.stopPropagation()}
|
|
234
|
+
{...props}
|
|
235
|
+
>
|
|
236
|
+
{children}
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
129
239
|
|
|
130
|
-
|
|
240
|
+
if (portal) {
|
|
241
|
+
return createPortal(content, document.body)
|
|
242
|
+
}
|
|
131
243
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
role="listbox"
|
|
136
|
-
className={cn(
|
|
137
|
-
"absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
138
|
-
className
|
|
139
|
-
)}
|
|
140
|
-
onClick={(e) => e.stopPropagation()}
|
|
141
|
-
{...props}
|
|
142
|
-
>
|
|
143
|
-
{children}
|
|
144
|
-
</div>
|
|
145
|
-
)
|
|
146
|
-
})
|
|
244
|
+
return content
|
|
245
|
+
}
|
|
246
|
+
)
|
|
147
247
|
SelectContent.displayName = "SelectContent"
|
|
148
248
|
|
|
149
249
|
interface SelectItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import * as React from "react"
|
|
3
|
+
import { createPortal } from "react-dom"
|
|
3
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
5
|
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Slot } from "@/components/ui/slot"
|
|
5
7
|
|
|
6
8
|
interface SheetContextValue {
|
|
7
9
|
open: boolean
|
|
@@ -19,6 +21,18 @@ interface SheetProps {
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Sheet (slide-in panel) component with smooth animations
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <Sheet>
|
|
27
|
+
* <SheetTrigger>Open</SheetTrigger>
|
|
28
|
+
* <SheetContent>
|
|
29
|
+
* <SheetHeader>
|
|
30
|
+
* <SheetTitle>Edit profile</SheetTitle>
|
|
31
|
+
* <SheetDescription>Make changes to your profile here.</SheetDescription>
|
|
32
|
+
* </SheetHeader>
|
|
33
|
+
* <div>Content</div>
|
|
34
|
+
* </SheetContent>
|
|
35
|
+
* </Sheet>
|
|
22
36
|
*/
|
|
23
37
|
function Sheet({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: SheetProps) {
|
|
24
38
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
@@ -47,17 +61,12 @@ const SheetTrigger = React.forwardRef<HTMLButtonElement, SheetTriggerProps>(
|
|
|
47
61
|
context.onOpenChange(true)
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
return React.cloneElement(children as React.ReactElement<any>, {
|
|
52
|
-
onClick: handleClick,
|
|
53
|
-
ref,
|
|
54
|
-
})
|
|
55
|
-
}
|
|
64
|
+
const Comp = asChild ? Slot : "button"
|
|
56
65
|
|
|
57
66
|
return (
|
|
58
|
-
<
|
|
67
|
+
<Comp ref={ref} onClick={handleClick} {...props}>
|
|
59
68
|
{children}
|
|
60
|
-
</
|
|
69
|
+
</Comp>
|
|
61
70
|
)
|
|
62
71
|
}
|
|
63
72
|
)
|
|
@@ -150,11 +159,18 @@ const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
|
|
|
150
159
|
}
|
|
151
160
|
}, [context.open, context])
|
|
152
161
|
|
|
162
|
+
const [mounted, setMounted] = React.useState(false)
|
|
163
|
+
|
|
164
|
+
React.useEffect(() => {
|
|
165
|
+
setMounted(true)
|
|
166
|
+
}, [])
|
|
167
|
+
|
|
153
168
|
if (!isVisible) return null
|
|
169
|
+
if (!mounted) return null
|
|
154
170
|
|
|
155
171
|
const sideKey = side || "right"
|
|
156
172
|
|
|
157
|
-
return (
|
|
173
|
+
return createPortal(
|
|
158
174
|
<>
|
|
159
175
|
{/* Overlay with fade animation */}
|
|
160
176
|
<div
|
|
@@ -190,7 +206,8 @@ const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
|
|
|
190
206
|
<span className="sr-only">Close</span>
|
|
191
207
|
</button>
|
|
192
208
|
</div>
|
|
193
|
-
|
|
209
|
+
</>,
|
|
210
|
+
document.body
|
|
194
211
|
)
|
|
195
212
|
}
|
|
196
213
|
)
|
|
@@ -6,6 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
6
6
|
|
|
7
7
|
import { cn } from "@/lib/utils"
|
|
8
8
|
import { Button } from "@/components/ui/button"
|
|
9
|
+
import { Slot } from "@/components/ui/slot"
|
|
9
10
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
|
10
11
|
|
|
11
12
|
|
|
@@ -102,6 +103,25 @@ const SidebarProvider = React.forwardRef<
|
|
|
102
103
|
})
|
|
103
104
|
SidebarProvider.displayName = "SidebarProvider"
|
|
104
105
|
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sidebar component for application layout
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* <SidebarProvider>
|
|
112
|
+
* <Sidebar>
|
|
113
|
+
* <SidebarHeader />
|
|
114
|
+
* <SidebarContent>
|
|
115
|
+
* <SidebarGroup />
|
|
116
|
+
* <SidebarMenu />
|
|
117
|
+
* </SidebarContent>
|
|
118
|
+
* <SidebarFooter />
|
|
119
|
+
* </Sidebar>
|
|
120
|
+
* <SidebarInset>
|
|
121
|
+
* <main>Content</main>
|
|
122
|
+
* </SidebarInset>
|
|
123
|
+
* </SidebarProvider>
|
|
124
|
+
*/
|
|
105
125
|
const Sidebar = React.forwardRef<
|
|
106
126
|
HTMLDivElement,
|
|
107
127
|
React.HTMLAttributes<HTMLDivElement> & {
|
|
@@ -355,25 +375,10 @@ const SidebarGroupLabel = React.forwardRef<
|
|
|
355
375
|
HTMLDivElement,
|
|
356
376
|
React.HTMLAttributes<HTMLDivElement> & { asChild?: boolean }
|
|
357
377
|
>(({ className, asChild = false, ...props }, ref) => {
|
|
358
|
-
|
|
359
|
-
if (asChild) {
|
|
360
|
-
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
361
|
-
return React.cloneElement(child, {
|
|
362
|
-
ref,
|
|
363
|
-
"data-sidebar": "group-label",
|
|
364
|
-
className: cn(
|
|
365
|
-
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
366
|
-
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
|
367
|
-
className,
|
|
368
|
-
child.props.className
|
|
369
|
-
),
|
|
370
|
-
...props,
|
|
371
|
-
children: child.props.children,
|
|
372
|
-
})
|
|
373
|
-
}
|
|
378
|
+
const Comp = asChild ? Slot : "div"
|
|
374
379
|
|
|
375
380
|
return (
|
|
376
|
-
<
|
|
381
|
+
<Comp
|
|
377
382
|
ref={ref}
|
|
378
383
|
data-sidebar="group-label"
|
|
379
384
|
className={cn(
|
|
@@ -467,26 +472,10 @@ const SidebarMenuButton = React.forwardRef<
|
|
|
467
472
|
}
|
|
468
473
|
}
|
|
469
474
|
|
|
470
|
-
|
|
471
|
-
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
472
|
-
|
|
473
|
-
return React.cloneElement(child, {
|
|
474
|
-
ref,
|
|
475
|
-
className: cn(buttonClass, child.props.className),
|
|
476
|
-
"data-active": isActive,
|
|
477
|
-
"data-sidebar": "menu-button",
|
|
478
|
-
"data-size": size,
|
|
479
|
-
...props,
|
|
480
|
-
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
481
|
-
handleClick(e)
|
|
482
|
-
child.props.onClick?.(e)
|
|
483
|
-
},
|
|
484
|
-
children: child.props.children
|
|
485
|
-
})
|
|
486
|
-
}
|
|
475
|
+
const Comp = asChild ? Slot : "button"
|
|
487
476
|
|
|
488
477
|
return (
|
|
489
|
-
<
|
|
478
|
+
<Comp
|
|
490
479
|
ref={ref}
|
|
491
480
|
data-sidebar="menu-button"
|
|
492
481
|
data-size={size}
|
|
@@ -504,31 +493,10 @@ const SidebarMenuAction = React.forwardRef<
|
|
|
504
493
|
HTMLButtonElement,
|
|
505
494
|
React.ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean; showOnHover?: boolean }
|
|
506
495
|
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
|
507
|
-
|
|
508
|
-
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
509
|
-
return React.cloneElement(child, {
|
|
510
|
-
ref,
|
|
511
|
-
"data-sidebar": "menu-action",
|
|
512
|
-
className: cn(
|
|
513
|
-
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
|
514
|
-
// Increases the hit area of the button on mobile.
|
|
515
|
-
"after:absolute after:-inset-2 after:md:hidden",
|
|
516
|
-
"peer-data-[size=sm]/menu-button:top-1",
|
|
517
|
-
"peer-data-[size=default]/menu-button:top-1.5",
|
|
518
|
-
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
519
|
-
"group-data-[collapsible=icon]:hidden",
|
|
520
|
-
showOnHover &&
|
|
521
|
-
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
|
522
|
-
className,
|
|
523
|
-
child.props.className
|
|
524
|
-
),
|
|
525
|
-
...props,
|
|
526
|
-
children: child.props.children
|
|
527
|
-
})
|
|
528
|
-
}
|
|
496
|
+
const Comp = asChild ? Slot : "button"
|
|
529
497
|
|
|
530
498
|
return (
|
|
531
|
-
<
|
|
499
|
+
<Comp
|
|
532
500
|
ref={ref}
|
|
533
501
|
data-sidebar="menu-action"
|
|
534
502
|
className={cn(
|
|
@@ -580,28 +548,10 @@ const SidebarMenuSubButton = React.forwardRef<
|
|
|
580
548
|
isActive?: boolean
|
|
581
549
|
}
|
|
582
550
|
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
|
583
|
-
|
|
584
|
-
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
585
|
-
return React.cloneElement(child, {
|
|
586
|
-
ref,
|
|
587
|
-
"data-sidebar": "menu-sub-button",
|
|
588
|
-
"data-size": size,
|
|
589
|
-
"data-active": isActive,
|
|
590
|
-
className: cn(
|
|
591
|
-
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 group-data-[collapsible=icon]:hidden",
|
|
592
|
-
size === "sm" && "text-xs",
|
|
593
|
-
size === "md" && "text-sm",
|
|
594
|
-
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
|
595
|
-
className,
|
|
596
|
-
child.props.className
|
|
597
|
-
),
|
|
598
|
-
...props,
|
|
599
|
-
children: child.props.children
|
|
600
|
-
})
|
|
601
|
-
}
|
|
551
|
+
const Comp = asChild ? Slot : "a"
|
|
602
552
|
|
|
603
553
|
return (
|
|
604
|
-
<
|
|
554
|
+
<Comp
|
|
605
555
|
ref={ref}
|
|
606
556
|
data-sidebar="menu-sub-button"
|
|
607
557
|
data-size={size}
|