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
@@ -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,32 +131,229 @@ 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
111
142
  const MenuStaticContext = React.createContext(false)
112
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
+ })
296
+
113
297
  const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(function MenuContent({
298
+ children,
114
299
  className,
300
+ collisionPadding = MENU_COLLISION_PADDING_PX,
115
301
  width,
116
302
  asChild = false,
117
303
  role = "menu",
118
304
  ...props
119
305
  }, ref) {
120
- const Comp = asChild ? Slot : "div"
121
-
122
306
  return (
123
- <DropdownMenuPrimitive.Content asChild {...props}>
124
- <Comp
125
- data-slot="menu"
126
- 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}
127
316
  ref={ref}
128
317
  role={role}
129
- className={cn(menuVariants({ width, className }))}
130
- />
318
+ width={width}
319
+ >
320
+ {children}
321
+ </MenuSurface>
131
322
  </DropdownMenuPrimitive.Content>
132
323
  )
133
324
  })
134
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
+
135
357
  function Menu({
136
358
  children,
137
359
  className,
@@ -189,6 +411,18 @@ type MenuItemProps = React.ComponentProps<"div"> &
189
411
  disabled?: boolean
190
412
  }
191
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
+
192
426
  const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem({
193
427
  className,
194
428
  kind = "default",
@@ -240,6 +474,55 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
240
474
 
241
475
  MenuItem.displayName = "MenuItem"
242
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
+
243
526
  type MenuItemContentProps = React.ComponentProps<"div"> &
244
527
  VariantProps<typeof menuItemContentVariants>
245
528
 
@@ -740,6 +1023,81 @@ function MenuSeparator({
740
1023
  return <DropdownMenuPrimitive.Separator asChild>{separator}</DropdownMenuPrimitive.Separator>
741
1024
  }
742
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
1099
+ }
1100
+
743
1101
  export {
744
1102
  Menu,
745
1103
  MenuCaption,
@@ -760,6 +1118,9 @@ export {
760
1118
  MenuPortal,
761
1119
  MenuRoot,
762
1120
  MenuSeparator,
1121
+ MenuSub,
1122
+ MenuSubContent,
1123
+ MenuSubTrigger,
763
1124
  MenuTrigger,
764
1125
  menuItemContentVariants,
765
1126
  menuItemIconVariants,
@@ -776,6 +1137,8 @@ export type {
776
1137
  MenuItemIconProps,
777
1138
  MenuItemKind,
778
1139
  MenuItemProps,
1140
+ MenuSubContentProps,
1141
+ MenuSubTriggerProps,
779
1142
  MenuItemSwitchProps,
780
1143
  MenuItemState,
781
1144
  MenuLabelProps,