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.
Files changed (37) hide show
  1. package/README.md +20 -8
  2. package/package.json +8 -2
  3. package/registry/components/autocomplete.tsx +637 -0
  4. package/registry/components/avatar.tsx +258 -22
  5. package/registry/components/badge.tsx +97 -35
  6. package/registry/components/date-picker-state.ts +253 -0
  7. package/registry/components/date-picker.tsx +115 -158
  8. package/registry/components/expanded/EmptyState.tsx +155 -0
  9. package/registry/components/expanded/emptyState.css +111 -0
  10. package/registry/components/expanded/slideout.css +1 -0
  11. package/registry/components/expanded/table.css +1 -0
  12. package/registry/components/input-otp.tsx +574 -0
  13. package/registry/components/input.tsx +21 -11
  14. package/registry/components/menu.tsx +371 -8
  15. package/registry/components/popover.tsx +840 -0
  16. package/registry/components/select.tsx +4 -0
  17. package/registry/components/skeleton.css +57 -0
  18. package/registry/components/skeleton.tsx +482 -0
  19. package/registry/components/spinner.tsx +79 -11
  20. package/registry/components/textarea.tsx +1 -1
  21. package/registry/components/tooltip.tsx +4 -0
  22. package/registry/examples/autocomplete-demo.tsx +109 -0
  23. package/registry/examples/avatar-demo.tsx +102 -47
  24. package/registry/examples/badge-demo.tsx +16 -0
  25. package/registry/examples/expanded/command-bar-demo.tsx +236 -0
  26. package/registry/examples/expanded/empty-state-demo.tsx +39 -0
  27. package/registry/examples/input-demo.tsx +1 -1
  28. package/registry/examples/input-otp-demo.tsx +72 -0
  29. package/registry/examples/menu-demo.tsx +101 -88
  30. package/registry/examples/popover-demo.tsx +546 -0
  31. package/registry/examples/select-demo.tsx +1 -1
  32. package/registry/examples/skeleton-demo.tsx +56 -0
  33. package/registry/examples/spinner-demo.tsx +23 -1
  34. package/registry/examples/textarea-demo.tsx +1 -1
  35. package/registry/index.json +240 -8
  36. package/registry/styles/globals.css +88 -0
  37. 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
+ }