banhaten 0.1.1 → 0.1.2
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 +20 -8
- package/package.json +8 -2
- package/registry/components/autocomplete.tsx +637 -0
- package/registry/components/avatar.tsx +258 -22
- package/registry/components/badge.tsx +97 -35
- package/registry/components/date-picker-state.ts +253 -0
- package/registry/components/date-picker.tsx +115 -158
- package/registry/components/expanded/EmptyState.tsx +155 -0
- package/registry/components/expanded/emptyState.css +111 -0
- package/registry/components/expanded/slideout.css +1 -0
- package/registry/components/expanded/table.css +1 -0
- package/registry/components/input-otp.tsx +574 -0
- package/registry/components/input.tsx +21 -11
- package/registry/components/menu.tsx +371 -8
- package/registry/components/popover.tsx +840 -0
- package/registry/components/select.tsx +4 -0
- package/registry/components/skeleton.css +57 -0
- package/registry/components/skeleton.tsx +482 -0
- package/registry/components/spinner.tsx +79 -11
- package/registry/components/textarea.tsx +1 -1
- package/registry/components/tooltip.tsx +4 -0
- package/registry/examples/autocomplete-demo.tsx +109 -0
- package/registry/examples/avatar-demo.tsx +102 -47
- package/registry/examples/badge-demo.tsx +16 -0
- package/registry/examples/expanded/command-bar-demo.tsx +236 -0
- package/registry/examples/expanded/empty-state-demo.tsx +39 -0
- package/registry/examples/input-demo.tsx +1 -1
- package/registry/examples/input-otp-demo.tsx +72 -0
- package/registry/examples/menu-demo.tsx +101 -88
- package/registry/examples/popover-demo.tsx +546 -0
- package/registry/examples/select-demo.tsx +1 -1
- package/registry/examples/skeleton-demo.tsx +56 -0
- package/registry/examples/spinner-demo.tsx +23 -1
- package/registry/examples/textarea-demo.tsx +1 -1
- package/registry/index.json +240 -8
- package/registry/styles/globals.css +88 -0
- package/src/cli/index.js +997 -62
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { XIcon } from "lucide-react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
type PopoverSide = "top" | "right" | "bottom" | "left"
|
|
9
|
+
type PopoverAlign = "start" | "center" | "end"
|
|
10
|
+
type PopoverSize = "sm" | "md" | "lg" | "auto"
|
|
11
|
+
type PopoverOffset = "tight" | "default" | "loose"
|
|
12
|
+
type PopoverArrowSide = "top" | "right" | "bottom" | "left"
|
|
13
|
+
type PopoverCollisionShift = {
|
|
14
|
+
x: number
|
|
15
|
+
y: number
|
|
16
|
+
}
|
|
17
|
+
type PopoverResizeObserverConstructor = new (
|
|
18
|
+
callback: () => void
|
|
19
|
+
) => {
|
|
20
|
+
disconnect: () => void
|
|
21
|
+
observe: (target: Element) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type PopoverContextValue = {
|
|
25
|
+
contentId: string
|
|
26
|
+
modal: boolean
|
|
27
|
+
open: boolean
|
|
28
|
+
setOpen: (open: boolean) => void
|
|
29
|
+
triggerId: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PopoverContext = React.createContext<PopoverContextValue | null>(null)
|
|
33
|
+
const zeroPopoverCollisionShift: PopoverCollisionShift = { x: 0, y: 0 }
|
|
34
|
+
// Viewport collision padding mirrors --bh-space-md-8.
|
|
35
|
+
const POPOVER_COLLISION_PADDING_PX = 8
|
|
36
|
+
const useSafeLayoutEffect =
|
|
37
|
+
typeof window === "undefined" ? React.useEffect : React.useLayoutEffect
|
|
38
|
+
|
|
39
|
+
const popoverContentVariants = cva(
|
|
40
|
+
[
|
|
41
|
+
"absolute z-[var(--bh-z-popover)] grid max-w-[var(--bh-popover-max-width)] gap-[var(--bh-popover-content-gap)]",
|
|
42
|
+
"rounded-[var(--bh-popover-radius)] bg-[var(--bh-popover-bg)] p-[var(--bh-popover-padding)]",
|
|
43
|
+
"text-start text-[var(--bh-content-default)] shadow-[var(--shadow-popover)] outline-none",
|
|
44
|
+
"font-[var(--bh-font-family)] tracking-[var(--bh-text-base-letter-spacing)]",
|
|
45
|
+
"data-[state=closed]:pointer-events-none data-[state=closed]:opacity-[var(--bh-opacity-0)]",
|
|
46
|
+
],
|
|
47
|
+
{
|
|
48
|
+
variants: {
|
|
49
|
+
offset: {
|
|
50
|
+
tight: "[--bh-popover-offset:var(--bh-popover-offset-tight)]",
|
|
51
|
+
default: "[--bh-popover-offset:var(--bh-popover-offset-default)]",
|
|
52
|
+
loose: "[--bh-popover-offset:var(--bh-popover-offset-loose)]",
|
|
53
|
+
},
|
|
54
|
+
size: {
|
|
55
|
+
sm: "w-[var(--bh-popover-sm-width)]",
|
|
56
|
+
md: "w-[var(--bh-popover-md-width)]",
|
|
57
|
+
lg: "w-[var(--bh-popover-lg-width)]",
|
|
58
|
+
auto: "w-max",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: {
|
|
62
|
+
offset: "default",
|
|
63
|
+
size: "md",
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
type PopoverProps = React.ComponentProps<"div"> & {
|
|
69
|
+
defaultOpen?: boolean
|
|
70
|
+
modal?: boolean
|
|
71
|
+
onOpenChange?: (open: boolean) => void
|
|
72
|
+
open?: boolean
|
|
73
|
+
openOnHover?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type PopoverAnchorProps = React.ComponentProps<"span"> & {
|
|
77
|
+
asChild?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type PopoverTriggerProps = React.ComponentProps<"button"> & {
|
|
81
|
+
asChild?: boolean
|
|
82
|
+
openOnFocus?: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type PopoverContentProps = React.ComponentProps<"div"> &
|
|
86
|
+
VariantProps<typeof popoverContentVariants> & {
|
|
87
|
+
align?: PopoverAlign
|
|
88
|
+
avoidCollisions?: boolean
|
|
89
|
+
collisionPadding?: number
|
|
90
|
+
forceMount?: boolean
|
|
91
|
+
showArrow?: boolean
|
|
92
|
+
side?: PopoverSide
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type PopoverHeaderProps = React.ComponentProps<"div">
|
|
96
|
+
type PopoverBodyProps = React.ComponentProps<"div">
|
|
97
|
+
type PopoverTitleProps = React.ComponentProps<"h3">
|
|
98
|
+
type PopoverDescriptionProps = React.ComponentProps<"p">
|
|
99
|
+
type PopoverFooterProps = React.ComponentProps<"div">
|
|
100
|
+
type PopoverCloseProps = React.ComponentProps<"button"> & {
|
|
101
|
+
asChild?: boolean
|
|
102
|
+
label?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(function Popover(
|
|
106
|
+
{
|
|
107
|
+
children,
|
|
108
|
+
className,
|
|
109
|
+
defaultOpen = false,
|
|
110
|
+
modal = false,
|
|
111
|
+
onOpenChange,
|
|
112
|
+
onPointerEnter,
|
|
113
|
+
onPointerLeave,
|
|
114
|
+
open,
|
|
115
|
+
openOnHover = false,
|
|
116
|
+
...props
|
|
117
|
+
},
|
|
118
|
+
ref
|
|
119
|
+
) {
|
|
120
|
+
const rootRef = React.useRef<HTMLDivElement | null>(null)
|
|
121
|
+
const contentId = React.useId()
|
|
122
|
+
const triggerId = React.useId()
|
|
123
|
+
const [isOpen, setIsOpen] = useControllablePopoverState({
|
|
124
|
+
defaultOpen,
|
|
125
|
+
onOpenChange,
|
|
126
|
+
open,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
if (!isOpen) return
|
|
131
|
+
|
|
132
|
+
const ownerDocument = rootRef.current?.ownerDocument ?? document
|
|
133
|
+
|
|
134
|
+
function handlePointerDown(event: PointerEvent) {
|
|
135
|
+
const root = rootRef.current
|
|
136
|
+
if (!root || root.contains(event.target as Node)) return
|
|
137
|
+
setIsOpen(false)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
141
|
+
if (event.key === "Escape") {
|
|
142
|
+
setIsOpen(false)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ownerDocument.addEventListener("pointerdown", handlePointerDown)
|
|
147
|
+
ownerDocument.addEventListener("keydown", handleKeyDown)
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
ownerDocument.removeEventListener("pointerdown", handlePointerDown)
|
|
151
|
+
ownerDocument.removeEventListener("keydown", handleKeyDown)
|
|
152
|
+
}
|
|
153
|
+
}, [isOpen, setIsOpen])
|
|
154
|
+
|
|
155
|
+
function setRootRef(node: HTMLDivElement | null) {
|
|
156
|
+
rootRef.current = node
|
|
157
|
+
|
|
158
|
+
if (typeof ref === "function") {
|
|
159
|
+
ref(node)
|
|
160
|
+
} else if (ref) {
|
|
161
|
+
ref.current = node
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<PopoverContext.Provider
|
|
167
|
+
value={{
|
|
168
|
+
contentId,
|
|
169
|
+
modal,
|
|
170
|
+
open: isOpen,
|
|
171
|
+
setOpen: setIsOpen,
|
|
172
|
+
triggerId,
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<div
|
|
176
|
+
data-slot="popover"
|
|
177
|
+
data-state={isOpen ? "open" : "closed"}
|
|
178
|
+
ref={setRootRef}
|
|
179
|
+
className={cn("relative inline-flex max-w-full", className)}
|
|
180
|
+
onPointerEnter={(event) => {
|
|
181
|
+
onPointerEnter?.(event)
|
|
182
|
+
if (openOnHover && !event.defaultPrevented) {
|
|
183
|
+
setIsOpen(true)
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
onPointerLeave={(event) => {
|
|
187
|
+
onPointerLeave?.(event)
|
|
188
|
+
if (openOnHover && !event.defaultPrevented) {
|
|
189
|
+
setIsOpen(false)
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
192
|
+
{...props}
|
|
193
|
+
>
|
|
194
|
+
{children}
|
|
195
|
+
</div>
|
|
196
|
+
</PopoverContext.Provider>
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
function PopoverAnchor({
|
|
201
|
+
asChild = false,
|
|
202
|
+
className,
|
|
203
|
+
...props
|
|
204
|
+
}: PopoverAnchorProps) {
|
|
205
|
+
const Comp = asChild ? Slot : "span"
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Comp
|
|
209
|
+
data-slot="popover-anchor"
|
|
210
|
+
className={cn("relative inline-flex max-w-full", className)}
|
|
211
|
+
{...props}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(
|
|
217
|
+
function PopoverTrigger(
|
|
218
|
+
{
|
|
219
|
+
asChild = false,
|
|
220
|
+
className,
|
|
221
|
+
disabled,
|
|
222
|
+
onClick,
|
|
223
|
+
onFocus,
|
|
224
|
+
onKeyDown,
|
|
225
|
+
openOnFocus = false,
|
|
226
|
+
type,
|
|
227
|
+
...props
|
|
228
|
+
},
|
|
229
|
+
ref
|
|
230
|
+
) {
|
|
231
|
+
const context = usePopoverContext("PopoverTrigger")
|
|
232
|
+
const Comp = asChild ? Slot : "button"
|
|
233
|
+
const isDisabled = Boolean(disabled)
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<Comp
|
|
237
|
+
aria-controls={context.open ? context.contentId : undefined}
|
|
238
|
+
aria-expanded={context.open}
|
|
239
|
+
aria-haspopup="dialog"
|
|
240
|
+
data-slot="popover-trigger"
|
|
241
|
+
data-state={context.open ? "open" : "closed"}
|
|
242
|
+
disabled={isDisabled}
|
|
243
|
+
id={context.triggerId}
|
|
244
|
+
ref={ref}
|
|
245
|
+
type={asChild ? undefined : type || "button"}
|
|
246
|
+
className={cn(
|
|
247
|
+
"inline-flex min-w-0 items-center justify-center gap-[var(--bh-popover-trigger-gap)]",
|
|
248
|
+
"rounded-[var(--bh-control-default)] text-start outline-none transition-[background-color,border-color,box-shadow]",
|
|
249
|
+
"focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
|
|
250
|
+
className
|
|
251
|
+
)}
|
|
252
|
+
onClick={(event) => {
|
|
253
|
+
onClick?.(event)
|
|
254
|
+
if (event.defaultPrevented || isDisabled) return
|
|
255
|
+
context.setOpen(!context.open)
|
|
256
|
+
}}
|
|
257
|
+
onFocus={(event) => {
|
|
258
|
+
onFocus?.(event)
|
|
259
|
+
if (openOnFocus && !event.defaultPrevented && !isDisabled) {
|
|
260
|
+
context.setOpen(true)
|
|
261
|
+
}
|
|
262
|
+
}}
|
|
263
|
+
onKeyDown={(event) => {
|
|
264
|
+
onKeyDown?.(event)
|
|
265
|
+
if (event.defaultPrevented || isDisabled) return
|
|
266
|
+
|
|
267
|
+
if (event.key === "ArrowDown") {
|
|
268
|
+
event.preventDefault()
|
|
269
|
+
context.setOpen(true)
|
|
270
|
+
}
|
|
271
|
+
}}
|
|
272
|
+
{...props}
|
|
273
|
+
/>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(
|
|
279
|
+
function PopoverContent(
|
|
280
|
+
{
|
|
281
|
+
align = "start",
|
|
282
|
+
avoidCollisions = true,
|
|
283
|
+
children,
|
|
284
|
+
className,
|
|
285
|
+
collisionPadding = POPOVER_COLLISION_PADDING_PX,
|
|
286
|
+
forceMount = false,
|
|
287
|
+
offset,
|
|
288
|
+
role = "dialog",
|
|
289
|
+
showArrow = true,
|
|
290
|
+
side = "bottom",
|
|
291
|
+
size,
|
|
292
|
+
style,
|
|
293
|
+
...props
|
|
294
|
+
},
|
|
295
|
+
ref
|
|
296
|
+
) {
|
|
297
|
+
const context = usePopoverContext("PopoverContent")
|
|
298
|
+
const isOpen = context.open
|
|
299
|
+
const contentRef = React.useRef<HTMLDivElement | null>(null)
|
|
300
|
+
const collisionShiftRef = React.useRef(zeroPopoverCollisionShift)
|
|
301
|
+
const [collisionShift, setCollisionShift] = React.useState(
|
|
302
|
+
zeroPopoverCollisionShift
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
function updateCollisionShift(nextShift: PopoverCollisionShift) {
|
|
306
|
+
collisionShiftRef.current = nextShift
|
|
307
|
+
setCollisionShift((currentShift) =>
|
|
308
|
+
isSamePopoverCollisionShift(currentShift, nextShift)
|
|
309
|
+
? currentShift
|
|
310
|
+
: nextShift
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function setContentRef(node: HTMLDivElement | null) {
|
|
315
|
+
contentRef.current = node
|
|
316
|
+
|
|
317
|
+
if (typeof ref === "function") {
|
|
318
|
+
ref(node)
|
|
319
|
+
} else if (ref) {
|
|
320
|
+
ref.current = node
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
useSafeLayoutEffect(() => {
|
|
325
|
+
if (!isOpen || !avoidCollisions) {
|
|
326
|
+
updateCollisionShift(zeroPopoverCollisionShift)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const node = contentRef.current
|
|
331
|
+
if (!node) return
|
|
332
|
+
const contentNode: HTMLDivElement = node
|
|
333
|
+
|
|
334
|
+
const ownerWindow = contentNode.ownerDocument.defaultView
|
|
335
|
+
if (!ownerWindow) return
|
|
336
|
+
const contentWindow: Window = ownerWindow
|
|
337
|
+
|
|
338
|
+
let animationFrame = 0
|
|
339
|
+
const resolvedCollisionPadding = Math.max(0, collisionPadding)
|
|
340
|
+
const requestFrame =
|
|
341
|
+
typeof contentWindow.requestAnimationFrame === "function"
|
|
342
|
+
? contentWindow.requestAnimationFrame.bind(contentWindow)
|
|
343
|
+
: (callback: FrameRequestCallback) =>
|
|
344
|
+
contentWindow.setTimeout(
|
|
345
|
+
() => callback(contentWindow.performance.now()),
|
|
346
|
+
0
|
|
347
|
+
)
|
|
348
|
+
const cancelFrame =
|
|
349
|
+
typeof contentWindow.cancelAnimationFrame === "function"
|
|
350
|
+
? contentWindow.cancelAnimationFrame.bind(contentWindow)
|
|
351
|
+
: contentWindow.clearTimeout.bind(contentWindow)
|
|
352
|
+
|
|
353
|
+
function measureCollisionShift() {
|
|
354
|
+
animationFrame = 0
|
|
355
|
+
|
|
356
|
+
const rect = contentNode.getBoundingClientRect()
|
|
357
|
+
const currentShift = collisionShiftRef.current
|
|
358
|
+
const unshiftedRect = {
|
|
359
|
+
bottom: rect.bottom - currentShift.y,
|
|
360
|
+
left: rect.left - currentShift.x,
|
|
361
|
+
right: rect.right - currentShift.x,
|
|
362
|
+
top: rect.top - currentShift.y,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
updateCollisionShift(
|
|
366
|
+
getPopoverCollisionShift({
|
|
367
|
+
collisionPadding: resolvedCollisionPadding,
|
|
368
|
+
rect: unshiftedRect,
|
|
369
|
+
viewportHeight: contentWindow.innerHeight,
|
|
370
|
+
viewportWidth: contentWindow.innerWidth,
|
|
371
|
+
})
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function queueCollisionShiftMeasure() {
|
|
376
|
+
if (animationFrame) {
|
|
377
|
+
cancelFrame(animationFrame)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
animationFrame = requestFrame(measureCollisionShift)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
queueCollisionShiftMeasure()
|
|
384
|
+
contentWindow.addEventListener("resize", queueCollisionShiftMeasure)
|
|
385
|
+
contentWindow.addEventListener("scroll", queueCollisionShiftMeasure, true)
|
|
386
|
+
|
|
387
|
+
const ResizeObserver = (
|
|
388
|
+
contentWindow as Window & {
|
|
389
|
+
ResizeObserver?: PopoverResizeObserverConstructor
|
|
390
|
+
}
|
|
391
|
+
).ResizeObserver
|
|
392
|
+
const resizeObserver = ResizeObserver
|
|
393
|
+
? new ResizeObserver(queueCollisionShiftMeasure)
|
|
394
|
+
: undefined
|
|
395
|
+
resizeObserver?.observe(contentNode)
|
|
396
|
+
|
|
397
|
+
return () => {
|
|
398
|
+
if (animationFrame) {
|
|
399
|
+
cancelFrame(animationFrame)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
contentWindow.removeEventListener("resize", queueCollisionShiftMeasure)
|
|
403
|
+
contentWindow.removeEventListener("scroll", queueCollisionShiftMeasure, true)
|
|
404
|
+
resizeObserver?.disconnect()
|
|
405
|
+
}
|
|
406
|
+
}, [align, avoidCollisions, collisionPadding, isOpen, offset, side, size])
|
|
407
|
+
|
|
408
|
+
if (!forceMount && !isOpen) {
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<div
|
|
414
|
+
aria-modal={context.modal || undefined}
|
|
415
|
+
data-align={align}
|
|
416
|
+
data-side={side}
|
|
417
|
+
data-slot="popover-content"
|
|
418
|
+
data-state={isOpen ? "open" : "closed"}
|
|
419
|
+
hidden={!isOpen}
|
|
420
|
+
id={context.contentId}
|
|
421
|
+
ref={setContentRef}
|
|
422
|
+
role={role}
|
|
423
|
+
style={{
|
|
424
|
+
...style,
|
|
425
|
+
transform: mergePopoverTransforms(
|
|
426
|
+
getPopoverContentTransform(side, align, collisionShift),
|
|
427
|
+
style?.transform
|
|
428
|
+
),
|
|
429
|
+
}}
|
|
430
|
+
className={cn(
|
|
431
|
+
popoverContentVariants({ offset, size }),
|
|
432
|
+
popoverPositionClasses[side][align],
|
|
433
|
+
className
|
|
434
|
+
)}
|
|
435
|
+
{...props}
|
|
436
|
+
>
|
|
437
|
+
{showArrow ? <PopoverArrow align={align} side={side} /> : null}
|
|
438
|
+
{children}
|
|
439
|
+
</div>
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
function PopoverHeader({ className, ...props }: PopoverHeaderProps) {
|
|
445
|
+
return (
|
|
446
|
+
<div
|
|
447
|
+
data-slot="popover-header"
|
|
448
|
+
className={cn(
|
|
449
|
+
"flex min-w-0 items-start justify-between gap-[var(--bh-popover-header-gap)]",
|
|
450
|
+
className
|
|
451
|
+
)}
|
|
452
|
+
{...props}
|
|
453
|
+
/>
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function PopoverBody({ className, ...props }: PopoverBodyProps) {
|
|
458
|
+
return (
|
|
459
|
+
<div
|
|
460
|
+
data-slot="popover-body"
|
|
461
|
+
className={cn("grid min-w-0 gap-[var(--bh-popover-body-gap)]", className)}
|
|
462
|
+
{...props}
|
|
463
|
+
/>
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function PopoverTitle({ className, dir = "auto", ...props }: PopoverTitleProps) {
|
|
468
|
+
return (
|
|
469
|
+
<h3
|
|
470
|
+
data-slot="popover-title"
|
|
471
|
+
dir={dir}
|
|
472
|
+
className={cn(
|
|
473
|
+
"m-0 min-w-0 text-start text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)]",
|
|
474
|
+
className
|
|
475
|
+
)}
|
|
476
|
+
{...props}
|
|
477
|
+
/>
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function PopoverDescription({
|
|
482
|
+
className,
|
|
483
|
+
dir = "auto",
|
|
484
|
+
...props
|
|
485
|
+
}: PopoverDescriptionProps) {
|
|
486
|
+
return (
|
|
487
|
+
<p
|
|
488
|
+
data-slot="popover-description"
|
|
489
|
+
dir={dir}
|
|
490
|
+
className={cn(
|
|
491
|
+
"m-0 min-w-0 text-start text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]",
|
|
492
|
+
className
|
|
493
|
+
)}
|
|
494
|
+
{...props}
|
|
495
|
+
/>
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function PopoverFooter({ className, ...props }: PopoverFooterProps) {
|
|
500
|
+
return (
|
|
501
|
+
<div
|
|
502
|
+
data-slot="popover-footer"
|
|
503
|
+
className={cn(
|
|
504
|
+
"flex min-w-0 flex-wrap items-center justify-end gap-[var(--bh-popover-footer-gap)]",
|
|
505
|
+
className
|
|
506
|
+
)}
|
|
507
|
+
{...props}
|
|
508
|
+
/>
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const PopoverClose = React.forwardRef<HTMLButtonElement, PopoverCloseProps>(
|
|
513
|
+
function PopoverClose(
|
|
514
|
+
{
|
|
515
|
+
asChild = false,
|
|
516
|
+
children,
|
|
517
|
+
className,
|
|
518
|
+
label = "Close popover",
|
|
519
|
+
onClick,
|
|
520
|
+
type,
|
|
521
|
+
...props
|
|
522
|
+
},
|
|
523
|
+
ref
|
|
524
|
+
) {
|
|
525
|
+
const context = usePopoverContext("PopoverClose")
|
|
526
|
+
const Comp = asChild ? Slot : "button"
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<Comp
|
|
530
|
+
aria-label={label}
|
|
531
|
+
data-slot="popover-close"
|
|
532
|
+
ref={ref}
|
|
533
|
+
type={asChild ? undefined : type || "button"}
|
|
534
|
+
className={cn(
|
|
535
|
+
"inline-flex size-[var(--bh-popover-close-size)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)]",
|
|
536
|
+
"border-0 bg-transparent p-0 text-[var(--bh-content-subtle)] outline-none transition-[background-color,box-shadow,color]",
|
|
537
|
+
"hover:bg-[var(--bh-interactive-ghost-hover)] hover:text-[var(--bh-content-default)] focus-visible:shadow-[var(--shadow-button-focus)]",
|
|
538
|
+
className
|
|
539
|
+
)}
|
|
540
|
+
onClick={(event) => {
|
|
541
|
+
onClick?.(event)
|
|
542
|
+
if (!event.defaultPrevented) {
|
|
543
|
+
context.setOpen(false)
|
|
544
|
+
}
|
|
545
|
+
}}
|
|
546
|
+
{...props}
|
|
547
|
+
>
|
|
548
|
+
{children ?? (
|
|
549
|
+
<XIcon
|
|
550
|
+
aria-hidden="true"
|
|
551
|
+
className="size-[var(--bh-popover-close-icon-size)]"
|
|
552
|
+
strokeWidth={2}
|
|
553
|
+
/>
|
|
554
|
+
)}
|
|
555
|
+
</Comp>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
function PopoverArrow({
|
|
561
|
+
align,
|
|
562
|
+
side,
|
|
563
|
+
}: {
|
|
564
|
+
align: PopoverAlign
|
|
565
|
+
side: PopoverSide
|
|
566
|
+
}) {
|
|
567
|
+
const arrowSide = popoverArrowSideByContentSide[side]
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<span
|
|
571
|
+
aria-hidden="true"
|
|
572
|
+
data-arrow-side={arrowSide}
|
|
573
|
+
data-slot="popover-arrow"
|
|
574
|
+
className={cn(
|
|
575
|
+
"pointer-events-none absolute z-[var(--bh-z-raised)]",
|
|
576
|
+
getPopoverArrowPlacement(side, align)
|
|
577
|
+
)}
|
|
578
|
+
>
|
|
579
|
+
<svg
|
|
580
|
+
className="block h-full w-full overflow-visible"
|
|
581
|
+
focusable="false"
|
|
582
|
+
viewBox={popoverArrowShape[arrowSide].viewBox}
|
|
583
|
+
>
|
|
584
|
+
<path d={popoverArrowShape[arrowSide].fillPath} fill="var(--bh-popover-bg)" />
|
|
585
|
+
<path
|
|
586
|
+
d={popoverArrowShape[arrowSide].strokePath}
|
|
587
|
+
fill="none"
|
|
588
|
+
stroke="var(--bh-popover-border)"
|
|
589
|
+
strokeLinecap="round"
|
|
590
|
+
strokeLinejoin="round"
|
|
591
|
+
strokeWidth="var(--bh-border-width-default)"
|
|
592
|
+
vectorEffect="non-scaling-stroke"
|
|
593
|
+
/>
|
|
594
|
+
</svg>
|
|
595
|
+
</span>
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function usePopoverContext(componentName: string) {
|
|
600
|
+
const context = React.useContext(PopoverContext)
|
|
601
|
+
|
|
602
|
+
if (!context) {
|
|
603
|
+
throw new Error(`${componentName} must be used within Popover`)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return context
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function useControllablePopoverState({
|
|
610
|
+
defaultOpen,
|
|
611
|
+
onOpenChange,
|
|
612
|
+
open,
|
|
613
|
+
}: {
|
|
614
|
+
defaultOpen: boolean
|
|
615
|
+
onOpenChange?: (open: boolean) => void
|
|
616
|
+
open?: boolean
|
|
617
|
+
}) {
|
|
618
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
619
|
+
const isControlled = open !== undefined
|
|
620
|
+
const resolvedOpen = isControlled ? Boolean(open) : uncontrolledOpen
|
|
621
|
+
|
|
622
|
+
const setOpen = React.useCallback(
|
|
623
|
+
(nextOpen: boolean) => {
|
|
624
|
+
if (!isControlled) {
|
|
625
|
+
setUncontrolledOpen(nextOpen)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
onOpenChange?.(nextOpen)
|
|
629
|
+
},
|
|
630
|
+
[isControlled, onOpenChange]
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
return [resolvedOpen, setOpen] as const
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function getPopoverArrowPlacement(side: PopoverSide, align: PopoverAlign) {
|
|
637
|
+
if (side === "bottom" || side === "top") {
|
|
638
|
+
const edge =
|
|
639
|
+
side === "bottom"
|
|
640
|
+
? "top-[var(--bh-popover-arrow-offset)]"
|
|
641
|
+
: "bottom-[var(--bh-popover-arrow-offset)]"
|
|
642
|
+
const size =
|
|
643
|
+
"h-[calc(var(--bh-popover-arrow-depth)+var(--bh-popover-arrow-overlap))] w-[var(--bh-popover-arrow-width)]"
|
|
644
|
+
|
|
645
|
+
if (align === "center") {
|
|
646
|
+
return `${edge} left-1/2 ${size} -translate-x-1/2`
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (align === "end") {
|
|
650
|
+
return `${edge} ${size} [inset-inline-end:var(--bh-popover-arrow-inline-offset)]`
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return `${edge} ${size} [inset-inline-start:var(--bh-popover-arrow-inline-offset)]`
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const edge =
|
|
657
|
+
side === "right"
|
|
658
|
+
? "left-[var(--bh-popover-arrow-offset)]"
|
|
659
|
+
: "right-[var(--bh-popover-arrow-offset)]"
|
|
660
|
+
const size =
|
|
661
|
+
"h-[var(--bh-popover-arrow-width)] w-[calc(var(--bh-popover-arrow-depth)+var(--bh-popover-arrow-overlap))]"
|
|
662
|
+
|
|
663
|
+
if (align === "center") {
|
|
664
|
+
return `${edge} top-1/2 ${size} -translate-y-1/2`
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (align === "end") {
|
|
668
|
+
return `${edge} bottom-[var(--bh-popover-arrow-inline-offset)] ${size}`
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return `${edge} top-[var(--bh-popover-arrow-inline-offset)] ${size}`
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function getPopoverCollisionShift({
|
|
675
|
+
collisionPadding,
|
|
676
|
+
rect,
|
|
677
|
+
viewportHeight,
|
|
678
|
+
viewportWidth,
|
|
679
|
+
}: {
|
|
680
|
+
collisionPadding: number
|
|
681
|
+
rect: Pick<DOMRect, "bottom" | "left" | "right" | "top">
|
|
682
|
+
viewportHeight: number
|
|
683
|
+
viewportWidth: number
|
|
684
|
+
}): PopoverCollisionShift {
|
|
685
|
+
let x = 0
|
|
686
|
+
let y = 0
|
|
687
|
+
const minX = collisionPadding
|
|
688
|
+
const minY = collisionPadding
|
|
689
|
+
const maxX = viewportWidth - collisionPadding
|
|
690
|
+
const maxY = viewportHeight - collisionPadding
|
|
691
|
+
|
|
692
|
+
if (rect.left < minX) {
|
|
693
|
+
x = minX - rect.left
|
|
694
|
+
} else if (rect.right > maxX) {
|
|
695
|
+
x = maxX - rect.right
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (rect.top < minY) {
|
|
699
|
+
y = minY - rect.top
|
|
700
|
+
} else if (rect.bottom > maxY) {
|
|
701
|
+
y = maxY - rect.bottom
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return { x: Math.round(x), y: Math.round(y) }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function getPopoverContentTransform(
|
|
708
|
+
side: PopoverSide,
|
|
709
|
+
align: PopoverAlign,
|
|
710
|
+
shift: PopoverCollisionShift
|
|
711
|
+
) {
|
|
712
|
+
const shiftX = formatPopoverTranslateValue(shift.x)
|
|
713
|
+
const shiftY = formatPopoverTranslateValue(shift.y)
|
|
714
|
+
|
|
715
|
+
if ((side === "bottom" || side === "top") && align === "center") {
|
|
716
|
+
return `translate(${formatPopoverTranslateCalc("-50%", shift.x)}, ${shiftY})`
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if ((side === "left" || side === "right") && align === "center") {
|
|
720
|
+
return `translate(${shiftX}, ${formatPopoverTranslateCalc("-50%", shift.y)})`
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return `translate(${shiftX}, ${shiftY})`
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function formatPopoverTranslateValue(value: number) {
|
|
727
|
+
return `${Math.round(value)}px`
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function formatPopoverTranslateCalc(base: string, value: number) {
|
|
731
|
+
const roundedValue = Math.round(value)
|
|
732
|
+
if (roundedValue === 0) return base
|
|
733
|
+
|
|
734
|
+
return `calc(${base} ${roundedValue < 0 ? "-" : "+"} ${Math.abs(roundedValue)}px)`
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function mergePopoverTransforms(
|
|
738
|
+
contentTransform: string,
|
|
739
|
+
customTransform: string | undefined
|
|
740
|
+
) {
|
|
741
|
+
return customTransform
|
|
742
|
+
? `${contentTransform} ${customTransform}`
|
|
743
|
+
: contentTransform
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function isSamePopoverCollisionShift(
|
|
747
|
+
currentShift: PopoverCollisionShift,
|
|
748
|
+
nextShift: PopoverCollisionShift
|
|
749
|
+
) {
|
|
750
|
+
return currentShift.x === nextShift.x && currentShift.y === nextShift.y
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const popoverPositionClasses: Record<
|
|
754
|
+
PopoverSide,
|
|
755
|
+
Record<PopoverAlign, string>
|
|
756
|
+
> = {
|
|
757
|
+
bottom: {
|
|
758
|
+
start: "top-[calc(100%+var(--bh-popover-offset))] [inset-inline-start:0]",
|
|
759
|
+
center: "top-[calc(100%+var(--bh-popover-offset))] left-1/2",
|
|
760
|
+
end: "top-[calc(100%+var(--bh-popover-offset))] [inset-inline-end:0]",
|
|
761
|
+
},
|
|
762
|
+
top: {
|
|
763
|
+
start: "bottom-[calc(100%+var(--bh-popover-offset))] [inset-inline-start:0]",
|
|
764
|
+
center: "bottom-[calc(100%+var(--bh-popover-offset))] left-1/2",
|
|
765
|
+
end: "bottom-[calc(100%+var(--bh-popover-offset))] [inset-inline-end:0]",
|
|
766
|
+
},
|
|
767
|
+
right: {
|
|
768
|
+
start: "left-[calc(100%+var(--bh-popover-offset))] top-0",
|
|
769
|
+
center: "left-[calc(100%+var(--bh-popover-offset))] top-1/2",
|
|
770
|
+
end: "bottom-0 left-[calc(100%+var(--bh-popover-offset))]",
|
|
771
|
+
},
|
|
772
|
+
left: {
|
|
773
|
+
start: "right-[calc(100%+var(--bh-popover-offset))] top-0",
|
|
774
|
+
center: "right-[calc(100%+var(--bh-popover-offset))] top-1/2",
|
|
775
|
+
end: "bottom-0 right-[calc(100%+var(--bh-popover-offset))]",
|
|
776
|
+
},
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const popoverArrowSideByContentSide: Record<PopoverSide, PopoverArrowSide> = {
|
|
780
|
+
bottom: "top",
|
|
781
|
+
left: "right",
|
|
782
|
+
right: "left",
|
|
783
|
+
top: "bottom",
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const popoverArrowShape = {
|
|
787
|
+
top: {
|
|
788
|
+
viewBox: "0 0 12 9",
|
|
789
|
+
fillPath: "M0 9V8L6 0L12 8V9Z",
|
|
790
|
+
strokePath: "M0 8L6 0L12 8",
|
|
791
|
+
},
|
|
792
|
+
bottom: {
|
|
793
|
+
viewBox: "0 0 12 9",
|
|
794
|
+
fillPath: "M0 0H12V1L6 9L0 1Z",
|
|
795
|
+
strokePath: "M0 1L6 9L12 1",
|
|
796
|
+
},
|
|
797
|
+
left: {
|
|
798
|
+
viewBox: "0 0 9 12",
|
|
799
|
+
fillPath: "M9 0H8L0 6L8 12H9Z",
|
|
800
|
+
strokePath: "M8 0L0 6L8 12",
|
|
801
|
+
},
|
|
802
|
+
right: {
|
|
803
|
+
viewBox: "0 0 9 12",
|
|
804
|
+
fillPath: "M0 0H1L9 6L1 12H0Z",
|
|
805
|
+
strokePath: "M1 0L9 6L1 12",
|
|
806
|
+
},
|
|
807
|
+
} as const
|
|
808
|
+
|
|
809
|
+
const PopoverRoot = Popover
|
|
810
|
+
|
|
811
|
+
export {
|
|
812
|
+
Popover,
|
|
813
|
+
PopoverAnchor,
|
|
814
|
+
PopoverBody,
|
|
815
|
+
PopoverClose,
|
|
816
|
+
PopoverContent,
|
|
817
|
+
PopoverDescription,
|
|
818
|
+
PopoverFooter,
|
|
819
|
+
PopoverHeader,
|
|
820
|
+
PopoverRoot,
|
|
821
|
+
PopoverTitle,
|
|
822
|
+
PopoverTrigger,
|
|
823
|
+
popoverContentVariants,
|
|
824
|
+
}
|
|
825
|
+
export type {
|
|
826
|
+
PopoverAlign,
|
|
827
|
+
PopoverAnchorProps,
|
|
828
|
+
PopoverBodyProps,
|
|
829
|
+
PopoverCloseProps,
|
|
830
|
+
PopoverContentProps,
|
|
831
|
+
PopoverDescriptionProps,
|
|
832
|
+
PopoverFooterProps,
|
|
833
|
+
PopoverHeaderProps,
|
|
834
|
+
PopoverOffset,
|
|
835
|
+
PopoverProps,
|
|
836
|
+
PopoverSide,
|
|
837
|
+
PopoverSize,
|
|
838
|
+
PopoverTitleProps,
|
|
839
|
+
PopoverTriggerProps,
|
|
840
|
+
}
|