banhaten 0.1.0 → 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 (81) hide show
  1. package/README.md +21 -9
  2. package/package.json +8 -2
  3. package/registry/components/accordion.tsx +37 -1
  4. package/registry/components/alert.tsx +14 -28
  5. package/registry/components/attribute.tsx +6 -10
  6. package/registry/components/autocomplete.tsx +637 -0
  7. package/registry/components/avatar.tsx +259 -24
  8. package/registry/components/badge.tsx +97 -35
  9. package/registry/components/button-group.tsx +1 -1
  10. package/registry/components/card.tsx +1 -1
  11. package/registry/components/checkbox.tsx +19 -16
  12. package/registry/components/date-picker-state.ts +253 -0
  13. package/registry/components/date-picker.tsx +115 -158
  14. package/registry/components/expanded/ActivityFeed.tsx +37 -23
  15. package/registry/components/expanded/Banner.tsx +54 -19
  16. package/registry/components/expanded/Breadcrumbs.tsx +10 -38
  17. package/registry/components/expanded/CatalogComponentsShowcase.tsx +11 -16
  18. package/registry/components/expanded/CatalogTag.tsx +4 -11
  19. package/registry/components/expanded/CommandBar.tsx +33 -53
  20. package/registry/components/expanded/EmptyState.tsx +155 -0
  21. package/registry/components/expanded/FileUpload.tsx +362 -59
  22. package/registry/components/expanded/OnboardingStepListItem.tsx +6 -10
  23. package/registry/components/expanded/PageHeader.tsx +2 -11
  24. package/registry/components/expanded/Slideout.tsx +12 -23
  25. package/registry/components/expanded/Steps.tsx +6 -8
  26. package/registry/components/expanded/Table.tsx +18 -40
  27. package/registry/components/expanded/Timeline.tsx +5 -24
  28. package/registry/components/expanded/activityFeed.css +10 -54
  29. package/registry/components/expanded/banner.css +8 -75
  30. package/registry/components/expanded/breadcrumbs.css +1 -1
  31. package/registry/components/expanded/commandBar.css +23 -26
  32. package/registry/components/expanded/divider.css +1 -1
  33. package/registry/components/expanded/emptyState.css +111 -0
  34. package/registry/components/expanded/fileUpload.css +304 -75
  35. package/registry/components/expanded/pageHeader.css +1 -1
  36. package/registry/components/expanded/slideout.css +1 -0
  37. package/registry/components/expanded/steps.css +15 -51
  38. package/registry/components/expanded/table.css +6 -1
  39. package/registry/components/expanded/timeline.css +18 -15
  40. package/registry/components/input-otp.tsx +574 -0
  41. package/registry/components/input.tsx +140 -59
  42. package/registry/components/menu.tsx +470 -80
  43. package/registry/components/pagination.tsx +6 -18
  44. package/registry/components/popover.tsx +840 -0
  45. package/registry/components/radio-card.tsx +25 -31
  46. package/registry/components/select-content.tsx +28 -123
  47. package/registry/components/select.tsx +13 -9
  48. package/registry/components/skeleton.css +57 -0
  49. package/registry/components/skeleton.tsx +482 -0
  50. package/registry/components/social-button.tsx +24 -90
  51. package/registry/components/spinner.tsx +91 -7
  52. package/registry/components/textarea.tsx +21 -36
  53. package/registry/components/toggle.tsx +7 -23
  54. package/registry/components/tooltip.tsx +8 -4
  55. package/registry/examples/attribute-demo.tsx +2 -2
  56. package/registry/examples/autocomplete-demo.tsx +109 -0
  57. package/registry/examples/avatar-demo.tsx +102 -47
  58. package/registry/examples/badge-demo.tsx +16 -0
  59. package/registry/examples/checkbox-demo.tsx +3 -8
  60. package/registry/examples/date-picker-demo.tsx +75 -22
  61. package/registry/examples/expanded/banner-demo.tsx +31 -6
  62. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -0
  63. package/registry/examples/expanded/command-bar-demo.tsx +236 -0
  64. package/registry/examples/expanded/empty-state-demo.tsx +39 -0
  65. package/registry/examples/expanded/file-upload-demo.tsx +60 -0
  66. package/registry/examples/expanded/steps-demo.tsx +11 -0
  67. package/registry/examples/expanded/table-demo.tsx +142 -0
  68. package/registry/examples/input-demo.tsx +1 -1
  69. package/registry/examples/input-otp-demo.tsx +72 -0
  70. package/registry/examples/menu-demo.tsx +101 -88
  71. package/registry/examples/popover-demo.tsx +546 -0
  72. package/registry/examples/progress-demo.tsx +2 -2
  73. package/registry/examples/select-demo.tsx +32 -18
  74. package/registry/examples/skeleton-demo.tsx +56 -0
  75. package/registry/examples/social-button-demo.tsx +33 -33
  76. package/registry/examples/spinner-demo.tsx +59 -0
  77. package/registry/examples/tag-demo.tsx +1 -1
  78. package/registry/examples/textarea-demo.tsx +1 -1
  79. package/registry/index.json +266 -20
  80. package/registry/styles/globals.css +93 -3
  81. package/src/cli/index.js +997 -62
@@ -10,6 +10,31 @@ type MenuItemKind = "default" | "multiline" | "action" | "progress" | "button"
10
10
  type MenuItemState = "default" | "disabled"
11
11
  type MenuItemIconPosition = "leading" | "trailing"
12
12
  type MenuItemAvatarSize = "sm" | "lg"
13
+ type MenuCollisionShift = {
14
+ x: number
15
+ y: number
16
+ }
17
+ type MenuCollisionPadding = NonNullable<
18
+ React.ComponentProps<typeof DropdownMenuPrimitive.Content>["collisionPadding"]
19
+ >
20
+ type MenuCollisionPaddingSides = {
21
+ bottom: number
22
+ left: number
23
+ right: number
24
+ top: number
25
+ }
26
+ type MenuResizeObserverConstructor = new (
27
+ callback: () => void
28
+ ) => {
29
+ disconnect: () => void
30
+ observe: (target: Element) => void
31
+ }
32
+
33
+ // Default Radix collisionPadding mirrors --bh-space-md-8.
34
+ const MENU_COLLISION_PADDING_PX = 8
35
+ const zeroMenuCollisionShift: MenuCollisionShift = { x: 0, y: 0 }
36
+ const useSafeLayoutEffect =
37
+ typeof window === "undefined" ? React.useEffect : React.useLayoutEffect
13
38
 
14
39
  const menuVariants = cva(
15
40
  "flex flex-col items-start gap-[var(--bh-menu-gap)] overflow-hidden rounded-[var(--bh-menu-radius)] bg-[var(--bh-menu-bg)] py-[var(--bh-menu-padding-y)] shadow-[var(--shadow-menu)]",
@@ -106,50 +131,261 @@ type MenuContentProps = React.ComponentProps<typeof DropdownMenuPrimitive.Conten
106
131
  asChild?: boolean
107
132
  }
108
133
 
134
+ type MenuSubContentProps = React.ComponentProps<typeof DropdownMenuPrimitive.SubContent> &
135
+ VariantProps<typeof menuVariants> & {
136
+ asChild?: boolean
137
+ }
138
+
109
139
  const MenuRoot = DropdownMenuPrimitive.Root
140
+ const MenuSub = DropdownMenuPrimitive.Sub
110
141
  const MenuTrigger = DropdownMenuPrimitive.Trigger
142
+ const MenuStaticContext = React.createContext(false)
143
+
144
+ type MenuSurfaceProps = React.ComponentProps<"div"> &
145
+ VariantProps<typeof menuVariants> & {
146
+ asChild?: boolean
147
+ avoidViewportNudge?: boolean
148
+ collisionPadding?: MenuCollisionPadding
149
+ }
150
+
151
+ const MenuSurface = React.forwardRef<HTMLDivElement, MenuSurfaceProps>(function MenuSurface({
152
+ asChild = false,
153
+ avoidViewportNudge = true,
154
+ children,
155
+ className,
156
+ collisionPadding = MENU_COLLISION_PADDING_PX,
157
+ role = "menu",
158
+ style,
159
+ width,
160
+ ...props
161
+ }, ref) {
162
+ const Comp = asChild ? Slot : "div"
163
+ const surfaceRef = React.useRef<HTMLDivElement | null>(null)
164
+ const collisionShiftRef = React.useRef(zeroMenuCollisionShift)
165
+ const [collisionShift, setCollisionShift] = React.useState(
166
+ zeroMenuCollisionShift
167
+ )
168
+
169
+ function updateCollisionShift(nextShift: MenuCollisionShift) {
170
+ collisionShiftRef.current = nextShift
171
+ setCollisionShift((currentShift) =>
172
+ isSameMenuCollisionShift(currentShift, nextShift)
173
+ ? currentShift
174
+ : nextShift
175
+ )
176
+ }
177
+
178
+ function setSurfaceRef(node: HTMLDivElement | null) {
179
+ surfaceRef.current = node
180
+
181
+ if (typeof ref === "function") {
182
+ ref(node)
183
+ } else if (ref) {
184
+ ref.current = node
185
+ }
186
+ }
187
+
188
+ useSafeLayoutEffect(() => {
189
+ if (!avoidViewportNudge) {
190
+ updateCollisionShift(zeroMenuCollisionShift)
191
+ return
192
+ }
193
+
194
+ const node = surfaceRef.current
195
+ if (!node) return
196
+ const surfaceNode: HTMLDivElement = node
197
+
198
+ const ownerWindow = surfaceNode.ownerDocument.defaultView
199
+ if (!ownerWindow) return
200
+ const surfaceWindow: Window = ownerWindow
201
+
202
+ let animationFrame = 0
203
+ const resolvedCollisionPadding = resolveMenuCollisionPadding(collisionPadding)
204
+ const requestFrame =
205
+ typeof surfaceWindow.requestAnimationFrame === "function"
206
+ ? surfaceWindow.requestAnimationFrame.bind(surfaceWindow)
207
+ : (callback: FrameRequestCallback) =>
208
+ surfaceWindow.setTimeout(
209
+ () => callback(surfaceWindow.performance.now()),
210
+ 0
211
+ )
212
+ const cancelFrame =
213
+ typeof surfaceWindow.cancelAnimationFrame === "function"
214
+ ? surfaceWindow.cancelAnimationFrame.bind(surfaceWindow)
215
+ : surfaceWindow.clearTimeout.bind(surfaceWindow)
216
+
217
+ function measureCollisionShift() {
218
+ animationFrame = 0
219
+
220
+ const rect = surfaceNode.getBoundingClientRect()
221
+ const currentShift = collisionShiftRef.current
222
+ const unshiftedRect = {
223
+ bottom: rect.bottom - currentShift.y,
224
+ left: rect.left - currentShift.x,
225
+ right: rect.right - currentShift.x,
226
+ top: rect.top - currentShift.y,
227
+ }
228
+
229
+ updateCollisionShift(
230
+ getMenuCollisionShift({
231
+ collisionPadding: resolvedCollisionPadding,
232
+ rect: unshiftedRect,
233
+ viewportHeight: surfaceWindow.innerHeight,
234
+ viewportWidth: surfaceWindow.innerWidth,
235
+ })
236
+ )
237
+ }
238
+
239
+ function queueCollisionShiftMeasure() {
240
+ if (animationFrame) {
241
+ cancelFrame(animationFrame)
242
+ }
243
+
244
+ animationFrame = requestFrame(measureCollisionShift)
245
+ }
246
+
247
+ queueCollisionShiftMeasure()
248
+ surfaceWindow.addEventListener("resize", queueCollisionShiftMeasure)
249
+ surfaceWindow.addEventListener("scroll", queueCollisionShiftMeasure, true)
250
+
251
+ const ResizeObserver = (
252
+ surfaceWindow as Window & {
253
+ ResizeObserver?: MenuResizeObserverConstructor
254
+ }
255
+ ).ResizeObserver
256
+ const resizeObserver = ResizeObserver
257
+ ? new ResizeObserver(queueCollisionShiftMeasure)
258
+ : undefined
259
+ resizeObserver?.observe(surfaceNode)
260
+
261
+ return () => {
262
+ if (animationFrame) {
263
+ cancelFrame(animationFrame)
264
+ }
265
+
266
+ surfaceWindow.removeEventListener("resize", queueCollisionShiftMeasure)
267
+ surfaceWindow.removeEventListener("scroll", queueCollisionShiftMeasure, true)
268
+ resizeObserver?.disconnect()
269
+ }
270
+ }, [avoidViewportNudge, collisionPadding, width])
271
+
272
+ return (
273
+ <Comp
274
+ data-slot="menu"
275
+ data-width={width ?? "menu"}
276
+ ref={setSurfaceRef}
277
+ role={role}
278
+ style={{
279
+ ...style,
280
+ transform: mergeMenuTransforms(
281
+ getMenuCollisionTransform(collisionShift),
282
+ style?.transform
283
+ ),
284
+ }}
285
+ className={cn(
286
+ menuVariants({ width }),
287
+ "max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto",
288
+ className
289
+ )}
290
+ {...props}
291
+ >
292
+ {children}
293
+ </Comp>
294
+ )
295
+ })
111
296
 
112
297
  const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(function MenuContent({
298
+ children,
113
299
  className,
300
+ collisionPadding = MENU_COLLISION_PADDING_PX,
114
301
  width,
115
302
  asChild = false,
116
303
  role = "menu",
117
304
  ...props
118
305
  }, ref) {
119
- const Comp = asChild ? Slot : "div"
120
-
121
306
  return (
122
- <DropdownMenuPrimitive.Content asChild {...props}>
123
- <Comp
124
- data-slot="menu"
125
- data-width={width ?? "menu"}
307
+ <DropdownMenuPrimitive.Content
308
+ asChild
309
+ collisionPadding={collisionPadding}
310
+ {...props}
311
+ >
312
+ <MenuSurface
313
+ asChild={asChild}
314
+ className={className}
315
+ collisionPadding={collisionPadding}
126
316
  ref={ref}
127
317
  role={role}
128
- className={cn(menuVariants({ width, className }))}
129
- />
318
+ width={width}
319
+ >
320
+ {children}
321
+ </MenuSurface>
130
322
  </DropdownMenuPrimitive.Content>
131
323
  )
132
324
  })
133
325
 
326
+ const MenuSubContent = React.forwardRef<HTMLDivElement, MenuSubContentProps>(function MenuSubContent({
327
+ children,
328
+ className,
329
+ collisionPadding = MENU_COLLISION_PADDING_PX,
330
+ sideOffset = 4,
331
+ width,
332
+ asChild = false,
333
+ role = "menu",
334
+ ...props
335
+ }, ref) {
336
+ return (
337
+ <DropdownMenuPrimitive.SubContent
338
+ asChild
339
+ collisionPadding={collisionPadding}
340
+ sideOffset={sideOffset}
341
+ {...props}
342
+ >
343
+ <MenuSurface
344
+ asChild={asChild}
345
+ className={className}
346
+ collisionPadding={collisionPadding}
347
+ ref={ref}
348
+ role={role}
349
+ width={width}
350
+ >
351
+ {children}
352
+ </MenuSurface>
353
+ </DropdownMenuPrimitive.SubContent>
354
+ )
355
+ })
356
+
134
357
  function Menu({
135
358
  children,
359
+ className,
136
360
  defaultOpen = true,
137
361
  modal = false,
138
362
  onOpenChange,
139
363
  open,
364
+ role = "menu",
365
+ width,
366
+ asChild = false,
140
367
  ...props
141
368
  }: MenuProps) {
142
- const rootProps =
143
- open === undefined
144
- ? { defaultOpen, onOpenChange }
145
- : { open, onOpenChange }
369
+ const Comp = asChild ? Slot : "div"
370
+ const isOpen = open ?? defaultOpen
371
+
372
+ void modal
373
+ void onOpenChange
374
+
375
+ if (!isOpen) return null
146
376
 
147
377
  return (
148
- <MenuRoot modal={modal} {...rootProps}>
149
- <MenuContent forceMount {...props}>
378
+ <MenuStaticContext.Provider value>
379
+ <Comp
380
+ data-slot="menu"
381
+ data-width={width ?? "menu"}
382
+ role={role}
383
+ className={cn(menuVariants({ width, className }))}
384
+ {...props}
385
+ >
150
386
  {children}
151
- </MenuContent>
152
- </MenuRoot>
387
+ </Comp>
388
+ </MenuStaticContext.Provider>
153
389
  )
154
390
  }
155
391
 
@@ -175,6 +411,18 @@ type MenuItemProps = React.ComponentProps<"div"> &
175
411
  disabled?: boolean
176
412
  }
177
413
 
414
+ type MenuSubTriggerPrimitiveProps = Pick<
415
+ React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>,
416
+ "textValue"
417
+ >
418
+
419
+ type MenuSubTriggerProps = React.ComponentProps<"div"> &
420
+ VariantProps<typeof menuItemVariants> &
421
+ MenuSubTriggerPrimitiveProps & {
422
+ asChild?: boolean
423
+ disabled?: boolean
424
+ }
425
+
178
426
  const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem({
179
427
  className,
180
428
  kind = "default",
@@ -188,9 +436,30 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
188
436
  ...props
189
437
  }, ref) {
190
438
  const Comp = asChild ? Slot : "div"
439
+ const isStaticMenu = React.useContext(MenuStaticContext)
191
440
  const selectedKind: MenuItemKind = kind ?? "default"
192
441
  const isDisabled = disabled
193
442
 
443
+ const item = (
444
+ <Comp
445
+ aria-disabled={isDisabled ? true : undefined}
446
+ data-disabled={isDisabled ? "true" : "false"}
447
+ data-kind={selectedKind}
448
+ data-slot="menu-item"
449
+ ref={ref}
450
+ role={role}
451
+ tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
452
+ className={cn(menuItemVariants({ kind: selectedKind, className }))}
453
+ {...props}
454
+ >
455
+ <MenuItemContent kind={selectedKind}>
456
+ <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
457
+ </MenuItemContent>
458
+ </Comp>
459
+ )
460
+
461
+ if (isStaticMenu) return item
462
+
194
463
  return (
195
464
  <DropdownMenuPrimitive.Item
196
465
  asChild
@@ -198,27 +467,62 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
198
467
  onSelect={onSelect}
199
468
  textValue={textValue ?? getMenuItemTextValue(children)}
200
469
  >
201
- <Comp
202
- aria-disabled={isDisabled ? true : undefined}
203
- data-disabled={isDisabled ? "true" : "false"}
204
- data-kind={selectedKind}
205
- data-slot="menu-item"
206
- ref={ref}
207
- role={role}
208
- tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
209
- className={cn(menuItemVariants({ kind: selectedKind, className }))}
210
- {...props}
211
- >
212
- <MenuItemContent kind={selectedKind}>
213
- <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
214
- </MenuItemContent>
215
- </Comp>
470
+ {item}
216
471
  </DropdownMenuPrimitive.Item>
217
472
  )
218
473
  })
219
474
 
220
475
  MenuItem.displayName = "MenuItem"
221
476
 
477
+ const MenuSubTrigger = React.forwardRef<HTMLDivElement, MenuSubTriggerProps>(function MenuSubTrigger({
478
+ className,
479
+ kind = "default",
480
+ disabled = false,
481
+ asChild = false,
482
+ role = "menuitem",
483
+ tabIndex,
484
+ children,
485
+ textValue,
486
+ ...props
487
+ }, ref) {
488
+ const Comp = asChild ? Slot : "div"
489
+ const isStaticMenu = React.useContext(MenuStaticContext)
490
+ const selectedKind: MenuItemKind = kind ?? "default"
491
+ const isDisabled = disabled
492
+
493
+ const item = (
494
+ <Comp
495
+ aria-disabled={isDisabled ? true : undefined}
496
+ data-disabled={isDisabled ? "true" : "false"}
497
+ data-kind={selectedKind}
498
+ data-slot="menu-sub-trigger"
499
+ ref={ref}
500
+ role={role}
501
+ tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
502
+ className={cn(menuItemVariants({ kind: selectedKind, className }))}
503
+ {...props}
504
+ >
505
+ <MenuItemContent kind={selectedKind}>
506
+ <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
507
+ </MenuItemContent>
508
+ </Comp>
509
+ )
510
+
511
+ if (isStaticMenu) return item
512
+
513
+ return (
514
+ <DropdownMenuPrimitive.SubTrigger
515
+ asChild
516
+ disabled={isDisabled}
517
+ textValue={textValue ?? getMenuItemTextValue(children)}
518
+ >
519
+ {item}
520
+ </DropdownMenuPrimitive.SubTrigger>
521
+ )
522
+ })
523
+
524
+ MenuSubTrigger.displayName = "MenuSubTrigger"
525
+
222
526
  type MenuItemContentProps = React.ComponentProps<"div"> &
223
527
  VariantProps<typeof menuItemContentVariants>
224
528
 
@@ -620,42 +924,45 @@ function MenuLabel({
620
924
  role = "presentation",
621
925
  ...props
622
926
  }: MenuLabelProps) {
623
- return (
624
- <DropdownMenuPrimitive.Label asChild>
625
- <div
626
- data-slot="menu-label"
627
- role={role}
628
- className={cn(
629
- "flex h-[var(--bh-menu-item-label-height)] w-full items-center gap-0 bg-[var(--bh-menu-item-bg)] px-[var(--bh-menu-label-padding-x)] py-[var(--bh-space-xs-4)] text-[length:var(--bh-text-body-xs-regular-font-size)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
630
- className
631
- )}
632
- {...props}
633
- >
634
- {children ?? (
635
- <>
636
- {hasRenderableContent(label) ? (
637
- <span
638
- data-slot="menu-label-primary"
639
- dir="auto"
640
- className="min-w-0 flex-1 truncate text-start font-[var(--bh-text-body-xs-medium-font-weight)] text-[var(--bh-content-default)]"
641
- >
642
- {label}
643
- </span>
644
- ) : null}
645
- {hasRenderableContent(caption) ? (
646
- <span
647
- data-slot="menu-label-caption"
648
- dir="auto"
649
- className="shrink-0 whitespace-nowrap text-end font-[var(--bh-text-body-xs-regular-font-weight)] text-[var(--bh-content-subtle)]"
650
- >
651
- {caption}
652
- </span>
653
- ) : null}
654
- </>
655
- )}
656
- </div>
657
- </DropdownMenuPrimitive.Label>
927
+ const isStaticMenu = React.useContext(MenuStaticContext)
928
+ const labelContent = (
929
+ <div
930
+ data-slot="menu-label"
931
+ role={role}
932
+ className={cn(
933
+ "flex h-[var(--bh-menu-item-label-height)] w-full items-center gap-0 bg-[var(--bh-menu-item-bg)] px-[var(--bh-menu-label-padding-x)] py-[var(--bh-space-xs-4)] text-[length:var(--bh-text-body-xs-regular-font-size)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
934
+ className
935
+ )}
936
+ {...props}
937
+ >
938
+ {children ?? (
939
+ <>
940
+ {hasRenderableContent(label) ? (
941
+ <span
942
+ data-slot="menu-label-primary"
943
+ dir="auto"
944
+ className="min-w-0 flex-1 truncate text-start font-[var(--bh-text-body-xs-medium-font-weight)] text-[var(--bh-content-default)]"
945
+ >
946
+ {label}
947
+ </span>
948
+ ) : null}
949
+ {hasRenderableContent(caption) ? (
950
+ <span
951
+ data-slot="menu-label-caption"
952
+ dir="auto"
953
+ className="shrink-0 whitespace-nowrap text-end font-[var(--bh-text-body-xs-regular-font-weight)] text-[var(--bh-content-subtle)]"
954
+ >
955
+ {caption}
956
+ </span>
957
+ ) : null}
958
+ </>
959
+ )}
960
+ </div>
658
961
  )
962
+
963
+ if (isStaticMenu) return labelContent
964
+
965
+ return <DropdownMenuPrimitive.Label asChild>{labelContent}</DropdownMenuPrimitive.Label>
659
966
  }
660
967
 
661
968
  function hasRenderableContent(content: React.ReactNode) {
@@ -696,21 +1003,99 @@ function MenuSeparator({
696
1003
  role = "separator",
697
1004
  ...props
698
1005
  }: React.ComponentProps<"div">) {
699
- return (
700
- <DropdownMenuPrimitive.Separator asChild>
701
- <div
702
- data-slot="menu-separator"
703
- role={role}
704
- className={cn(
705
- "flex h-[var(--bh-menu-item-divider-height)] w-full items-center py-[var(--bh-menu-divider-padding-y)]",
706
- className
707
- )}
708
- {...props}
709
- >
710
- <span className="block w-full border-t border-[var(--bh-border-subtle)]" />
711
- </div>
712
- </DropdownMenuPrimitive.Separator>
1006
+ const isStaticMenu = React.useContext(MenuStaticContext)
1007
+ const separator = (
1008
+ <div
1009
+ data-slot="menu-separator"
1010
+ role={role}
1011
+ className={cn(
1012
+ "flex h-[var(--bh-menu-item-divider-height)] w-full items-center py-[var(--bh-menu-divider-padding-y)]",
1013
+ className
1014
+ )}
1015
+ {...props}
1016
+ >
1017
+ <span className="block w-full border-t border-[var(--bh-border-subtle)]" />
1018
+ </div>
713
1019
  )
1020
+
1021
+ if (isStaticMenu) return separator
1022
+
1023
+ return <DropdownMenuPrimitive.Separator asChild>{separator}</DropdownMenuPrimitive.Separator>
1024
+ }
1025
+
1026
+ function getMenuCollisionShift({
1027
+ collisionPadding,
1028
+ rect,
1029
+ viewportHeight,
1030
+ viewportWidth,
1031
+ }: {
1032
+ collisionPadding: MenuCollisionPaddingSides
1033
+ rect: Pick<DOMRect, "bottom" | "left" | "right" | "top">
1034
+ viewportHeight: number
1035
+ viewportWidth: number
1036
+ }): MenuCollisionShift {
1037
+ let x = 0
1038
+ let y = 0
1039
+ const minX = collisionPadding.left
1040
+ const minY = collisionPadding.top
1041
+ const maxX = viewportWidth - collisionPadding.right
1042
+ const maxY = viewportHeight - collisionPadding.bottom
1043
+
1044
+ if (rect.left < minX) {
1045
+ x = minX - rect.left
1046
+ } else if (rect.right > maxX) {
1047
+ x = maxX - rect.right
1048
+ }
1049
+
1050
+ if (rect.top < minY) {
1051
+ y = minY - rect.top
1052
+ } else if (rect.bottom > maxY) {
1053
+ y = maxY - rect.bottom
1054
+ }
1055
+
1056
+ return { x: Math.round(x), y: Math.round(y) }
1057
+ }
1058
+
1059
+ function resolveMenuCollisionPadding(
1060
+ collisionPadding: MenuCollisionPadding
1061
+ ): MenuCollisionPaddingSides {
1062
+ if (typeof collisionPadding === "number") {
1063
+ const padding = Math.max(0, collisionPadding)
1064
+
1065
+ return {
1066
+ bottom: padding,
1067
+ left: padding,
1068
+ right: padding,
1069
+ top: padding,
1070
+ }
1071
+ }
1072
+
1073
+ return {
1074
+ bottom: Math.max(0, collisionPadding.bottom ?? 0),
1075
+ left: Math.max(0, collisionPadding.left ?? 0),
1076
+ right: Math.max(0, collisionPadding.right ?? 0),
1077
+ top: Math.max(0, collisionPadding.top ?? 0),
1078
+ }
1079
+ }
1080
+
1081
+ function getMenuCollisionTransform(shift: MenuCollisionShift) {
1082
+ return `translate(${Math.round(shift.x)}px, ${Math.round(shift.y)}px)`
1083
+ }
1084
+
1085
+ function mergeMenuTransforms(
1086
+ collisionTransform: string,
1087
+ customTransform: string | undefined
1088
+ ) {
1089
+ return customTransform
1090
+ ? `${collisionTransform} ${customTransform}`
1091
+ : collisionTransform
1092
+ }
1093
+
1094
+ function isSameMenuCollisionShift(
1095
+ currentShift: MenuCollisionShift,
1096
+ nextShift: MenuCollisionShift
1097
+ ) {
1098
+ return currentShift.x === nextShift.x && currentShift.y === nextShift.y
714
1099
  }
715
1100
 
716
1101
  export {
@@ -733,6 +1118,9 @@ export {
733
1118
  MenuPortal,
734
1119
  MenuRoot,
735
1120
  MenuSeparator,
1121
+ MenuSub,
1122
+ MenuSubContent,
1123
+ MenuSubTrigger,
736
1124
  MenuTrigger,
737
1125
  menuItemContentVariants,
738
1126
  menuItemIconVariants,
@@ -749,6 +1137,8 @@ export type {
749
1137
  MenuItemIconProps,
750
1138
  MenuItemKind,
751
1139
  MenuItemProps,
1140
+ MenuSubContentProps,
1141
+ MenuSubTriggerProps,
752
1142
  MenuItemSwitchProps,
753
1143
  MenuItemState,
754
1144
  MenuLabelProps,
@@ -1,5 +1,6 @@
1
1
  import * as React from "react"
2
2
  import { cva, type VariantProps } from "class-variance-authority"
3
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
3
4
 
4
5
  import { cn } from "@/lib/utils"
5
6
 
@@ -444,29 +445,16 @@ function PaginationPageItem({
444
445
  }
445
446
 
446
447
  function PaginationArrowIcon({ type }: { type: "previous" | "next" }) {
448
+ const Icon = type === "previous" ? ChevronLeftIcon : ChevronRightIcon
449
+
447
450
  return (
448
- <svg
451
+ <Icon
449
452
  aria-hidden="true"
450
453
  data-rtl-flip="true"
451
454
  data-slot="pagination-icon"
452
- fill="none"
453
455
  focusable="false"
454
- viewBox="0 0 20 20"
455
- >
456
- {type === "previous" ? (
457
- <path
458
- d="M2.357 5.30332L6.48183 9.42807L5.30333 10.6066L0 5.30332L5.30333 0L6.48183 1.17851L2.357 5.30332Z"
459
- fill="currentColor"
460
- transform="translate(6.666 4.697)"
461
- />
462
- ) : (
463
- <path
464
- d="M4.12482 5.30332L0 1.17851L1.17852 0L6.48182 5.30332L1.17852 10.6066L0 9.42807L4.12482 5.30332Z"
465
- fill="currentColor"
466
- transform="translate(6.852 4.697)"
467
- />
468
- )}
469
- </svg>
456
+ strokeWidth={2.25}
457
+ />
470
458
  )
471
459
  }
472
460