@bug-on/md3-react 2.0.3 → 3.0.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 (308) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/index.css.d.ts +2 -0
  4. package/dist/index.d.mts +6127 -0
  5. package/dist/index.d.ts +6127 -71
  6. package/dist/index.js +1653 -614
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1566 -547
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/material-symbols-cdn.css.d.ts +2 -0
  11. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  12. package/dist/typography.css.d.ts +2 -0
  13. package/package.json +22 -19
  14. package/scripts/copy-assets.js +82 -0
  15. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  16. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  17. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  18. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  19. package/src/assets/loading-indicator.svg +19 -0
  20. package/src/assets/material-symbols-cdn.css +65 -0
  21. package/src/assets/material-symbols-self-hosted.css +90 -0
  22. package/src/css.d.ts +20 -0
  23. package/src/hooks/useClickOutside.ts +37 -0
  24. package/src/hooks/useMediaQuery.ts +28 -0
  25. package/src/hooks/useRipple.ts +88 -0
  26. package/src/index.css +23 -0
  27. package/src/index.ts +349 -0
  28. package/src/lib/material-symbols-preconnect.tsx +82 -0
  29. package/src/lib/theme-utils.ts +180 -0
  30. package/src/lib/utils.ts +6 -0
  31. package/src/test/button.test.tsx +59 -0
  32. package/src/test/icon.test.tsx +91 -0
  33. package/src/test/loading-indicator.test.tsx +128 -0
  34. package/src/test/progress-indicator.test.tsx +306 -0
  35. package/src/test/setup.ts +80 -0
  36. package/src/test/typography.test.tsx +206 -0
  37. package/src/types/index.ts +7 -0
  38. package/src/types/md3.ts +31 -0
  39. package/src/ui/Text.tsx +60 -0
  40. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  41. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  42. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  43. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  44. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  45. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  46. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  47. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  48. package/src/ui/app-bar/app-bar.types.ts +441 -0
  49. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  50. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  51. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  52. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  53. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  54. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  55. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  56. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  57. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  58. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  59. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  60. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  61. package/src/ui/app-bar/search-view.tsx +227 -0
  62. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  63. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  64. package/src/ui/badge.test.tsx +345 -0
  65. package/src/ui/badge.tsx +282 -0
  66. package/src/ui/button-group.test.tsx +71 -0
  67. package/src/ui/button-group.tsx +350 -0
  68. package/src/ui/button.test.tsx +297 -0
  69. package/src/ui/button.tsx +669 -0
  70. package/src/ui/card.test.tsx +187 -0
  71. package/src/ui/card.tsx +259 -0
  72. package/src/ui/checkbox.test.tsx +423 -0
  73. package/src/ui/checkbox.tsx +525 -0
  74. package/src/ui/chip.test.tsx +292 -0
  75. package/src/ui/chip.tsx +548 -0
  76. package/src/ui/code-block.tsx +219 -0
  77. package/src/ui/dialog.test.tsx +300 -0
  78. package/src/ui/dialog.tsx +384 -0
  79. package/src/ui/divider.test.tsx +314 -0
  80. package/src/ui/divider.tsx +412 -0
  81. package/src/ui/drawer.tsx +240 -0
  82. package/src/ui/fab-menu.test.tsx +494 -0
  83. package/src/ui/fab-menu.tsx +739 -0
  84. package/src/ui/fab.test.tsx +232 -0
  85. package/src/ui/fab.tsx +505 -0
  86. package/src/ui/icon-button.test.tsx +515 -0
  87. package/src/ui/icon-button.tsx +525 -0
  88. package/src/ui/icon.test.tsx +197 -0
  89. package/src/ui/icon.tsx +179 -0
  90. package/src/ui/loading-indicator.test.tsx +73 -0
  91. package/src/ui/loading-indicator.tsx +312 -0
  92. package/src/ui/menu/context-menu.tsx +275 -0
  93. package/src/ui/menu/index.ts +77 -0
  94. package/src/ui/menu/menu-animations.ts +102 -0
  95. package/src/ui/menu/menu-context.tsx +99 -0
  96. package/src/ui/menu/menu-divider.tsx +47 -0
  97. package/src/ui/menu/menu-group.tsx +200 -0
  98. package/src/ui/menu/menu-item.tsx +294 -0
  99. package/src/ui/menu/menu-tokens.ts +208 -0
  100. package/src/ui/menu/menu-types.ts +313 -0
  101. package/src/ui/menu/menu.test.tsx +624 -0
  102. package/src/ui/menu/menu.tsx +289 -0
  103. package/src/ui/menu/sub-menu.tsx +223 -0
  104. package/src/ui/menu/vertical-menu.tsx +382 -0
  105. package/src/ui/navigation-rail.test.tsx +404 -0
  106. package/src/ui/navigation-rail.tsx +604 -0
  107. package/src/ui/progress-indicator/circular.tsx +248 -0
  108. package/src/ui/progress-indicator/hooks.ts +51 -0
  109. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  110. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  111. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  112. package/src/ui/progress-indicator/linear.tsx +143 -0
  113. package/src/ui/progress-indicator/types.ts +158 -0
  114. package/src/ui/progress-indicator/utils.ts +73 -0
  115. package/src/ui/radio-button.test.tsx +407 -0
  116. package/src/ui/radio-button.tsx +551 -0
  117. package/src/ui/ripple.test.tsx +72 -0
  118. package/src/ui/ripple.tsx +234 -0
  119. package/src/ui/scroll-area.test.tsx +58 -0
  120. package/src/ui/scroll-area.tsx +139 -0
  121. package/src/ui/search/animated-placeholder.tsx +145 -0
  122. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  123. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  124. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  125. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  126. package/src/ui/search/index.ts +44 -0
  127. package/src/ui/search/search-bar.tsx +220 -0
  128. package/src/ui/search/search-context.tsx +42 -0
  129. package/src/ui/search/search-view-docked.tsx +194 -0
  130. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  131. package/src/ui/search/search.test.tsx +233 -0
  132. package/src/ui/search/search.tokens.ts +134 -0
  133. package/src/ui/search/search.tsx +131 -0
  134. package/src/ui/search/search.types.ts +154 -0
  135. package/src/ui/search/trailing-action.tsx +49 -0
  136. package/src/ui/shared/constants.ts +122 -0
  137. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  138. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  139. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  140. package/src/ui/slider/range-slider.tsx +561 -0
  141. package/src/ui/slider/slider-thumb.tsx +379 -0
  142. package/src/ui/slider/slider-track.tsx +912 -0
  143. package/src/ui/slider/slider.tokens.ts +189 -0
  144. package/src/ui/slider/slider.tsx +259 -0
  145. package/src/ui/slider/slider.types.ts +288 -0
  146. package/src/ui/snackbar/index.ts +20 -0
  147. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  148. package/src/ui/snackbar/snackbar.tsx +476 -0
  149. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  150. package/src/ui/switch/switch.stories.tsx +309 -0
  151. package/src/ui/switch/switch.test.tsx +243 -0
  152. package/src/ui/switch/switch.tokens.ts +89 -0
  153. package/src/ui/switch/switch.tsx +504 -0
  154. package/src/ui/switch/switch.types.ts +62 -0
  155. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  156. package/src/ui/tabs/tab.tsx +407 -0
  157. package/src/ui/tabs/tabs-content.tsx +89 -0
  158. package/src/ui/tabs/tabs-list.tsx +146 -0
  159. package/src/ui/tabs/tabs.test.tsx +290 -0
  160. package/src/ui/tabs/tabs.tokens.ts +121 -0
  161. package/src/ui/tabs/tabs.tsx +229 -0
  162. package/src/ui/tabs/tabs.types.ts +185 -0
  163. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  164. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  165. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  166. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  167. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  168. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  169. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  170. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  171. package/src/ui/text-field/text-field.test.tsx +454 -0
  172. package/src/ui/text-field/text-field.tokens.ts +104 -0
  173. package/src/ui/text-field/text-field.tsx +548 -0
  174. package/src/ui/text-field/text-field.types.ts +180 -0
  175. package/src/ui/theme-provider/index.tsx +190 -0
  176. package/src/ui/toc.test.tsx +108 -0
  177. package/src/ui/toc.tsx +172 -0
  178. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  179. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  180. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  181. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  182. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  183. package/src/ui/tooltip/tooltip.types.ts +70 -0
  184. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  185. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  186. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  187. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  188. package/src/ui/typography/type-scale-tokens.ts +205 -0
  189. package/src/ui/typography/typography-key-tokens.ts +43 -0
  190. package/src/ui/typography/typography-tokens.ts +360 -0
  191. package/src/ui/typography/typography.css +22 -0
  192. package/src/ui/typography/typography.tsx +559 -0
  193. package/test-render.tsx +4 -0
  194. package/test-shadow.html +26 -0
  195. package/test_output.txt +164 -0
  196. package/test_output_v2.txt +5 -0
  197. package/tsconfig.build.json +10 -0
  198. package/tsconfig.json +18 -0
  199. package/tsup.config.ts +20 -0
  200. package/vitest.config.ts +11 -0
  201. package/dist/hooks/useClickOutside.d.ts +0 -8
  202. package/dist/hooks/useMediaQuery.d.ts +0 -11
  203. package/dist/hooks/useRipple.d.ts +0 -26
  204. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  205. package/dist/lib/theme-utils.d.ts +0 -63
  206. package/dist/lib/utils.d.ts +0 -2
  207. package/dist/types/index.d.ts +0 -1
  208. package/dist/types/md3.d.ts +0 -14
  209. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  210. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  211. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  212. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  213. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  214. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  215. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  216. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  217. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  218. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  219. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  220. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  221. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  222. package/dist/ui/app-bar/search-view.d.ts +0 -54
  223. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  224. package/dist/ui/badge.d.ts +0 -125
  225. package/dist/ui/button-group.d.ts +0 -59
  226. package/dist/ui/button.d.ts +0 -148
  227. package/dist/ui/card.d.ts +0 -62
  228. package/dist/ui/checkbox.d.ts +0 -82
  229. package/dist/ui/chip.d.ts +0 -110
  230. package/dist/ui/code-block.d.ts +0 -14
  231. package/dist/ui/dialog.d.ts +0 -111
  232. package/dist/ui/divider.d.ts +0 -164
  233. package/dist/ui/drawer.d.ts +0 -39
  234. package/dist/ui/dropdown.d.ts +0 -29
  235. package/dist/ui/fab-menu.d.ts +0 -204
  236. package/dist/ui/fab.d.ts +0 -162
  237. package/dist/ui/icon-button.d.ts +0 -131
  238. package/dist/ui/icon.d.ts +0 -88
  239. package/dist/ui/loading-indicator.d.ts +0 -42
  240. package/dist/ui/navigation-rail.d.ts +0 -29
  241. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  242. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  243. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  244. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  245. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  246. package/dist/ui/progress-indicator/types.d.ts +0 -151
  247. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  248. package/dist/ui/radio-button.d.ts +0 -106
  249. package/dist/ui/ripple.d.ts +0 -126
  250. package/dist/ui/scroll-area.d.ts +0 -27
  251. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  252. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  253. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  254. package/dist/ui/search/index.d.ts +0 -27
  255. package/dist/ui/search/search-bar.d.ts +0 -32
  256. package/dist/ui/search/search-context.d.ts +0 -24
  257. package/dist/ui/search/search-view-docked.d.ts +0 -25
  258. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  259. package/dist/ui/search/search.d.ts +0 -50
  260. package/dist/ui/search/search.tokens.d.ts +0 -112
  261. package/dist/ui/search/search.types.d.ts +0 -131
  262. package/dist/ui/search/trailing-action.d.ts +0 -9
  263. package/dist/ui/shared/constants.d.ts +0 -86
  264. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  265. package/dist/ui/slider/range-slider.d.ts +0 -47
  266. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  267. package/dist/ui/slider/slider-track.d.ts +0 -25
  268. package/dist/ui/slider/slider.d.ts +0 -60
  269. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  270. package/dist/ui/slider/slider.types.d.ts +0 -259
  271. package/dist/ui/snackbar/index.d.ts +0 -6
  272. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  273. package/dist/ui/switch/switch.d.ts +0 -30
  274. package/dist/ui/switch/switch.stories.d.ts +0 -48
  275. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  276. package/dist/ui/switch/switch.types.d.ts +0 -59
  277. package/dist/ui/tabs/tab.d.ts +0 -43
  278. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  279. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  280. package/dist/ui/tabs/tabs.d.ts +0 -60
  281. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  282. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  283. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  284. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  285. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  286. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  287. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  288. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  289. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  290. package/dist/ui/text-field/text-field.d.ts +0 -49
  291. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  292. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  293. package/dist/ui/theme-provider/index.d.ts +0 -48
  294. package/dist/ui/toc.d.ts +0 -80
  295. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  296. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  297. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  298. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  299. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  300. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  301. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  302. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  303. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  304. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  305. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  306. package/dist/ui/typography/typography.d.ts +0 -265
  307. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  308. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,382 @@
1
+ // ─── MD3 Expressive Menu — Vertical Menu (always-visible static variant) ──────
2
+ // Spec: SegmentedMenuTokens + MenuDefaults — two sub-variants:
3
+ // • Gap → gap-0.5 (2dp) between groups, no divider line
4
+ // • Divider → outline-variant <hr> between groups
5
+ //
6
+ // Key difference from Menu (baseline):
7
+ // • No portal, no popup animation, no trigger
8
+ // • Always rendered (static)
9
+ // • Shape morphing on hover works identically (reuses MenuGroup)
10
+ // • Still provides MenuContext with isStatic=true so MenuItems know to use Slot
11
+ //
12
+ // Vertical Menu Gap architecture:
13
+ // • Outer container: NO overflow-hidden (would clip group shape morphing!)
14
+ // Instead, elevation shadow is applied here, bg is transparent.
15
+ // • Each MenuGroup manages its own background + border-radius via Framer Motion.
16
+ // • The 2dp gap between groups is transparent — page background shows through.
17
+ //
18
+ // Vertical Menu Divider architecture:
19
+ // • Outer container applies containerBg + overflow-hidden + rounded-2xl
20
+ // (groups inside all stay flush, no shape morph needed for divider variant).
21
+ import * as React from "react";
22
+ import { cn } from "../../lib/utils";
23
+ import { MenuProvider, useMenuContext } from "./menu-context";
24
+ import { MenuGroup } from "./menu-group";
25
+ import {
26
+ MENU_GROUP_GAP,
27
+ MENU_MAX_WIDTH,
28
+ MENU_MIN_WIDTH,
29
+ STANDARD_COLORS,
30
+ VIBRANT_COLORS,
31
+ } from "./menu-tokens";
32
+ import type {
33
+ MenuGroupProps,
34
+ VerticalMenuContentProps,
35
+ VerticalMenuDividerProps,
36
+ VerticalMenuProps,
37
+ VerticalMenuSeparatorStyle,
38
+ } from "./menu-types";
39
+
40
+ // ─── VerticalMenuDivider ──────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * A plain horizontal divider for use between groups in a VerticalMenuContent
44
+ * with `separatorStyle="divider"`.
45
+ *
46
+ * Uses the same visual spec as MenuDivider (outline-variant color, 12dp
47
+ * horizontal padding, 2dp vertical padding) but is a plain `<hr>` — no Radix
48
+ * dependency, safe inside static (non-popup) contexts.
49
+ *
50
+ * @example
51
+ * <VerticalMenuContent separatorStyle="divider">
52
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
53
+ * <VerticalMenuDivider />
54
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
55
+ * </VerticalMenuContent>
56
+ */
57
+ export const VerticalMenuDivider = React.forwardRef<
58
+ HTMLHRElement,
59
+ VerticalMenuDividerProps & React.HTMLAttributes<HTMLHRElement>
60
+ >(({ className, index, count, isGapVariant, ...props }, ref) => (
61
+ <hr
62
+ ref={ref}
63
+ className={cn(
64
+ // HorizontalDividerPadding: horizontal=12dp, vertical=2dp
65
+ "mx-3 my-0.5",
66
+ // 1px height, no default border
67
+ "h-px border-0",
68
+ // Source: MenuDefaults.HorizontalDividerPadding / outline-variant
69
+ "bg-m3-outline-variant",
70
+ className,
71
+ )}
72
+ {...props}
73
+ />
74
+ ));
75
+ VerticalMenuDivider.displayName = "VerticalMenuDivider";
76
+
77
+ // ─── VerticalMenuGroup ────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * A group within a VerticalMenu.
81
+ *
82
+ * Functionally identical to `MenuGroup` — shape morphing on hover, auto-injected
83
+ * `itemPosition` into MenuItem children, `colorVariant` from context.
84
+ *
85
+ * This component is a named re-export of `MenuGroup` so consumers of Vertical
86
+ * Menu have a semantically clear API without any code duplication.
87
+ */
88
+ export const VerticalMenuGroup = MenuGroup;
89
+
90
+ // ─── VerticalMenuContent ──────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Renders a vertical list of VerticalMenuGroup children.
94
+ *
95
+ * Handles two separation styles:
96
+ * - `gap` → 2dp (`gap-0.5`) visual gap between groups (default)
97
+ * - `divider` → auto-inserts a `VerticalMenuDivider` between each pair of groups
98
+ *
99
+ * Auto-injects `index` and `count` props into VerticalMenuGroup children so
100
+ * that position-based shape morphing (leading/middle/trailing) works correctly.
101
+ *
102
+ * @example
103
+ * // Gap variant (default)
104
+ * <VerticalMenuContent separatorStyle="gap">
105
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
106
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
107
+ * </VerticalMenuContent>
108
+ *
109
+ * @example
110
+ * // Divider variant
111
+ * <VerticalMenuContent separatorStyle="divider">
112
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
113
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
114
+ * </VerticalMenuContent>
115
+ */
116
+ export const VerticalMenuContent = React.forwardRef<
117
+ HTMLDivElement,
118
+ VerticalMenuContentProps & React.HTMLAttributes<HTMLDivElement>
119
+ >(
120
+ (
121
+ {
122
+ children,
123
+ separatorStyle = "gap",
124
+ colorVariant: propColorVariant,
125
+ className,
126
+ ...props
127
+ },
128
+ ref,
129
+ ) => {
130
+ const { colorVariant: contextColorVariant } = useMenuContext();
131
+ const colorVariant = propColorVariant ?? contextColorVariant;
132
+ const colors =
133
+ colorVariant === "vibrant" ? VIBRANT_COLORS : STANDARD_COLORS;
134
+
135
+ // Helper to recursively flatten fragments and collect valid elements.
136
+ // This is necessary because cloneElement cannot be used on React.Fragment.
137
+ const flattenChildren = (
138
+ children: React.ReactNode,
139
+ ): React.ReactElement[] => {
140
+ return React.Children.toArray(children).reduce(
141
+ (acc: React.ReactElement[], child) => {
142
+ if (React.isValidElement(child)) {
143
+ if (child.type === React.Fragment) {
144
+ return acc.concat(
145
+ flattenChildren(
146
+ (child as React.ReactElement<{ children?: React.ReactNode }>)
147
+ .props.children,
148
+ ),
149
+ );
150
+ }
151
+ acc.push(child as React.ReactElement);
152
+ }
153
+ return acc;
154
+ },
155
+ [],
156
+ );
157
+ };
158
+
159
+ // Collect only valid VerticalMenuGroup-like elements for index/count injection
160
+ const validChildren = flattenChildren(children);
161
+ const groupCount = validChildren.length;
162
+
163
+ // Auto-inject `index` and `count` into each child (enables leading/middle/trailing shape)
164
+ const enhancedChildren = validChildren.map((child, i) =>
165
+ React.cloneElement(child as React.ReactElement<MenuGroupProps>, {
166
+ index: i,
167
+ count: groupCount,
168
+ isGapVariant: separatorStyle === "gap",
169
+ }),
170
+ );
171
+
172
+ // For divider style: interleave VerticalMenuDivider between each group
173
+ const renderedChildren =
174
+ separatorStyle === "divider"
175
+ ? enhancedChildren.reduce<React.ReactNode[]>((acc, child, i) => {
176
+ if (i > 0) {
177
+ acc.push(
178
+ <VerticalMenuDivider
179
+ key={`divider-${(child as React.ReactElement).key || i}`}
180
+ />,
181
+ );
182
+ }
183
+ acc.push(child);
184
+ return acc;
185
+ }, [])
186
+ : enhancedChildren;
187
+
188
+ return (
189
+ <div
190
+ ref={ref}
191
+ className={cn(
192
+ "flex flex-col w-full",
193
+ // Gap variant: transparent background + 2dp gap — page bg shows through gaps.
194
+ // Divider variant: solid container background behind all groups.
195
+ separatorStyle === "gap"
196
+ ? cn("bg-transparent", MENU_GROUP_GAP)
197
+ : colors.containerBg,
198
+ className,
199
+ )}
200
+ {...props}
201
+ >
202
+ {renderedChildren}
203
+ </div>
204
+ );
205
+ },
206
+ );
207
+ VerticalMenuContent.displayName = "VerticalMenuContent";
208
+
209
+ // ─── VerticalMenu (Root) ──────────────────────────────────────────────────────
210
+
211
+ /**
212
+ * Root of an always-visible vertical menu.
213
+ *
214
+ * Wraps children in `MenuProvider` (always `open={true}`) so MenuItem and
215
+ * MenuGroup components receive the correct `colorVariant` from context.
216
+ *
217
+ * Unlike the popup `Menu` component, there is no Radix DropdownMenu, no portal,
218
+ * and no enter/exit animation — the list is statically rendered at all times.
219
+ *
220
+ * ### Shape morphing
221
+ * For the **gap variant**, the outer container has NO `overflow-hidden` — this is
222
+ * intentional! Each `VerticalMenuGroup` manages its own shape via Framer Motion's
223
+ * `animate.borderRadius`. `overflow-hidden` on the parent would clip these
224
+ * morphing corners. The 2dp transparent gap lets page background show through.
225
+ *
226
+ * For the **divider variant**, the outer container applies `overflow-hidden` +
227
+ * `rounded-2xl` + background — groups sit flush inside without morphing.
228
+ *
229
+ * @example
230
+ * // Vertical Menu with Gap (floating segments)
231
+ * <VerticalMenu colorVariant="standard">
232
+ * <VerticalMenuContent separatorStyle="gap">
233
+ * <VerticalMenuGroup>
234
+ * <MenuItem leadingIcon={<Icon name="visibility" size={20} />}>Item 1</MenuItem>
235
+ * <MenuItem leadingIcon={<Icon name="content_copy" size={20} />} trailingText="⌘C">Item 2</MenuItem>
236
+ * <MenuItem selected leadingIcon={<Icon name="edit" size={20} />}>Item 3</MenuItem>
237
+ * </VerticalMenuGroup>
238
+ * <VerticalMenuGroup>
239
+ * <MenuItem trailingIcon={<Icon name="chevron_right" size={20} />}>Item 4</MenuItem>
240
+ * </VerticalMenuGroup>
241
+ * </VerticalMenuContent>
242
+ * </VerticalMenu>
243
+ *
244
+ * @example
245
+ * // Vertical Menu with Divider
246
+ * <VerticalMenu colorVariant="standard">
247
+ * <VerticalMenuContent separatorStyle="divider">
248
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
249
+ * <VerticalMenuGroup>...</VerticalMenuGroup>
250
+ * </VerticalMenuContent>
251
+ * </VerticalMenu>
252
+ */
253
+ export const VerticalMenu = React.forwardRef<
254
+ HTMLDivElement,
255
+ VerticalMenuProps & React.HTMLAttributes<HTMLDivElement>
256
+ >(({ children, colorVariant = "standard", className, ...props }, ref) => {
257
+ // Vertical menus are always visible — `open` is a permanent true.
258
+ // onOpenChange is a no-op (no controlled/uncontrolled toggle needed).
259
+ const noop = React.useCallback(() => {}, []);
260
+
261
+ const colors = colorVariant === "vibrant" ? VIBRANT_COLORS : STANDARD_COLORS;
262
+
263
+ // Internal ref for keyboard navigation — merged with forwarded ref.
264
+ const containerRef = React.useRef<HTMLDivElement>(null);
265
+ const mergedRef = React.useCallback(
266
+ (node: HTMLDivElement | null) => {
267
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current =
268
+ node;
269
+ if (typeof ref === "function") ref(node);
270
+ else if (ref)
271
+ (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
272
+ },
273
+ [ref],
274
+ );
275
+
276
+ // Arrow key navigation: WAI-ARIA composite widget pattern.
277
+ // Moves focus among menuitem elements, skipping disabled ones.
278
+ const handleKeyDown = React.useCallback(
279
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
280
+ if (!containerRef.current) return;
281
+ const items = Array.from(
282
+ containerRef.current.querySelectorAll<HTMLElement>(
283
+ '[role="menuitem"]:not([aria-disabled="true"]):not([tabindex="-1"]),' +
284
+ '[role="menuitemcheckbox"]:not([aria-disabled="true"]):not([tabindex="-1"]),' +
285
+ '[role="menuitemradio"]:not([aria-disabled="true"]):not([tabindex="-1"])',
286
+ ),
287
+ );
288
+ if (!items.length) return;
289
+ const idx = items.indexOf(document.activeElement as HTMLElement);
290
+ let next: number | null = null;
291
+ switch (e.key) {
292
+ case "ArrowDown":
293
+ e.preventDefault();
294
+ next = idx < items.length - 1 ? idx + 1 : 0;
295
+ break;
296
+ case "ArrowUp":
297
+ e.preventDefault();
298
+ next = idx > 0 ? idx - 1 : items.length - 1;
299
+ break;
300
+ case "Home":
301
+ e.preventDefault();
302
+ next = 0;
303
+ break;
304
+ case "End":
305
+ e.preventDefault();
306
+ next = items.length - 1;
307
+ break;
308
+ default:
309
+ return;
310
+ }
311
+ if (next !== null) items[next]?.focus();
312
+ },
313
+ [],
314
+ );
315
+
316
+ // Detect separator style from VerticalMenuContent child to decide container styling.
317
+ const separatorStyle = detectSeparatorStyle(children);
318
+ const isGapVariant = separatorStyle !== "divider";
319
+
320
+ return (
321
+ <MenuProvider
322
+ variant="expressive"
323
+ colorVariant={colorVariant}
324
+ menuPrimitive="static"
325
+ open={true}
326
+ onOpenChange={noop}
327
+ >
328
+ <div
329
+ ref={mergedRef}
330
+ role="menu"
331
+ aria-orientation="vertical"
332
+ onKeyDown={handleKeyDown}
333
+ className={cn(
334
+ // Width constraints: 112dp min, 280dp max (MenuTokens)
335
+ MENU_MIN_WIDTH,
336
+ MENU_MAX_WIDTH,
337
+ "flex flex-col",
338
+ isGapVariant
339
+ ? [
340
+ // GAP VARIANT: NO overflow-hidden — groups must morph freely.
341
+ "outline-none",
342
+ // NO background — transparent between segments.
343
+ // NO rounded corners — each group manages its own shape.
344
+ // Elevation is managed by each individual group.
345
+ ]
346
+ : [
347
+ // DIVIDER VARIANT: Container clips the content.
348
+ "rounded-2xl",
349
+ "overflow-hidden",
350
+ colors.containerBg,
351
+ "elevation-2",
352
+ "outline-none",
353
+ ],
354
+ className,
355
+ )}
356
+ {...props}
357
+ >
358
+ {children}
359
+ </div>
360
+ </MenuProvider>
361
+ );
362
+ });
363
+ VerticalMenu.displayName = "VerticalMenu";
364
+
365
+ // ─── Helper ───────────────────────────────────────────────────────────────────
366
+
367
+ /**
368
+ * Inspects the `separatorStyle` prop from the first VerticalMenuContent child.
369
+ * Used by VerticalMenu root to decide container styling.
370
+ */
371
+ function detectSeparatorStyle(
372
+ children: React.ReactNode,
373
+ ): VerticalMenuSeparatorStyle {
374
+ const child = React.Children.toArray(children).find(React.isValidElement);
375
+ if (child) {
376
+ const style = (
377
+ child.props as { separatorStyle?: VerticalMenuSeparatorStyle }
378
+ ).separatorStyle;
379
+ if (style) return style;
380
+ }
381
+ return "gap";
382
+ }