banhaten 0.1.3 → 0.2.0

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 (258) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CONTRIBUTING.md +44 -0
  3. package/README.md +65 -13
  4. package/SECURITY.md +15 -0
  5. package/banhaten.config.example.json +0 -2
  6. package/docs/design-system/README.md +9 -0
  7. package/docs/design-system/adoption-quality-remediation.md +87 -0
  8. package/docs/design-system/agent-usage.md +13 -0
  9. package/docs/design-system/appearance-presets.md +6 -1
  10. package/docs/design-system/component-docs-pattern.md +81 -0
  11. package/docs/design-system/component-token-consistency-audit.md +13 -3
  12. package/docs/design-system/components/README.md +3 -2
  13. package/docs/design-system/components/accordion.md +16 -0
  14. package/docs/design-system/components/activity-feed.md +15 -1
  15. package/docs/design-system/components/alert-dialog.md +17 -0
  16. package/docs/design-system/components/alert.md +46 -14
  17. package/docs/design-system/components/aspect-ratio.md +15 -0
  18. package/docs/design-system/components/attribute.md +16 -0
  19. package/docs/design-system/components/autocomplete.md +19 -0
  20. package/docs/design-system/components/avatar.md +18 -0
  21. package/docs/design-system/components/badge.md +19 -0
  22. package/docs/design-system/components/banner.md +15 -1
  23. package/docs/design-system/components/breadcrumbs.md +14 -0
  24. package/docs/design-system/components/button-group.md +34 -0
  25. package/docs/design-system/components/button.md +18 -0
  26. package/docs/design-system/components/card.md +18 -1
  27. package/docs/design-system/components/carousel.md +20 -3
  28. package/docs/design-system/components/catalog-components.md +15 -1
  29. package/docs/design-system/components/chart.md +15 -0
  30. package/docs/design-system/components/checkbox.md +16 -0
  31. package/docs/design-system/components/collapsible.md +15 -0
  32. package/docs/design-system/components/command.md +15 -0
  33. package/docs/design-system/components/context-menu.md +15 -0
  34. package/docs/design-system/components/date-picker.md +21 -1
  35. package/docs/design-system/components/divider.md +16 -0
  36. package/docs/design-system/components/empty-state.md +17 -0
  37. package/docs/design-system/components/field.md +17 -0
  38. package/docs/design-system/components/file-upload.md +14 -0
  39. package/docs/design-system/components/hover-card.md +15 -0
  40. package/docs/design-system/components/input-group.md +17 -0
  41. package/docs/design-system/components/input-otp.md +18 -0
  42. package/docs/design-system/components/input.md +43 -7
  43. package/docs/design-system/components/item.md +72 -0
  44. package/docs/design-system/components/kbd.md +18 -0
  45. package/docs/design-system/components/label.md +16 -0
  46. package/docs/design-system/components/menu.md +17 -0
  47. package/docs/design-system/components/menubar.md +15 -0
  48. package/docs/design-system/components/modal.md +25 -4
  49. package/docs/design-system/components/native-select.md +18 -0
  50. package/docs/design-system/components/navigation-menu.md +15 -0
  51. package/docs/design-system/components/onboarding-step-list-item.md +15 -1
  52. package/docs/design-system/components/page-header.md +15 -1
  53. package/docs/design-system/components/pagination.md +30 -8
  54. package/docs/design-system/components/popover.md +19 -0
  55. package/docs/design-system/components/progress-slider.md +18 -0
  56. package/docs/design-system/components/progress.md +17 -0
  57. package/docs/design-system/components/radio-card.md +16 -0
  58. package/docs/design-system/components/radio-group.md +17 -0
  59. package/docs/design-system/components/resizable.md +15 -0
  60. package/docs/design-system/components/scroll-area.md +15 -0
  61. package/docs/design-system/components/segmented-control.md +71 -0
  62. package/docs/design-system/components/select.md +44 -7
  63. package/docs/design-system/components/sheet.md +17 -0
  64. package/docs/design-system/components/sidebar.md +15 -0
  65. package/docs/design-system/components/skeleton.md +18 -0
  66. package/docs/design-system/components/slideout.md +14 -0
  67. package/docs/design-system/components/slider.md +20 -0
  68. package/docs/design-system/components/social-button.md +21 -3
  69. package/docs/design-system/components/spinner.md +16 -0
  70. package/docs/design-system/components/steps.md +14 -0
  71. package/docs/design-system/components/table.md +15 -1
  72. package/docs/design-system/components/tabs.md +17 -0
  73. package/docs/design-system/components/tag.md +17 -0
  74. package/docs/design-system/components/textarea.md +16 -0
  75. package/docs/design-system/components/timeline.md +14 -0
  76. package/docs/design-system/components/toast.md +17 -0
  77. package/docs/design-system/components/toggle.md +17 -0
  78. package/docs/design-system/components/toolbar.md +21 -1
  79. package/docs/design-system/components/tooltip.md +18 -1
  80. package/docs/design-system/density-and-elevation.md +44 -0
  81. package/docs/design-system/direction-and-localization.md +23 -0
  82. package/docs/design-system/form-recipes.md +73 -0
  83. package/docs/design-system/gap-promotion-policy.md +21 -0
  84. package/docs/design-system/gap-promotion-report.md +166 -0
  85. package/docs/design-system/installation.md +68 -0
  86. package/docs/design-system/launch-comparison-shadcn.md +98 -0
  87. package/docs/design-system/registry-and-cli.md +30 -7
  88. package/docs/design-system/release-policy.md +30 -0
  89. package/docs/design-system/roadmap.md +14 -0
  90. package/docs/design-system/support-matrix.md +23 -0
  91. package/docs/design-system/tabs.md +1 -1
  92. package/docs/design-system/token-governance.md +23 -1
  93. package/package.json +29 -15
  94. package/registry/component-contract.json +40 -0
  95. package/registry/components/alert-dialog.tsx +1 -1
  96. package/registry/components/alert.tsx +86 -65
  97. package/registry/components/autocomplete.tsx +23 -9
  98. package/registry/components/avatar.tsx +7 -7
  99. package/registry/components/badge.tsx +1 -1
  100. package/registry/components/button-group.tsx +321 -4
  101. package/registry/components/button.tsx +38 -17
  102. package/registry/components/card.tsx +68 -59
  103. package/registry/components/carousel.tsx +53 -5
  104. package/registry/components/chart.tsx +36 -3
  105. package/registry/components/checkbox.tsx +3 -3
  106. package/registry/components/collapsible.tsx +1 -1
  107. package/registry/components/command.tsx +1 -1
  108. package/registry/components/context-menu.tsx +1 -1
  109. package/registry/components/date-picker.tsx +245 -104
  110. package/registry/components/expanded/ActivityFeed.tsx +6 -15
  111. package/registry/components/expanded/Banner.tsx +3 -3
  112. package/registry/components/expanded/Breadcrumbs.tsx +5 -5
  113. package/registry/components/expanded/CatalogComponentsShowcase.tsx +13 -13
  114. package/registry/components/expanded/CatalogDivider.tsx +1 -1
  115. package/registry/components/expanded/EmptyState.tsx +1 -1
  116. package/registry/components/expanded/FileUpload.tsx +9 -2
  117. package/registry/components/expanded/OnboardingStepListItem.tsx +2 -4
  118. package/registry/components/expanded/PageHeader.tsx +13 -13
  119. package/registry/components/expanded/Slideout.tsx +15 -415
  120. package/registry/components/expanded/Steps.tsx +4 -4
  121. package/registry/components/expanded/Table.tsx +116 -69
  122. package/registry/components/expanded/Timeline.tsx +3 -3
  123. package/registry/components/expanded/activityFeed.css +1 -1
  124. package/registry/components/expanded/breadcrumbs.css +1 -1
  125. package/registry/components/expanded/divider.css +1 -1
  126. package/registry/components/expanded/emptyState.css +1 -1
  127. package/registry/components/expanded/fileUpload.css +6 -6
  128. package/registry/components/expanded/slideout.css +9 -685
  129. package/registry/components/expanded/steps.css +3 -3
  130. package/registry/components/expanded/table.css +2 -2
  131. package/registry/components/expanded/timeline.css +2 -2
  132. package/registry/components/input-content.ts +1 -0
  133. package/registry/components/input-otp.tsx +13 -3
  134. package/registry/components/input.tsx +68 -43
  135. package/registry/components/item.tsx +189 -0
  136. package/registry/components/kbd.tsx +111 -4
  137. package/registry/components/menu.tsx +31 -245
  138. package/registry/components/menubar.tsx +1 -1
  139. package/registry/components/modal.tsx +142 -102
  140. package/registry/components/native-select.tsx +2 -2
  141. package/registry/components/pagination.tsx +80 -58
  142. package/registry/components/popover.tsx +162 -664
  143. package/registry/components/radio-card.tsx +116 -87
  144. package/registry/components/segmented-control.tsx +87 -0
  145. package/registry/components/select-content.tsx +2 -2
  146. package/registry/components/select.tsx +52 -16
  147. package/registry/components/sheet.tsx +1 -1
  148. package/registry/components/sidebar.tsx +1 -1
  149. package/registry/components/skeleton.css +0 -3
  150. package/registry/components/social-button.tsx +47 -25
  151. package/registry/components/spinner.tsx +18 -55
  152. package/registry/components/tabs.css +1 -1
  153. package/registry/components/tabs.tsx +8 -4
  154. package/registry/components/tag.tsx +2 -2
  155. package/registry/components/textarea.tsx +3 -3
  156. package/registry/components/toast.tsx +16 -11
  157. package/registry/components/toggle.tsx +2 -2
  158. package/registry/components/toolbar.tsx +89 -17
  159. package/registry/components/tooltip.tsx +1 -1
  160. package/registry/examples/alert-demo.tsx +25 -5
  161. package/registry/examples/attribute-demo.tsx +1 -1
  162. package/registry/examples/autocomplete-demo.tsx +3 -3
  163. package/registry/examples/avatar-demo.tsx +7 -7
  164. package/registry/examples/badge-demo.tsx +1 -1
  165. package/registry/examples/button-demo.tsx +6 -6
  166. package/registry/examples/button-group-demo.tsx +23 -2
  167. package/registry/examples/card-demo.tsx +46 -8
  168. package/registry/examples/carousel-demo.tsx +20 -2
  169. package/registry/examples/chart-demo.tsx +1 -1
  170. package/registry/examples/checkbox-demo.tsx +5 -5
  171. package/registry/examples/collection-filter-bar-demo.tsx +135 -0
  172. package/registry/examples/command-demo.tsx +3 -3
  173. package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
  174. package/registry/examples/context-menu-demo.tsx +1 -1
  175. package/registry/examples/control-density-contract-demo.tsx +61 -0
  176. package/registry/examples/date-picker-demo.tsx +12 -12
  177. package/registry/examples/divider-demo.tsx +3 -3
  178. package/registry/examples/expanded/banner-demo.tsx +1 -1
  179. package/registry/examples/expanded/breadcrumbs-demo.tsx +1 -1
  180. package/registry/examples/expanded/empty-state-demo.tsx +1 -1
  181. package/registry/examples/expanded/file-upload-demo.tsx +1 -1
  182. package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
  183. package/registry/examples/expanded/slideout-demo.tsx +2 -2
  184. package/registry/examples/expanded/steps-demo.tsx +1 -1
  185. package/registry/examples/expanded/tabs-demo.tsx +1 -1
  186. package/registry/examples/expanded/timeline-demo.tsx +1 -1
  187. package/registry/examples/hover-card-demo.tsx +2 -2
  188. package/registry/examples/input-demo.tsx +6 -6
  189. package/registry/examples/input-otp-demo.tsx +4 -4
  190. package/registry/examples/item-demo.tsx +46 -0
  191. package/registry/examples/kbd-demo.tsx +1 -1
  192. package/registry/examples/label-demo.tsx +1 -1
  193. package/registry/examples/menu-demo.tsx +1 -1
  194. package/registry/examples/modal-demo.tsx +67 -9
  195. package/registry/examples/navigation-menu-demo.tsx +1 -1
  196. package/registry/examples/pagination-demo.tsx +6 -3
  197. package/registry/examples/popover-demo.tsx +19 -19
  198. package/registry/examples/progress-demo.tsx +1 -1
  199. package/registry/examples/progress-slider-demo.tsx +1 -1
  200. package/registry/examples/radio-card-demo.tsx +4 -4
  201. package/registry/examples/radio-group-demo.tsx +2 -2
  202. package/registry/examples/resizable-demo.tsx +1 -1
  203. package/registry/examples/results-toolbar-demo.tsx +44 -0
  204. package/registry/examples/scroll-area-demo.tsx +3 -3
  205. package/registry/examples/segmented-control-demo.tsx +39 -0
  206. package/registry/examples/select-demo.tsx +1 -1
  207. package/registry/examples/sidebar-demo.tsx +1 -1
  208. package/registry/examples/slider-demo.tsx +3 -3
  209. package/registry/examples/social-button-demo.tsx +18 -8
  210. package/registry/examples/spinner-demo.tsx +13 -13
  211. package/registry/examples/tabs-demo.tsx +1 -1
  212. package/registry/examples/tag-demo.tsx +1 -1
  213. package/registry/examples/textarea-demo.tsx +4 -4
  214. package/registry/examples/toast-demo.tsx +1 -1
  215. package/registry/examples/toggle-demo.tsx +6 -6
  216. package/registry/examples/tooltip-demo.tsx +4 -4
  217. package/registry/guidance/USAGE.md +27 -0
  218. package/registry/hooks/use-direction.ts +193 -13
  219. package/registry/index.json +5311 -377
  220. package/registry/styles/globals.css +2440 -4061
  221. package/registry.json +119 -149
  222. package/schema/config.schema.json +11 -8
  223. package/schema/registry.schema.json +150 -45
  224. package/schema/tokens.schema.json +134 -34
  225. package/src/cli/command-line.js +194 -0
  226. package/src/cli/config-editors.js +9 -0
  227. package/src/cli/doctor.js +399 -0
  228. package/src/cli/file-transaction.js +295 -0
  229. package/src/cli/global-css.js +153 -0
  230. package/src/cli/index.js +567 -1613
  231. package/src/cli/install-plan.js +592 -0
  232. package/src/cli/install-receipt.js +171 -0
  233. package/src/cli/jsonc.js +203 -0
  234. package/src/cli/package-install-state.js +254 -0
  235. package/src/cli/package-manager.js +238 -0
  236. package/src/cli/package-plan.js +185 -0
  237. package/src/cli/package-version.js +97 -0
  238. package/src/cli/registry-query.js +207 -0
  239. package/src/cli/reporting.js +55 -0
  240. package/src/cli/schema-validation.js +19 -0
  241. package/src/cli/source-scanner.js +686 -0
  242. package/src/cli/vite-config.js +478 -0
  243. package/tokens/banhaten.tokens.json +4312 -309
  244. package/docs/design-system/components/command-bar.md +0 -57
  245. package/registry/components/expanded/BannerBoard.tsx +0 -225
  246. package/registry/components/expanded/CatalogTag.tsx +0 -85
  247. package/registry/components/expanded/CommandBar.tsx +0 -386
  248. package/registry/components/expanded/IconExplorer.tsx +0 -612
  249. package/registry/components/expanded/TimelineShowcase.tsx +0 -158
  250. package/registry/components/expanded/commandBar.css +0 -470
  251. package/registry/components/expanded/iconExplorer.css +0 -764
  252. package/registry/components/expanded/iconPacks.ts +0 -866
  253. package/registry/components/expanded/tag.css +0 -148
  254. package/registry/examples/expanded/icons-demo.tsx +0 -5
  255. package/tokens/Color mode.zip +0 -0
  256. package/tokens/Numbers.zip +0 -0
  257. package/tokens/Radius.zip +0 -0
  258. package/tokens/Theme.zip +0 -0
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import * as React from "react"
2
4
  import { cva, type VariantProps } from "class-variance-authority"
3
5
 
@@ -9,6 +11,7 @@ const kbdVariants = cva(
9
11
  "border border-[var(--bh-border-default)] bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-subtle)]",
10
12
  "font-[var(--bh-font-family)] text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)]",
11
13
  "leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] shadow-[var(--shadow-button-soft)]",
14
+ "data-[disabled=true]:border-[var(--bh-border-disabled)] data-[disabled=true]:bg-[var(--bh-interactive-soft-disabled)] data-[disabled=true]:text-[var(--bh-content-disabled)] data-[disabled=true]:shadow-none",
12
15
  ],
13
16
  {
14
17
  variants: {
@@ -24,14 +27,17 @@ const kbdVariants = cva(
24
27
  )
25
28
 
26
29
  type KbdProps = React.ComponentProps<"kbd"> &
27
- VariantProps<typeof kbdVariants>
30
+ VariantProps<typeof kbdVariants> & {
31
+ disabled?: boolean
32
+ }
28
33
 
29
34
  const Kbd = React.forwardRef<HTMLElement, KbdProps>(function Kbd(
30
- { className, size, ...props },
35
+ { className, disabled = false, size, ...props },
31
36
  ref
32
37
  ) {
33
38
  return (
34
39
  <kbd
40
+ data-disabled={disabled ? "true" : undefined}
35
41
  data-slot="kbd"
36
42
  ref={ref}
37
43
  className={cn(kbdVariants({ size, className }))}
@@ -40,5 +46,106 @@ const Kbd = React.forwardRef<HTMLElement, KbdProps>(function Kbd(
40
46
  )
41
47
  })
42
48
 
43
- export { Kbd, kbdVariants }
44
- export type { KbdProps }
49
+ type ShortcutPlatform = "auto" | "mac" | "windows" | "linux"
50
+
51
+ type KbdShortcutProps = Omit<React.ComponentProps<"span">, "children"> & {
52
+ disabled?: boolean
53
+ keys: readonly string[]
54
+ platform?: ShortcutPlatform
55
+ size?: KbdProps["size"]
56
+ }
57
+
58
+ const KbdShortcut = React.forwardRef<HTMLSpanElement, KbdShortcutProps>(
59
+ function KbdShortcut(
60
+ {
61
+ "aria-label": ariaLabel,
62
+ className,
63
+ disabled = false,
64
+ keys,
65
+ platform = "auto",
66
+ size = "sm",
67
+ ...props
68
+ },
69
+ ref
70
+ ) {
71
+ const [resolvedPlatform, setResolvedPlatform] = React.useState<
72
+ Exclude<ShortcutPlatform, "auto">
73
+ >(platform === "auto" ? "mac" : platform)
74
+
75
+ React.useEffect(() => {
76
+ if (platform !== "auto") {
77
+ setResolvedPlatform(platform)
78
+ return
79
+ }
80
+
81
+ setResolvedPlatform(detectShortcutPlatform())
82
+ }, [platform])
83
+
84
+ const resolvedKeys = keys.map((key) =>
85
+ resolveShortcutKey(key, resolvedPlatform)
86
+ )
87
+ const accessibleLabel =
88
+ ariaLabel ?? resolvedKeys.map(readableShortcutKey).join(" plus ")
89
+
90
+ return (
91
+ <span
92
+ aria-label={accessibleLabel}
93
+ className={cn(
94
+ "inline-flex shrink-0 items-center gap-[var(--bh-space-xs-4)]",
95
+ className
96
+ )}
97
+ data-disabled={disabled ? "true" : undefined}
98
+ data-platform={resolvedPlatform}
99
+ data-slot="kbd-shortcut"
100
+ dir="ltr"
101
+ ref={ref}
102
+ role="group"
103
+ {...props}
104
+ >
105
+ {resolvedKeys.map((key, index) => (
106
+ <Kbd
107
+ aria-hidden="true"
108
+ disabled={disabled}
109
+ key={`${key}-${index}`}
110
+ size={size}
111
+ >
112
+ {key}
113
+ </Kbd>
114
+ ))}
115
+ </span>
116
+ )
117
+ }
118
+ )
119
+
120
+ function detectShortcutPlatform(): Exclude<ShortcutPlatform, "auto"> {
121
+ if (typeof navigator === "undefined") return "mac"
122
+ const platform = navigator.platform.toLowerCase()
123
+ if (
124
+ platform.includes("mac") ||
125
+ platform.includes("iphone") ||
126
+ platform.includes("ipad")
127
+ ) {
128
+ return "mac"
129
+ }
130
+ if (platform.includes("linux")) return "linux"
131
+ return "windows"
132
+ }
133
+
134
+ function resolveShortcutKey(
135
+ key: string,
136
+ platform: Exclude<ShortcutPlatform, "auto">
137
+ ) {
138
+ if (key !== "Mod") return key
139
+ return platform === "mac" ? "⌘" : "Ctrl"
140
+ }
141
+
142
+ function readableShortcutKey(key: string) {
143
+ if (key === "⌘") return "Command"
144
+ if (key === "⌥") return "Option"
145
+ if (key === "⇧") return "Shift"
146
+ if (key === "⌃") return "Control"
147
+ return key
148
+ }
149
+
150
+ export { Kbd, KbdShortcut, kbdVariants }
151
+ export type { KbdProps, KbdShortcutProps, ShortcutPlatform }
@@ -4,39 +4,21 @@ import { Slot } from "@radix-ui/react-slot"
4
4
  import { cva, type VariantProps } from "class-variance-authority"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
+ import { useResolvedDirection } from "@/hooks/use-direction"
7
8
 
8
9
  type MenuWidth = "menu" | "default" | "multiline" | "action" | "auto"
9
10
  type MenuItemKind = "default" | "multiline" | "action" | "progress" | "button"
10
11
  type MenuItemState = "default" | "disabled"
11
12
  type MenuItemIconPosition = "leading" | "trailing"
12
13
  type MenuItemAvatarSize = "sm" | "lg"
13
- type MenuCollisionShift = {
14
- x: number
15
- y: number
16
- }
17
14
  type MenuCollisionPadding = NonNullable<
18
15
  React.ComponentProps<typeof DropdownMenuPrimitive.Content>["collisionPadding"]
19
16
  >
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
17
 
33
18
  // Default Radix collisionPadding mirrors --bh-space-md-8.
34
19
  const MENU_COLLISION_PADDING_PX = 8
35
20
  // Radix sideOffset is numeric; this mirrors --bh-space-xs-4.
36
21
  const MENU_SUBCONTENT_SIDE_OFFSET_PX = 4
37
- const zeroMenuCollisionShift: MenuCollisionShift = { x: 0, y: 0 }
38
- const useSafeLayoutEffect =
39
- typeof window === "undefined" ? React.useEffect : React.useLayoutEffect
40
22
 
41
23
  const menuVariants = cva(
42
24
  "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)]",
@@ -119,14 +101,7 @@ const menuItemIconVariants = cva(
119
101
  }
120
102
  )
121
103
 
122
- type MenuProps = React.ComponentProps<"div"> &
123
- VariantProps<typeof menuVariants> & {
124
- asChild?: boolean
125
- defaultOpen?: boolean
126
- modal?: boolean
127
- onOpenChange?: (open: boolean) => void
128
- open?: boolean
129
- }
104
+ type MenuProps = React.ComponentProps<typeof DropdownMenuPrimitive.Root>
130
105
 
131
106
  type MenuContentProps = React.ComponentProps<typeof DropdownMenuPrimitive.Content> &
132
107
  VariantProps<typeof menuVariants> & {
@@ -143,147 +118,38 @@ const MenuSub = DropdownMenuPrimitive.Sub
143
118
  const MenuTrigger = DropdownMenuPrimitive.Trigger
144
119
  const MenuStaticContext = React.createContext(false)
145
120
 
121
+ function Menu({ dir, ...props }: MenuProps) {
122
+ const directionBoundaryRef = React.useRef<HTMLSpanElement | null>(null)
123
+ const resolvedDirection = useResolvedDirection(dir, directionBoundaryRef)
124
+
125
+ return (
126
+ <span className="contents" data-slot="menu-root-boundary" ref={directionBoundaryRef}>
127
+ <MenuRoot dir={resolvedDirection} {...props} />
128
+ </span>
129
+ )
130
+ }
131
+
146
132
  type MenuSurfaceProps = React.ComponentProps<"div"> &
147
133
  VariantProps<typeof menuVariants> & {
148
134
  asChild?: boolean
149
- avoidViewportNudge?: boolean
150
- collisionPadding?: MenuCollisionPadding
151
135
  }
152
136
 
153
137
  const MenuSurface = React.forwardRef<HTMLDivElement, MenuSurfaceProps>(function MenuSurface({
154
138
  asChild = false,
155
- avoidViewportNudge = true,
156
139
  children,
157
140
  className,
158
- collisionPadding = MENU_COLLISION_PADDING_PX,
159
141
  role = "menu",
160
- style,
161
142
  width,
162
143
  ...props
163
144
  }, ref) {
164
145
  const Comp = asChild ? Slot : "div"
165
- const surfaceRef = React.useRef<HTMLDivElement | null>(null)
166
- const collisionShiftRef = React.useRef(zeroMenuCollisionShift)
167
- const [collisionShift, setCollisionShift] = React.useState(
168
- zeroMenuCollisionShift
169
- )
170
-
171
- function updateCollisionShift(nextShift: MenuCollisionShift) {
172
- collisionShiftRef.current = nextShift
173
- setCollisionShift((currentShift) =>
174
- isSameMenuCollisionShift(currentShift, nextShift)
175
- ? currentShift
176
- : nextShift
177
- )
178
- }
179
-
180
- function setSurfaceRef(node: HTMLDivElement | null) {
181
- surfaceRef.current = node
182
-
183
- if (typeof ref === "function") {
184
- ref(node)
185
- } else if (ref) {
186
- ref.current = node
187
- }
188
- }
189
-
190
- useSafeLayoutEffect(() => {
191
- if (!avoidViewportNudge) {
192
- updateCollisionShift(zeroMenuCollisionShift)
193
- return
194
- }
195
-
196
- const node = surfaceRef.current
197
- if (!node) return
198
- const surfaceNode: HTMLDivElement = node
199
-
200
- const ownerWindow = surfaceNode.ownerDocument.defaultView
201
- if (!ownerWindow) return
202
- const surfaceWindow: Window = ownerWindow
203
-
204
- let animationFrame = 0
205
- const resolvedCollisionPadding = resolveMenuCollisionPadding(collisionPadding)
206
- const requestFrame =
207
- typeof surfaceWindow.requestAnimationFrame === "function"
208
- ? surfaceWindow.requestAnimationFrame.bind(surfaceWindow)
209
- : (callback: FrameRequestCallback) =>
210
- surfaceWindow.setTimeout(
211
- () => callback(surfaceWindow.performance.now()),
212
- 0
213
- )
214
- const cancelFrame =
215
- typeof surfaceWindow.cancelAnimationFrame === "function"
216
- ? surfaceWindow.cancelAnimationFrame.bind(surfaceWindow)
217
- : surfaceWindow.clearTimeout.bind(surfaceWindow)
218
-
219
- function measureCollisionShift() {
220
- animationFrame = 0
221
-
222
- const rect = surfaceNode.getBoundingClientRect()
223
- const currentShift = collisionShiftRef.current
224
- const unshiftedRect = {
225
- bottom: rect.bottom - currentShift.y,
226
- left: rect.left - currentShift.x,
227
- right: rect.right - currentShift.x,
228
- top: rect.top - currentShift.y,
229
- }
230
-
231
- updateCollisionShift(
232
- getMenuCollisionShift({
233
- collisionPadding: resolvedCollisionPadding,
234
- rect: unshiftedRect,
235
- viewportHeight: surfaceWindow.innerHeight,
236
- viewportWidth: surfaceWindow.innerWidth,
237
- })
238
- )
239
- }
240
-
241
- function queueCollisionShiftMeasure() {
242
- if (animationFrame) {
243
- cancelFrame(animationFrame)
244
- }
245
-
246
- animationFrame = requestFrame(measureCollisionShift)
247
- }
248
-
249
- queueCollisionShiftMeasure()
250
- surfaceWindow.addEventListener("resize", queueCollisionShiftMeasure)
251
- surfaceWindow.addEventListener("scroll", queueCollisionShiftMeasure, true)
252
-
253
- const ResizeObserver = (
254
- surfaceWindow as Window & {
255
- ResizeObserver?: MenuResizeObserverConstructor
256
- }
257
- ).ResizeObserver
258
- const resizeObserver = ResizeObserver
259
- ? new ResizeObserver(queueCollisionShiftMeasure)
260
- : undefined
261
- resizeObserver?.observe(surfaceNode)
262
-
263
- return () => {
264
- if (animationFrame) {
265
- cancelFrame(animationFrame)
266
- }
267
-
268
- surfaceWindow.removeEventListener("resize", queueCollisionShiftMeasure)
269
- surfaceWindow.removeEventListener("scroll", queueCollisionShiftMeasure, true)
270
- resizeObserver?.disconnect()
271
- }
272
- }, [avoidViewportNudge, collisionPadding, width])
273
146
 
274
147
  return (
275
148
  <Comp
276
149
  data-slot="menu"
277
150
  data-width={width ?? "menu"}
278
- ref={setSurfaceRef}
151
+ ref={ref}
279
152
  role={role}
280
- style={{
281
- ...style,
282
- transform: mergeMenuTransforms(
283
- getMenuCollisionTransform(collisionShift),
284
- style?.transform
285
- ),
286
- }}
287
153
  className={cn(
288
154
  menuVariants({ width }),
289
155
  "max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto",
@@ -314,7 +180,6 @@ const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(function
314
180
  <MenuSurface
315
181
  asChild={asChild}
316
182
  className={className}
317
- collisionPadding={collisionPadding}
318
183
  ref={ref}
319
184
  role={role}
320
185
  width={width}
@@ -345,7 +210,6 @@ const MenuSubContent = React.forwardRef<HTMLDivElement, MenuSubContentProps>(fun
345
210
  <MenuSurface
346
211
  asChild={asChild}
347
212
  className={className}
348
- collisionPadding={collisionPadding}
349
213
  ref={ref}
350
214
  role={role}
351
215
  width={width}
@@ -356,37 +220,30 @@ const MenuSubContent = React.forwardRef<HTMLDivElement, MenuSubContentProps>(fun
356
220
  )
357
221
  })
358
222
 
359
- function Menu({
223
+ type MenuPreviewProps = React.ComponentProps<"div"> &
224
+ VariantProps<typeof menuVariants> & {
225
+ asChild?: boolean
226
+ }
227
+
228
+ function MenuPreview({
360
229
  children,
361
230
  className,
362
- defaultOpen = true,
363
- modal = false,
364
- onOpenChange,
365
- open,
366
231
  role = "menu",
367
232
  width,
368
233
  asChild = false,
369
234
  ...props
370
- }: MenuProps) {
371
- const Comp = asChild ? Slot : "div"
372
- const isOpen = open ?? defaultOpen
373
-
374
- void modal
375
- void onOpenChange
376
-
377
- if (!isOpen) return null
378
-
235
+ }: MenuPreviewProps) {
379
236
  return (
380
237
  <MenuStaticContext.Provider value>
381
- <Comp
382
- data-slot="menu"
383
- data-width={width ?? "menu"}
238
+ <MenuSurface
239
+ asChild={asChild}
240
+ className={className}
384
241
  role={role}
385
- className={cn(menuVariants({ width, className }))}
242
+ width={width}
386
243
  {...props}
387
244
  >
388
245
  {children}
389
- </Comp>
246
+ </MenuSurface>
390
247
  </MenuStaticContext.Provider>
391
248
  )
392
249
  }
@@ -907,7 +764,7 @@ function MenuProgressSpinner({ className }: { className?: string }) {
907
764
  fill="none"
908
765
  stroke="currentColor"
909
766
  strokeLinecap="round"
910
- strokeWidth="3"
767
+ strokeWidth="var(--bh-icon-stroke-300)"
911
768
  />
912
769
  </svg>
913
770
  )
@@ -1025,81 +882,6 @@ function MenuSeparator({
1025
882
  return <DropdownMenuPrimitive.Separator asChild>{separator}</DropdownMenuPrimitive.Separator>
1026
883
  }
1027
884
 
1028
- function getMenuCollisionShift({
1029
- collisionPadding,
1030
- rect,
1031
- viewportHeight,
1032
- viewportWidth,
1033
- }: {
1034
- collisionPadding: MenuCollisionPaddingSides
1035
- rect: Pick<DOMRect, "bottom" | "left" | "right" | "top">
1036
- viewportHeight: number
1037
- viewportWidth: number
1038
- }): MenuCollisionShift {
1039
- let x = 0
1040
- let y = 0
1041
- const minX = collisionPadding.left
1042
- const minY = collisionPadding.top
1043
- const maxX = viewportWidth - collisionPadding.right
1044
- const maxY = viewportHeight - collisionPadding.bottom
1045
-
1046
- if (rect.left < minX) {
1047
- x = minX - rect.left
1048
- } else if (rect.right > maxX) {
1049
- x = maxX - rect.right
1050
- }
1051
-
1052
- if (rect.top < minY) {
1053
- y = minY - rect.top
1054
- } else if (rect.bottom > maxY) {
1055
- y = maxY - rect.bottom
1056
- }
1057
-
1058
- return { x: Math.round(x), y: Math.round(y) }
1059
- }
1060
-
1061
- function resolveMenuCollisionPadding(
1062
- collisionPadding: MenuCollisionPadding
1063
- ): MenuCollisionPaddingSides {
1064
- if (typeof collisionPadding === "number") {
1065
- const padding = Math.max(0, collisionPadding)
1066
-
1067
- return {
1068
- bottom: padding,
1069
- left: padding,
1070
- right: padding,
1071
- top: padding,
1072
- }
1073
- }
1074
-
1075
- return {
1076
- bottom: Math.max(0, collisionPadding.bottom ?? 0),
1077
- left: Math.max(0, collisionPadding.left ?? 0),
1078
- right: Math.max(0, collisionPadding.right ?? 0),
1079
- top: Math.max(0, collisionPadding.top ?? 0),
1080
- }
1081
- }
1082
-
1083
- function getMenuCollisionTransform(shift: MenuCollisionShift) {
1084
- return `translate(${Math.round(shift.x)}px, ${Math.round(shift.y)}px)`
1085
- }
1086
-
1087
- function mergeMenuTransforms(
1088
- collisionTransform: string,
1089
- customTransform: string | undefined
1090
- ) {
1091
- return customTransform
1092
- ? `${collisionTransform} ${customTransform}`
1093
- : collisionTransform
1094
- }
1095
-
1096
- function isSameMenuCollisionShift(
1097
- currentShift: MenuCollisionShift,
1098
- nextShift: MenuCollisionShift
1099
- ) {
1100
- return currentShift.x === nextShift.x && currentShift.y === nextShift.y
1101
- }
1102
-
1103
885
  export {
1104
886
  Menu,
1105
887
  MenuCaption,
@@ -1117,12 +899,14 @@ export {
1117
899
  MenuItemTitle,
1118
900
  MenuLabel,
1119
901
  MenuProgress,
902
+ MenuPreview,
1120
903
  MenuPortal,
1121
904
  MenuRoot,
1122
905
  MenuSeparator,
1123
906
  MenuSub,
1124
907
  MenuSubContent,
1125
908
  MenuSubTrigger,
909
+ MenuSurface,
1126
910
  MenuTrigger,
1127
911
  menuItemContentVariants,
1128
912
  menuItemIconVariants,
@@ -1146,5 +930,7 @@ export type {
1146
930
  MenuLabelProps,
1147
931
  MenuProgressProps,
1148
932
  MenuProps,
933
+ MenuPreviewProps,
934
+ MenuSurfaceProps,
1149
935
  MenuWidth,
1150
936
  }
@@ -78,7 +78,7 @@ const menuContentClassName =
78
78
  "z-[var(--bh-z-popover)] min-w-[var(--bh-menu-width)] overflow-hidden rounded-[var(--bh-radius-xl-10)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-xs-4)] text-[var(--bh-content-default)] shadow-[var(--shadow-menu)]"
79
79
 
80
80
  const menuItemClassName =
81
- "relative flex min-h-[var(--bh-menu-item-min-height)] cursor-default select-none items-center gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-md-6)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] text-start text-[length:var(--bh-text-body-sm-regular-font-size)] font-[var(--bh-text-body-sm-regular-font-weight)] leading-[var(--bh-text-body-sm-regular-line-height)] tracking-[var(--bh-text-body-sm-regular-letter-spacing)] text-[var(--bh-content-default)] outline-none data-[disabled]:pointer-events-none data-[disabled]:text-[var(--bh-content-disabled)] data-[highlighted]:bg-[var(--bh-menu-item-hover-bg)]"
81
+ "relative flex min-h-[var(--bh-menu-item-default-height)] cursor-default select-none items-center gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-md-6)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] text-start text-[length:var(--bh-text-body-sm-regular-font-size)] font-[var(--bh-text-body-sm-regular-font-weight)] leading-[var(--bh-text-body-sm-regular-line-height)] tracking-[var(--bh-text-body-sm-regular-letter-spacing)] text-[var(--bh-content-default)] outline-none data-[disabled]:pointer-events-none data-[disabled]:text-[var(--bh-content-disabled)] data-[highlighted]:bg-[var(--bh-menu-item-hover-bg)]"
82
82
 
83
83
  const MenubarContent = React.forwardRef<
84
84
  React.ElementRef<typeof MenubarPrimitive.Content>,