banhaten 0.1.3 → 0.3.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 (275) hide show
  1. package/CHANGELOG.md +73 -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-implementation-ownership-audit.md +244 -0
  12. package/docs/design-system/component-quality-audit-2026-08-20.md +223 -0
  13. package/docs/design-system/component-showcase-consistency-report.md +2 -0
  14. package/docs/design-system/component-token-consistency-audit.md +13 -3
  15. package/docs/design-system/components/README.md +4 -2
  16. package/docs/design-system/components/accordion.md +16 -0
  17. package/docs/design-system/components/activity-feed.md +17 -1
  18. package/docs/design-system/components/activity-heatmap.md +73 -0
  19. package/docs/design-system/components/alert-dialog.md +17 -0
  20. package/docs/design-system/components/alert.md +46 -14
  21. package/docs/design-system/components/aspect-ratio.md +15 -0
  22. package/docs/design-system/components/attribute.md +16 -0
  23. package/docs/design-system/components/autocomplete.md +19 -0
  24. package/docs/design-system/components/avatar.md +18 -0
  25. package/docs/design-system/components/badge.md +19 -0
  26. package/docs/design-system/components/banner.md +19 -1
  27. package/docs/design-system/components/breadcrumbs.md +17 -0
  28. package/docs/design-system/components/button-group.md +35 -0
  29. package/docs/design-system/components/button.md +18 -0
  30. package/docs/design-system/components/card.md +26 -1
  31. package/docs/design-system/components/carousel.md +20 -3
  32. package/docs/design-system/components/catalog-components.md +18 -1
  33. package/docs/design-system/components/chart.md +15 -0
  34. package/docs/design-system/components/checkbox.md +16 -0
  35. package/docs/design-system/components/collapsible.md +15 -0
  36. package/docs/design-system/components/command.md +15 -0
  37. package/docs/design-system/components/context-menu.md +15 -0
  38. package/docs/design-system/components/date-picker.md +21 -1
  39. package/docs/design-system/components/divider.md +16 -0
  40. package/docs/design-system/components/empty-state.md +17 -0
  41. package/docs/design-system/components/field.md +17 -0
  42. package/docs/design-system/components/file-upload.md +18 -0
  43. package/docs/design-system/components/hover-card.md +15 -0
  44. package/docs/design-system/components/input-group.md +17 -0
  45. package/docs/design-system/components/input-otp.md +18 -0
  46. package/docs/design-system/components/input.md +43 -7
  47. package/docs/design-system/components/item.md +72 -0
  48. package/docs/design-system/components/kbd.md +18 -0
  49. package/docs/design-system/components/label.md +16 -0
  50. package/docs/design-system/components/menu.md +17 -0
  51. package/docs/design-system/components/menubar.md +15 -0
  52. package/docs/design-system/components/modal.md +25 -4
  53. package/docs/design-system/components/native-select.md +18 -0
  54. package/docs/design-system/components/navigation-menu.md +15 -0
  55. package/docs/design-system/components/onboarding-step-list-item.md +16 -1
  56. package/docs/design-system/components/page-header.md +17 -1
  57. package/docs/design-system/components/pagination.md +31 -8
  58. package/docs/design-system/components/popover.md +19 -0
  59. package/docs/design-system/components/progress-slider.md +18 -0
  60. package/docs/design-system/components/progress.md +17 -0
  61. package/docs/design-system/components/radio-card.md +16 -0
  62. package/docs/design-system/components/radio-group.md +17 -0
  63. package/docs/design-system/components/resizable.md +15 -0
  64. package/docs/design-system/components/scroll-area.md +20 -1
  65. package/docs/design-system/components/segmented-control.md +71 -0
  66. package/docs/design-system/components/select.md +44 -7
  67. package/docs/design-system/components/sheet.md +17 -0
  68. package/docs/design-system/components/sidebar.md +17 -0
  69. package/docs/design-system/components/skeleton.md +18 -0
  70. package/docs/design-system/components/slideout.md +17 -0
  71. package/docs/design-system/components/slider.md +20 -0
  72. package/docs/design-system/components/social-button.md +21 -3
  73. package/docs/design-system/components/spinner.md +16 -0
  74. package/docs/design-system/components/steps.md +19 -0
  75. package/docs/design-system/components/table.md +36 -4
  76. package/docs/design-system/components/tabs.md +17 -0
  77. package/docs/design-system/components/tag.md +17 -0
  78. package/docs/design-system/components/textarea.md +16 -0
  79. package/docs/design-system/components/timeline.md +17 -0
  80. package/docs/design-system/components/toast.md +17 -0
  81. package/docs/design-system/components/toggle.md +17 -0
  82. package/docs/design-system/components/toolbar.md +21 -1
  83. package/docs/design-system/components/tooltip.md +18 -1
  84. package/docs/design-system/density-and-elevation.md +61 -0
  85. package/docs/design-system/direction-and-localization.md +23 -0
  86. package/docs/design-system/form-recipes.md +73 -0
  87. package/docs/design-system/gap-promotion-policy.md +21 -0
  88. package/docs/design-system/gap-promotion-report.md +100 -0
  89. package/docs/design-system/installation.md +68 -0
  90. package/docs/design-system/launch-comparison-shadcn.md +98 -0
  91. package/docs/design-system/registry-and-cli.md +30 -7
  92. package/docs/design-system/release-policy.md +30 -0
  93. package/docs/design-system/roadmap.md +14 -0
  94. package/docs/design-system/support-matrix.md +23 -0
  95. package/docs/design-system/tabs.md +1 -1
  96. package/docs/design-system/token-governance.md +23 -1
  97. package/package.json +30 -15
  98. package/registry/component-contract.json +40 -0
  99. package/registry/components/alert-dialog.tsx +2 -2
  100. package/registry/components/alert.tsx +101 -76
  101. package/registry/components/autocomplete.tsx +23 -9
  102. package/registry/components/avatar.tsx +14 -10
  103. package/registry/components/badge.tsx +18 -18
  104. package/registry/components/button-group.tsx +324 -5
  105. package/registry/components/button.tsx +38 -17
  106. package/registry/components/card.tsx +87 -62
  107. package/registry/components/carousel.tsx +67 -5
  108. package/registry/components/chart.tsx +36 -3
  109. package/registry/components/checkbox.tsx +3 -3
  110. package/registry/components/collapsible.tsx +1 -1
  111. package/registry/components/command.tsx +38 -4
  112. package/registry/components/context-menu.tsx +1 -1
  113. package/registry/components/date-picker.tsx +312 -135
  114. package/registry/components/expanded/ActivityFeed.tsx +9 -22
  115. package/registry/components/expanded/ActivityHeatmap.tsx +64 -0
  116. package/registry/components/expanded/Banner.tsx +3 -4
  117. package/registry/components/expanded/Breadcrumbs.tsx +8 -5
  118. package/registry/components/expanded/CatalogComponentsShowcase.tsx +16 -17
  119. package/registry/components/expanded/CatalogDivider.tsx +4 -3
  120. package/registry/components/expanded/EmptyState.tsx +1 -1
  121. package/registry/components/expanded/FileUpload.tsx +16 -7
  122. package/registry/components/expanded/OnboardingStepListItem.tsx +2 -6
  123. package/registry/components/expanded/PageHeader.tsx +47 -56
  124. package/registry/components/expanded/Slideout.tsx +29 -424
  125. package/registry/components/expanded/Steps.tsx +151 -13
  126. package/registry/components/expanded/Table.tsx +161 -82
  127. package/registry/components/expanded/Timeline.tsx +14 -27
  128. package/registry/components/expanded/activityFeed.css +9 -9
  129. package/registry/components/expanded/activityHeatmap.css +66 -0
  130. package/registry/components/expanded/banner.css +21 -11
  131. package/registry/components/expanded/breadcrumbs.css +2 -2
  132. package/registry/components/expanded/divider.css +1 -1
  133. package/registry/components/expanded/emptyState.css +1 -1
  134. package/registry/components/expanded/fileUpload.css +7 -7
  135. package/registry/components/expanded/onboardingStepListItem.css +2 -2
  136. package/registry/components/expanded/pageHeader.css +14 -11
  137. package/registry/components/expanded/slideout.css +10 -690
  138. package/registry/components/expanded/steps.css +78 -15
  139. package/registry/components/expanded/table.css +60 -69
  140. package/registry/components/expanded/timeline.css +19 -18
  141. package/registry/components/field.tsx +61 -8
  142. package/registry/components/input-content.ts +1 -0
  143. package/registry/components/input-otp.tsx +13 -3
  144. package/registry/components/input.tsx +68 -43
  145. package/registry/components/item.tsx +189 -0
  146. package/registry/components/kbd.tsx +111 -4
  147. package/registry/components/menu.tsx +87 -251
  148. package/registry/components/menubar.tsx +1 -1
  149. package/registry/components/modal.tsx +143 -103
  150. package/registry/components/native-select.tsx +2 -2
  151. package/registry/components/navigation-menu.tsx +1 -1
  152. package/registry/components/pagination.css +36 -0
  153. package/registry/components/pagination.tsx +88 -64
  154. package/registry/components/popover.tsx +162 -664
  155. package/registry/components/progress-slider.tsx +1 -1
  156. package/registry/components/radio-card.tsx +116 -87
  157. package/registry/components/resizable.tsx +36 -2
  158. package/registry/components/scroll-area.tsx +30 -6
  159. package/registry/components/segmented-control.tsx +87 -0
  160. package/registry/components/select-content.tsx +2 -2
  161. package/registry/components/select.tsx +86 -27
  162. package/registry/components/sheet.tsx +1 -1
  163. package/registry/components/sidebar.tsx +12 -6
  164. package/registry/components/skeleton.css +0 -3
  165. package/registry/components/social-button.tsx +59 -30
  166. package/registry/components/spinner.tsx +18 -55
  167. package/registry/components/table-elements.tsx +128 -0
  168. package/registry/components/tabs.css +1 -1
  169. package/registry/components/tabs.tsx +13 -14
  170. package/registry/components/tag.tsx +3 -3
  171. package/registry/components/textarea.tsx +3 -3
  172. package/registry/components/toast.tsx +16 -11
  173. package/registry/components/toggle.tsx +4 -4
  174. package/registry/components/toolbar.tsx +94 -22
  175. package/registry/components/tooltip.tsx +2 -2
  176. package/registry/examples/alert-demo.tsx +25 -5
  177. package/registry/examples/attribute-demo.tsx +1 -1
  178. package/registry/examples/autocomplete-demo.tsx +3 -3
  179. package/registry/examples/avatar-demo.tsx +14 -11
  180. package/registry/examples/badge-demo.tsx +1 -1
  181. package/registry/examples/button-demo.tsx +6 -6
  182. package/registry/examples/button-group-demo.tsx +23 -2
  183. package/registry/examples/card-demo.tsx +46 -8
  184. package/registry/examples/carousel-demo.tsx +20 -2
  185. package/registry/examples/chart-demo.tsx +1 -1
  186. package/registry/examples/checkbox-demo.tsx +5 -5
  187. package/registry/examples/collection-filter-bar-demo.tsx +135 -0
  188. package/registry/examples/command-demo.tsx +3 -3
  189. package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
  190. package/registry/examples/context-menu-demo.tsx +1 -1
  191. package/registry/examples/control-density-contract-demo.tsx +61 -0
  192. package/registry/examples/date-picker-demo.tsx +12 -12
  193. package/registry/examples/divider-demo.tsx +3 -3
  194. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -0
  195. package/registry/examples/expanded/banner-demo.tsx +1 -1
  196. package/registry/examples/expanded/breadcrumbs-demo.tsx +4 -4
  197. package/registry/examples/expanded/empty-state-demo.tsx +1 -1
  198. package/registry/examples/expanded/file-upload-demo.tsx +1 -1
  199. package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
  200. package/registry/examples/expanded/slideout-demo.tsx +2 -2
  201. package/registry/examples/expanded/steps-demo.tsx +1 -1
  202. package/registry/examples/expanded/tabs-demo.tsx +1 -1
  203. package/registry/examples/expanded/timeline-demo.tsx +1 -1
  204. package/registry/examples/hover-card-demo.tsx +2 -2
  205. package/registry/examples/input-demo.tsx +6 -6
  206. package/registry/examples/input-otp-demo.tsx +4 -4
  207. package/registry/examples/item-demo.tsx +46 -0
  208. package/registry/examples/kbd-demo.tsx +1 -1
  209. package/registry/examples/label-demo.tsx +1 -1
  210. package/registry/examples/menu-demo.tsx +1 -1
  211. package/registry/examples/modal-demo.tsx +67 -9
  212. package/registry/examples/navigation-menu-demo.tsx +1 -1
  213. package/registry/examples/pagination-demo.tsx +10 -7
  214. package/registry/examples/popover-demo.tsx +19 -19
  215. package/registry/examples/progress-demo.tsx +1 -1
  216. package/registry/examples/progress-slider-demo.tsx +1 -1
  217. package/registry/examples/radio-card-demo.tsx +4 -4
  218. package/registry/examples/radio-group-demo.tsx +2 -2
  219. package/registry/examples/resizable-demo.tsx +1 -1
  220. package/registry/examples/results-toolbar-demo.tsx +44 -0
  221. package/registry/examples/scroll-area-demo.tsx +3 -3
  222. package/registry/examples/segmented-control-demo.tsx +39 -0
  223. package/registry/examples/select-demo.tsx +1 -1
  224. package/registry/examples/sidebar-demo.tsx +5 -3
  225. package/registry/examples/slider-demo.tsx +3 -3
  226. package/registry/examples/social-button-demo.tsx +18 -8
  227. package/registry/examples/spinner-demo.tsx +13 -13
  228. package/registry/examples/tabs-demo.tsx +1 -1
  229. package/registry/examples/tag-demo.tsx +1 -1
  230. package/registry/examples/textarea-demo.tsx +4 -4
  231. package/registry/examples/toast-demo.tsx +1 -1
  232. package/registry/examples/toggle-demo.tsx +6 -6
  233. package/registry/examples/tooltip-demo.tsx +4 -4
  234. package/registry/guidance/USAGE.md +27 -0
  235. package/registry/hooks/use-direction.ts +193 -13
  236. package/registry/index.json +5668 -375
  237. package/registry/styles/globals.css +2404 -3999
  238. package/registry.json +156 -149
  239. package/schema/config.schema.json +11 -8
  240. package/schema/registry.schema.json +150 -45
  241. package/schema/tokens.schema.json +134 -34
  242. package/src/cli/command-line.js +194 -0
  243. package/src/cli/config-editors.js +9 -0
  244. package/src/cli/doctor.js +399 -0
  245. package/src/cli/file-transaction.js +295 -0
  246. package/src/cli/global-css.js +153 -0
  247. package/src/cli/index.js +567 -1613
  248. package/src/cli/install-plan.js +592 -0
  249. package/src/cli/install-receipt.js +171 -0
  250. package/src/cli/jsonc.js +203 -0
  251. package/src/cli/package-install-state.js +254 -0
  252. package/src/cli/package-manager.js +238 -0
  253. package/src/cli/package-plan.js +185 -0
  254. package/src/cli/package-version.js +97 -0
  255. package/src/cli/registry-query.js +207 -0
  256. package/src/cli/reporting.js +55 -0
  257. package/src/cli/schema-validation.js +19 -0
  258. package/src/cli/source-scanner.js +686 -0
  259. package/src/cli/vite-config.js +478 -0
  260. package/tokens/banhaten.tokens.json +4444 -331
  261. package/docs/design-system/components/command-bar.md +0 -57
  262. package/registry/components/expanded/BannerBoard.tsx +0 -225
  263. package/registry/components/expanded/CatalogTag.tsx +0 -85
  264. package/registry/components/expanded/CommandBar.tsx +0 -386
  265. package/registry/components/expanded/IconExplorer.tsx +0 -612
  266. package/registry/components/expanded/TimelineShowcase.tsx +0 -158
  267. package/registry/components/expanded/commandBar.css +0 -470
  268. package/registry/components/expanded/iconExplorer.css +0 -764
  269. package/registry/components/expanded/iconPacks.ts +0 -866
  270. package/registry/components/expanded/tag.css +0 -148
  271. package/registry/examples/expanded/icons-demo.tsx +0 -5
  272. package/tokens/Color mode.zip +0 -0
  273. package/tokens/Numbers.zip +0 -0
  274. package/tokens/Radius.zip +0 -0
  275. package/tokens/Theme.zip +0 -0
@@ -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
  }
@@ -426,11 +283,12 @@ type MenuSubTriggerProps = React.ComponentProps<"div"> &
426
283
  }
427
284
 
428
285
  const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem({
286
+ "aria-checked": ariaChecked,
429
287
  className,
430
288
  kind = "default",
431
289
  disabled = false,
432
290
  asChild = false,
433
- role = "menuitem",
291
+ role,
434
292
  tabIndex,
435
293
  children,
436
294
  onSelect,
@@ -441,15 +299,23 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
441
299
  const isStaticMenu = React.useContext(MenuStaticContext)
442
300
  const selectedKind: MenuItemKind = kind ?? "default"
443
301
  const isDisabled = disabled
302
+ const switchState = findMenuItemSwitchState(children)
303
+ const resolvedRole = role ??
304
+ (switchState === undefined ? "menuitem" : "menuitemcheckbox")
305
+ const resolvedChecked =
306
+ resolvedRole === "menuitemcheckbox"
307
+ ? (ariaChecked ?? switchState ?? false)
308
+ : undefined
444
309
 
445
310
  const item = (
446
311
  <Comp
312
+ aria-checked={resolvedChecked}
447
313
  aria-disabled={isDisabled ? true : undefined}
448
314
  data-disabled={isDisabled ? "true" : "false"}
449
315
  data-kind={selectedKind}
450
316
  data-slot="menu-item"
451
317
  ref={ref}
452
- role={role}
318
+ role={resolvedRole}
453
319
  tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
454
320
  className={cn(menuItemVariants({ kind: selectedKind, className }))}
455
321
  {...props}
@@ -462,6 +328,20 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
462
328
 
463
329
  if (isStaticMenu) return item
464
330
 
331
+ if (switchState !== undefined) {
332
+ return (
333
+ <DropdownMenuPrimitive.CheckboxItem
334
+ asChild
335
+ checked={switchState}
336
+ disabled={isDisabled}
337
+ onSelect={onSelect}
338
+ textValue={textValue ?? getMenuItemTextValue(children)}
339
+ >
340
+ {item}
341
+ </DropdownMenuPrimitive.CheckboxItem>
342
+ )
343
+ }
344
+
465
345
  return (
466
346
  <DropdownMenuPrimitive.Item
467
347
  asChild
@@ -476,6 +356,27 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
476
356
 
477
357
  MenuItem.displayName = "MenuItem"
478
358
 
359
+ function findMenuItemSwitchState(children: React.ReactNode): boolean | undefined {
360
+ let state: boolean | undefined
361
+
362
+ React.Children.forEach(children, (child) => {
363
+ if (state !== undefined || !React.isValidElement(child)) return
364
+
365
+ const element = child as React.ReactElement<{
366
+ active?: boolean
367
+ children?: React.ReactNode
368
+ }>
369
+ if (element.type === MenuItemSwitch) {
370
+ state = Boolean(element.props.active)
371
+ return
372
+ }
373
+
374
+ state = findMenuItemSwitchState(element.props.children)
375
+ })
376
+
377
+ return state
378
+ }
379
+
479
380
  const MenuSubTrigger = React.forwardRef<HTMLDivElement, MenuSubTriggerProps>(function MenuSubTrigger({
480
381
  className,
481
382
  kind = "default",
@@ -719,7 +620,10 @@ function MenuItemBadge({
719
620
  )
720
621
  }
721
622
 
722
- type MenuItemSwitchProps = Omit<React.ComponentProps<"span">, "role"> & {
623
+ type MenuItemSwitchProps = Omit<
624
+ React.ComponentProps<"span">,
625
+ "aria-checked" | "aria-label" | "aria-labelledby" | "role"
626
+ > & {
723
627
  active?: boolean
724
628
  disabled?: boolean
725
629
  }
@@ -732,12 +636,10 @@ function MenuItemSwitch({
732
636
  }: MenuItemSwitchProps) {
733
637
  return (
734
638
  <span
735
- aria-checked={active}
736
- aria-disabled={disabled || undefined}
639
+ aria-hidden="true"
737
640
  data-active={active ? "true" : "false"}
738
641
  data-disabled={disabled ? "true" : "false"}
739
642
  data-slot="menu-item-switch"
740
- role="switch"
741
643
  className={cn(
742
644
  "flex h-[var(--bh-menu-switch-height)] w-[var(--bh-menu-switch-width)] shrink-0 items-center rounded-[var(--bh-radius-full)] p-[var(--bh-menu-switch-padding)] group-data-[disabled=true]/menu-item:bg-[var(--bh-interactive-switch-disabled)]",
743
645
  active
@@ -820,6 +722,8 @@ function MenuProgress({
820
722
  ...props
821
723
  }: MenuProgressProps) {
822
724
  const clampedValue = Math.min(100, Math.max(0, value))
725
+ const labelId = React.useId()
726
+ const hasLabel = hasRenderableContent(label)
823
727
 
824
728
  return (
825
729
  <div
@@ -831,6 +735,8 @@ function MenuProgress({
831
735
  {...props}
832
736
  >
833
737
  <div
738
+ aria-label={hasLabel ? undefined : "Progress"}
739
+ aria-labelledby={hasLabel ? labelId : undefined}
834
740
  aria-valuemax={100}
835
741
  aria-valuemin={0}
836
742
  aria-valuenow={clampedValue}
@@ -860,6 +766,7 @@ function MenuProgress({
860
766
  >
861
767
  {hasRenderableContent(label) ? (
862
768
  <span
769
+ id={labelId}
863
770
  data-slot="menu-progress-label-text"
864
771
  dir="auto"
865
772
  className="truncate text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)] leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] text-[var(--bh-content-default)]"
@@ -907,7 +814,7 @@ function MenuProgressSpinner({ className }: { className?: string }) {
907
814
  fill="none"
908
815
  stroke="currentColor"
909
816
  strokeLinecap="round"
910
- strokeWidth="3"
817
+ strokeWidth="var(--bh-icon-stroke-300)"
911
818
  />
912
819
  </svg>
913
820
  )
@@ -1025,81 +932,6 @@ function MenuSeparator({
1025
932
  return <DropdownMenuPrimitive.Separator asChild>{separator}</DropdownMenuPrimitive.Separator>
1026
933
  }
1027
934
 
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
935
  export {
1104
936
  Menu,
1105
937
  MenuCaption,
@@ -1117,12 +949,14 @@ export {
1117
949
  MenuItemTitle,
1118
950
  MenuLabel,
1119
951
  MenuProgress,
952
+ MenuPreview,
1120
953
  MenuPortal,
1121
954
  MenuRoot,
1122
955
  MenuSeparator,
1123
956
  MenuSub,
1124
957
  MenuSubContent,
1125
958
  MenuSubTrigger,
959
+ MenuSurface,
1126
960
  MenuTrigger,
1127
961
  menuItemContentVariants,
1128
962
  menuItemIconVariants,
@@ -1146,5 +980,7 @@ export type {
1146
980
  MenuLabelProps,
1147
981
  MenuProgressProps,
1148
982
  MenuProps,
983
+ MenuPreviewProps,
984
+ MenuSurfaceProps,
1149
985
  MenuWidth,
1150
986
  }
@@ -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>,