@bug-on/md3-react 2.0.2 → 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 (296) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/index.css +23 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6127 -0
  6. package/dist/index.d.ts +6127 -69
  7. package/dist/index.js +2536 -665
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2443 -603
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/material-symbols-cdn.css.d.ts +2 -0
  12. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  13. package/dist/typography.css.d.ts +2 -0
  14. package/package.json +23 -19
  15. package/scripts/copy-assets.js +82 -0
  16. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  17. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  18. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  19. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  20. package/src/assets/loading-indicator.svg +19 -0
  21. package/src/assets/material-symbols-cdn.css +65 -0
  22. package/src/assets/material-symbols-self-hosted.css +90 -0
  23. package/src/css.d.ts +20 -0
  24. package/{dist/hooks/index.d.ts → src/hooks/index.ts} +1 -0
  25. package/src/hooks/useClickOutside.ts +37 -0
  26. package/src/hooks/useMediaQuery.ts +28 -0
  27. package/src/hooks/useRipple.ts +88 -0
  28. package/src/index.css +23 -0
  29. package/src/index.ts +349 -0
  30. package/src/lib/material-symbols-preconnect.tsx +82 -0
  31. package/src/lib/theme-utils.ts +180 -0
  32. package/src/lib/utils.ts +6 -0
  33. package/src/test/button.test.tsx +59 -0
  34. package/src/test/icon.test.tsx +91 -0
  35. package/src/test/loading-indicator.test.tsx +128 -0
  36. package/src/test/progress-indicator.test.tsx +306 -0
  37. package/src/test/setup.ts +80 -0
  38. package/src/test/typography.test.tsx +206 -0
  39. package/src/types/index.ts +7 -0
  40. package/src/types/md3.ts +31 -0
  41. package/src/ui/Text.tsx +60 -0
  42. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  43. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  44. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  45. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  46. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  47. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  48. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  49. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  50. package/src/ui/app-bar/app-bar.types.ts +441 -0
  51. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  52. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  53. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  54. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  55. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  56. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  57. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  58. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  59. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  60. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  61. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  62. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  63. package/src/ui/app-bar/search-view.tsx +227 -0
  64. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  65. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  66. package/src/ui/badge.test.tsx +345 -0
  67. package/src/ui/badge.tsx +282 -0
  68. package/src/ui/button-group.test.tsx +71 -0
  69. package/src/ui/button-group.tsx +350 -0
  70. package/src/ui/button.test.tsx +297 -0
  71. package/src/ui/button.tsx +669 -0
  72. package/src/ui/card.test.tsx +187 -0
  73. package/src/ui/card.tsx +259 -0
  74. package/src/ui/checkbox.test.tsx +423 -0
  75. package/src/ui/checkbox.tsx +525 -0
  76. package/src/ui/chip.test.tsx +292 -0
  77. package/src/ui/chip.tsx +548 -0
  78. package/src/ui/code-block.tsx +219 -0
  79. package/src/ui/dialog.test.tsx +300 -0
  80. package/src/ui/dialog.tsx +384 -0
  81. package/src/ui/divider.test.tsx +314 -0
  82. package/src/ui/divider.tsx +412 -0
  83. package/src/ui/drawer.tsx +240 -0
  84. package/src/ui/fab-menu.test.tsx +494 -0
  85. package/src/ui/fab-menu.tsx +739 -0
  86. package/src/ui/fab.test.tsx +232 -0
  87. package/src/ui/fab.tsx +505 -0
  88. package/src/ui/icon-button.test.tsx +515 -0
  89. package/src/ui/icon-button.tsx +525 -0
  90. package/src/ui/icon.test.tsx +197 -0
  91. package/src/ui/icon.tsx +179 -0
  92. package/src/ui/loading-indicator.test.tsx +73 -0
  93. package/src/ui/loading-indicator.tsx +312 -0
  94. package/src/ui/menu/context-menu.tsx +275 -0
  95. package/src/ui/menu/index.ts +77 -0
  96. package/src/ui/menu/menu-animations.ts +102 -0
  97. package/src/ui/menu/menu-context.tsx +99 -0
  98. package/src/ui/menu/menu-divider.tsx +47 -0
  99. package/src/ui/menu/menu-group.tsx +200 -0
  100. package/src/ui/menu/menu-item.tsx +294 -0
  101. package/src/ui/menu/menu-tokens.ts +208 -0
  102. package/src/ui/menu/menu-types.ts +313 -0
  103. package/src/ui/menu/menu.test.tsx +624 -0
  104. package/src/ui/menu/menu.tsx +289 -0
  105. package/src/ui/menu/sub-menu.tsx +223 -0
  106. package/src/ui/menu/vertical-menu.tsx +382 -0
  107. package/src/ui/navigation-rail.test.tsx +404 -0
  108. package/src/ui/navigation-rail.tsx +604 -0
  109. package/src/ui/progress-indicator/circular.tsx +248 -0
  110. package/src/ui/progress-indicator/hooks.ts +51 -0
  111. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  112. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  113. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  114. package/src/ui/progress-indicator/linear.tsx +143 -0
  115. package/src/ui/progress-indicator/types.ts +158 -0
  116. package/src/ui/progress-indicator/utils.ts +73 -0
  117. package/src/ui/radio-button.test.tsx +407 -0
  118. package/src/ui/radio-button.tsx +551 -0
  119. package/src/ui/ripple.test.tsx +72 -0
  120. package/src/ui/ripple.tsx +234 -0
  121. package/src/ui/scroll-area.test.tsx +58 -0
  122. package/src/ui/scroll-area.tsx +139 -0
  123. package/src/ui/search/animated-placeholder.tsx +145 -0
  124. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  125. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  126. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  127. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  128. package/src/ui/search/index.ts +44 -0
  129. package/src/ui/search/search-bar.tsx +220 -0
  130. package/src/ui/search/search-context.tsx +42 -0
  131. package/src/ui/search/search-view-docked.tsx +194 -0
  132. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  133. package/src/ui/search/search.test.tsx +233 -0
  134. package/src/ui/search/search.tokens.ts +134 -0
  135. package/src/ui/search/search.tsx +131 -0
  136. package/src/ui/search/search.types.ts +154 -0
  137. package/src/ui/search/trailing-action.tsx +49 -0
  138. package/src/ui/shared/constants.ts +122 -0
  139. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  140. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  141. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  142. package/src/ui/slider/range-slider.tsx +561 -0
  143. package/src/ui/slider/slider-thumb.tsx +379 -0
  144. package/src/ui/slider/slider-track.tsx +912 -0
  145. package/src/ui/slider/slider.tokens.ts +189 -0
  146. package/src/ui/slider/slider.tsx +259 -0
  147. package/src/ui/slider/slider.types.ts +288 -0
  148. package/src/ui/snackbar/index.ts +20 -0
  149. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  150. package/src/ui/snackbar/snackbar.tsx +476 -0
  151. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  152. package/src/ui/switch/switch.stories.tsx +309 -0
  153. package/src/ui/switch/switch.test.tsx +243 -0
  154. package/src/ui/switch/switch.tokens.ts +89 -0
  155. package/src/ui/switch/switch.tsx +504 -0
  156. package/src/ui/switch/switch.types.ts +62 -0
  157. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  158. package/src/ui/tabs/tab.tsx +407 -0
  159. package/src/ui/tabs/tabs-content.tsx +89 -0
  160. package/src/ui/tabs/tabs-list.tsx +146 -0
  161. package/src/ui/tabs/tabs.test.tsx +290 -0
  162. package/src/ui/tabs/tabs.tokens.ts +121 -0
  163. package/src/ui/tabs/tabs.tsx +229 -0
  164. package/src/ui/tabs/tabs.types.ts +185 -0
  165. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  166. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  167. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  168. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  169. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  170. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  171. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  172. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  173. package/src/ui/text-field/text-field.test.tsx +454 -0
  174. package/src/ui/text-field/text-field.tokens.ts +104 -0
  175. package/src/ui/text-field/text-field.tsx +548 -0
  176. package/src/ui/text-field/text-field.types.ts +180 -0
  177. package/src/ui/theme-provider/index.tsx +190 -0
  178. package/src/ui/toc.test.tsx +108 -0
  179. package/src/ui/toc.tsx +172 -0
  180. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  181. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  182. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  183. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  184. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  185. package/src/ui/tooltip/tooltip.types.ts +70 -0
  186. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  187. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  188. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  189. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  190. package/src/ui/typography/type-scale-tokens.ts +205 -0
  191. package/src/ui/typography/typography-key-tokens.ts +43 -0
  192. package/src/ui/typography/typography-tokens.ts +360 -0
  193. package/src/ui/typography/typography.css +22 -0
  194. package/src/ui/typography/typography.tsx +559 -0
  195. package/test-render.tsx +4 -0
  196. package/test-shadow.html +26 -0
  197. package/test_output.txt +164 -0
  198. package/test_output_v2.txt +5 -0
  199. package/tsconfig.build.json +10 -0
  200. package/tsconfig.json +18 -0
  201. package/tsup.config.ts +20 -0
  202. package/vitest.config.ts +11 -0
  203. package/dist/hooks/useMediaQuery.d.ts +0 -11
  204. package/dist/hooks/useRipple.d.ts +0 -26
  205. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  206. package/dist/lib/theme-utils.d.ts +0 -63
  207. package/dist/lib/utils.d.ts +0 -2
  208. package/dist/types/index.d.ts +0 -1
  209. package/dist/types/md3.d.ts +0 -14
  210. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  211. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  212. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  213. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  214. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  215. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  216. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  217. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  218. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  219. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  220. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  221. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  222. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  223. package/dist/ui/app-bar/search-view.d.ts +0 -54
  224. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  225. package/dist/ui/badge.d.ts +0 -125
  226. package/dist/ui/button-group.d.ts +0 -59
  227. package/dist/ui/button.d.ts +0 -148
  228. package/dist/ui/card.d.ts +0 -62
  229. package/dist/ui/checkbox.d.ts +0 -82
  230. package/dist/ui/chip.d.ts +0 -110
  231. package/dist/ui/code-block.d.ts +0 -14
  232. package/dist/ui/dialog.d.ts +0 -111
  233. package/dist/ui/divider.d.ts +0 -164
  234. package/dist/ui/drawer.d.ts +0 -39
  235. package/dist/ui/dropdown.d.ts +0 -29
  236. package/dist/ui/fab-menu.d.ts +0 -204
  237. package/dist/ui/fab.d.ts +0 -162
  238. package/dist/ui/icon-button.d.ts +0 -131
  239. package/dist/ui/icon.d.ts +0 -88
  240. package/dist/ui/loading-indicator.d.ts +0 -42
  241. package/dist/ui/navigation-rail.d.ts +0 -29
  242. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  243. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  244. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  245. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  246. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  247. package/dist/ui/progress-indicator/types.d.ts +0 -151
  248. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  249. package/dist/ui/radio-button.d.ts +0 -106
  250. package/dist/ui/ripple.d.ts +0 -126
  251. package/dist/ui/scroll-area.d.ts +0 -27
  252. package/dist/ui/shared/constants.d.ts +0 -86
  253. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  254. package/dist/ui/slider/range-slider.d.ts +0 -47
  255. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  256. package/dist/ui/slider/slider-track.d.ts +0 -25
  257. package/dist/ui/slider/slider.d.ts +0 -60
  258. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  259. package/dist/ui/slider/slider.types.d.ts +0 -259
  260. package/dist/ui/snackbar/index.d.ts +0 -6
  261. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  262. package/dist/ui/switch/switch.d.ts +0 -30
  263. package/dist/ui/switch/switch.stories.d.ts +0 -48
  264. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  265. package/dist/ui/switch/switch.types.d.ts +0 -59
  266. package/dist/ui/tabs/tab.d.ts +0 -43
  267. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  268. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  269. package/dist/ui/tabs/tabs.d.ts +0 -60
  270. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  271. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  272. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  273. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  274. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  275. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  276. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  277. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  278. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  279. package/dist/ui/text-field/text-field.d.ts +0 -49
  280. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  281. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  282. package/dist/ui/theme-provider/index.d.ts +0 -48
  283. package/dist/ui/toc.d.ts +0 -80
  284. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  285. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  286. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  287. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  288. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  289. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  290. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  291. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  292. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  293. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  294. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  295. package/dist/ui/typography/typography.d.ts +0 -265
  296. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,102 @@
1
+ // ─── MD3 Expressive Menu — Framer Motion Animation Variants ──────────────────
2
+ // FastSpatial: mirrors MotionSchemeKeyTokens.FastSpatial
3
+ // Android: spring(stiffness=380, dampingRatio=0.7)
4
+ // Framer: damping = 2 × 0.7 × √(380 × 1) ≈ 27.3 → use 28
5
+ // FastEffects: mirrors MotionSchemeKeyTokens.FastEffects
6
+ // Android: duration=150ms, FastOutLinearIn
7
+ // Framer: duration=0.15, ease=[0.4, 0, 1, 1]
8
+
9
+ import type { Transition, Variants } from "motion/react";
10
+
11
+ // ─── Shared spring/easing specs ───────────────────────────────────────────────
12
+
13
+ /** FastSpatial spring — used for shape morphing and spatial enter animations */
14
+ export const FAST_SPATIAL_SPRING: Transition = {
15
+ type: "spring",
16
+ stiffness: 380,
17
+ damping: 28,
18
+ mass: 1,
19
+ };
20
+
21
+ /** FastEffects transition — used for opacity and exit animations */
22
+ export const FAST_EFFECTS_TRANSITION: Transition = {
23
+ duration: 0.15,
24
+ ease: [0.4, 0, 1, 1], // FastOutLinearIn
25
+ };
26
+
27
+ // ─── Menu popup container ─────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Enter/exit animation for the menu popup container.
31
+ * Scale from 0.8→1.0 with FastSpatial spring.
32
+ * Transform-origin is driven by the Radix CSS variable
33
+ * `--radix-dropdown-menu-content-transform-origin`.
34
+ */
35
+ export const MENU_CONTAINER_VARIANTS: Variants = {
36
+ hidden: {
37
+ opacity: 0,
38
+ scale: 0.8,
39
+ },
40
+ visible: {
41
+ opacity: 1,
42
+ scale: 1,
43
+ transition: FAST_SPATIAL_SPRING,
44
+ },
45
+ exit: {
46
+ opacity: 0,
47
+ scale: 0.8,
48
+ transition: FAST_EFFECTS_TRANSITION,
49
+ },
50
+ };
51
+
52
+ // ─── Selected check icon ──────────────────────────────────────────────────────
53
+
54
+ /** Size of the check icon in px (20dp per SegmentedMenuTokens.ItemLeadingIconSize) */
55
+ export const MENU_CHECK_ICON_SIZE = 20;
56
+
57
+ /**
58
+ * Expand/collapse animation for the check icon that appears when a MenuItem is
59
+ * selected. Uses horizontal expansion (width + marginInlineEnd) with FastSpatial.
60
+ *
61
+ * ONLY used for the animated check ↔ selectedIcon swap in selectable items.
62
+ * Regular (static) leading icons should NOT use these variants.
63
+ */
64
+ export const CHECK_ICON_VARIANTS: Variants = {
65
+ hidden: {
66
+ opacity: 0,
67
+ width: 0,
68
+ },
69
+ visible: {
70
+ opacity: 1,
71
+ width: MENU_CHECK_ICON_SIZE,
72
+ transition: FAST_SPATIAL_SPRING,
73
+ },
74
+ exit: {
75
+ opacity: 0,
76
+ width: 0,
77
+ transition: FAST_EFFECTS_TRANSITION,
78
+ },
79
+ };
80
+
81
+ // ─── SubMenu content ──────────────────────────────────────────────────────────
82
+
83
+ /** SubMenu popup uses the same FastSpatial/FastEffects pattern as root menu */
84
+ export const SUBMENU_CONTAINER_VARIANTS: Variants = {
85
+ hidden: {
86
+ opacity: 0,
87
+ scale: 0.9,
88
+ x: -4,
89
+ },
90
+ visible: {
91
+ opacity: 1,
92
+ scale: 1,
93
+ x: 0,
94
+ transition: FAST_SPATIAL_SPRING,
95
+ },
96
+ exit: {
97
+ opacity: 0,
98
+ scale: 0.9,
99
+ x: -4,
100
+ transition: FAST_EFFECTS_TRANSITION,
101
+ },
102
+ };
@@ -0,0 +1,99 @@
1
+ // ─── MD3 Expressive Menu — React Context ──────────────────────────────────────
2
+ import * as React from "react";
3
+ import type {
4
+ MenuColorVariant,
5
+ MenuPrimitive,
6
+ MenuVariant,
7
+ } from "./menu-types";
8
+
9
+ // ─── Context shape ────────────────────────────────────────────────────────────
10
+
11
+ interface MenuContextValue {
12
+ /** Visual variant: baseline (M3 standard) or expressive (shape-morphing) */
13
+ variant: MenuVariant;
14
+ /** Color variant inherited by all children unless overridden */
15
+ colorVariant: MenuColorVariant;
16
+ /**
17
+ * Which Radix primitive family drives this menu:
18
+ * - "dropdown" → @radix-ui/react-dropdown-menu (button/field trigger)
19
+ * - "context" → @radix-ui/react-context-menu (right-click trigger)
20
+ * - "static" → plain HTML via Slot (VerticalMenu, always-visible)
21
+ */
22
+ menuPrimitive: MenuPrimitive;
23
+ /**
24
+ * Whether the menu popup is currently open.
25
+ * Used by MenuContent to drive AnimatePresence for exit animations.
26
+ */
27
+ open: boolean;
28
+ /** Setter forwarded from Menu root — kept in sync with Radix Root open state */
29
+ onOpenChange: (open: boolean) => void;
30
+ }
31
+
32
+ // ─── Backward-compat derived getter ───────────────────────────────────────────
33
+ // Components that still reference `isStatic` (MenuGroup, etc.) use this helper
34
+ // during the incremental migration period.
35
+ export function isStaticPrimitive(primitive: MenuPrimitive): boolean {
36
+ return primitive === "static";
37
+ }
38
+
39
+ // ─── Context ──────────────────────────────────────────────────────────────────
40
+
41
+ const MenuContext = React.createContext<MenuContextValue>({
42
+ variant: "baseline",
43
+ colorVariant: "standard",
44
+ menuPrimitive: "dropdown",
45
+ open: false,
46
+ onOpenChange: () => {},
47
+ });
48
+
49
+ // ─── Provider ─────────────────────────────────────────────────────────────────
50
+
51
+ export interface MenuProviderProps {
52
+ variant?: MenuVariant;
53
+ colorVariant?: MenuColorVariant;
54
+ menuPrimitive?: MenuPrimitive;
55
+ open: boolean;
56
+ onOpenChange: (open: boolean) => void;
57
+ children: React.ReactNode;
58
+ }
59
+
60
+ export function MenuProvider({
61
+ variant = "baseline",
62
+ colorVariant = "standard",
63
+ menuPrimitive = "dropdown",
64
+ open,
65
+ onOpenChange,
66
+ children,
67
+ }: MenuProviderProps) {
68
+ const value = React.useMemo<MenuContextValue>(
69
+ () => ({ variant, colorVariant, menuPrimitive, open, onOpenChange }),
70
+ [variant, colorVariant, menuPrimitive, open, onOpenChange],
71
+ );
72
+
73
+ return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;
74
+ }
75
+
76
+ // ─── Hook ─────────────────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * Returns the nearest MenuContext value.
80
+ * Safe to use outside a MenuProvider (returns default: baseline, dropdown, closed).
81
+ *
82
+ * Includes backward-compat shims:
83
+ * - `menuVariant` → alias for `variant` (deprecated, will be removed)
84
+ * - `isStatic` → `menuPrimitive === "static"`
85
+ */
86
+ export function useMenuContext(): MenuContextValue & {
87
+ isStatic: boolean;
88
+ menuVariant: MenuVariant;
89
+ } {
90
+ const ctx = React.useContext(MenuContext);
91
+ return React.useMemo(
92
+ () => ({
93
+ ...ctx,
94
+ isStatic: ctx.menuPrimitive === "static",
95
+ menuVariant: ctx.variant,
96
+ }),
97
+ [ctx],
98
+ );
99
+ }
@@ -0,0 +1,47 @@
1
+ // ─── MD3 Expressive Menu — MenuDivider ───────────────────────────────────────
2
+ // Spec: HorizontalDividerPadding = PaddingValues(horizontal = 12.dp, vertical = 2.dp)
3
+ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
4
+ import * as React from "react";
5
+ import { cn } from "../../lib/utils";
6
+ import { useMenuContext } from "./menu-context";
7
+ import type { MenuDividerProps } from "./menu-types";
8
+
9
+ /**
10
+ * A horizontal divider for use between MenuItems or MenuGroups.
11
+ *
12
+ * Uses traditional line-based separation (as opposed to the gap-based
13
+ * separation in MenuGroup, which is the Expressive default).
14
+ *
15
+ * Spec: horizontal=12dp padding, vertical=2dp padding, `outline-variant` color.
16
+ *
17
+ * @example
18
+ * <MenuContent>
19
+ * <MenuItem>Cut</MenuItem>
20
+ * <MenuDivider />
21
+ * <MenuItem>Paste</MenuItem>
22
+ * </MenuContent>
23
+ */
24
+ export const MenuDivider = React.forwardRef<HTMLHRElement, MenuDividerProps>(
25
+ ({ className, ...props }, ref) => {
26
+ const { menuVariant } = useMenuContext();
27
+
28
+ return (
29
+ <DropdownMenu.Separator asChild>
30
+ <hr
31
+ ref={ref}
32
+ className={cn(
33
+ // Baseline: 8dp vertical margin, 0 horizontal. Expressive: 12dp horizontal, 2dp vertical
34
+ menuVariant === "baseline" ? "my-2 mx-0" : "mx-3 my-0.5",
35
+ // 1px height line
36
+ "h-px border-0",
37
+ // outline-variant color
38
+ "bg-m3-outline-variant",
39
+ className,
40
+ )}
41
+ {...props}
42
+ />
43
+ </DropdownMenu.Separator>
44
+ );
45
+ },
46
+ );
47
+ MenuDivider.displayName = "MenuDivider";
@@ -0,0 +1,200 @@
1
+ // ─── MD3 Expressive Menu — MenuGroup ────────────────────────────────────────
2
+ // Gap-based grouping with shape morphing on hover (core Expressive feature)
3
+ import { m } from "motion/react";
4
+ import * as React from "react";
5
+ import { cn } from "../../lib/utils";
6
+ import { FAST_SPATIAL_SPRING } from "./menu-animations";
7
+
8
+ import { useMenuContext } from "./menu-context";
9
+ import {
10
+ BASELINE_COLORS,
11
+ GROUP_SHAPES,
12
+ MENU_GROUP_PADDING_Y,
13
+ STANDARD_COLORS,
14
+ VIBRANT_COLORS,
15
+ } from "./menu-tokens";
16
+ import type {
17
+ MenuGroupPosition,
18
+ MenuGroupProps,
19
+ MenuItemPosition,
20
+ MenuItemProps,
21
+ } from "./menu-types";
22
+
23
+ // Extend MenuGroupProps with data-* attributes for testing and aria- attributes
24
+ type MenuGroupDivProps = MenuGroupProps & {
25
+ [key: `data-${string}`]: string | undefined;
26
+ id?: string;
27
+ "aria-label"?: string;
28
+ "aria-labelledby"?: string;
29
+ };
30
+
31
+ // ─── Position helper ──────────────────────────────────────────────────────────
32
+
33
+ function getGroupPosition(index: number, count: number): MenuGroupPosition {
34
+ if (count === 1) return "standalone";
35
+ if (index === 0) return "leading";
36
+ if (index === count - 1) return "trailing";
37
+ return "middle";
38
+ }
39
+
40
+ function getGroupActiveShape(position: MenuGroupPosition): string {
41
+ return GROUP_SHAPES[
42
+ `${position}Active` as keyof typeof GROUP_SHAPES
43
+ ] as string;
44
+ }
45
+
46
+ // ─── MenuGroup ────────────────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * A container that groups MenuItem elements with gap-based visual separation —
50
+ * the defining feature of MD3 Expressive menus.
51
+ *
52
+ * Shape morphing: on hover, the container's border-radius transitions from the
53
+ * "inactive" small shape to the "active" large shape via a FastSpatial spring.
54
+ * The shape depends on the group's position (leading/middle/trailing/standalone).
55
+ *
56
+ * MenuItem children automatically receive `itemPosition` props based on their
57
+ * index within the group.
58
+ *
59
+ * @example
60
+ * <MenuContent>
61
+ * <MenuGroup>
62
+ * <MenuItem>Cut</MenuItem>
63
+ * <MenuItem>Copy</MenuItem>
64
+ * <MenuItem>Paste</MenuItem>
65
+ * </MenuGroup>
66
+ * <MenuGroup>
67
+ * <MenuItem>Select All</MenuItem>
68
+ * </MenuGroup>
69
+ * </MenuContent>
70
+ */
71
+ export const MenuGroup = React.forwardRef<HTMLDivElement, MenuGroupDivProps>(
72
+ (
73
+ {
74
+ children,
75
+ label,
76
+ index = 0,
77
+ count = 1,
78
+ colorVariant: propColorVariant,
79
+ isGapVariant,
80
+ itemPosition,
81
+ className,
82
+ ...rest
83
+ },
84
+ ref,
85
+ ) => {
86
+ const {
87
+ menuVariant,
88
+ colorVariant: contextColorVariant,
89
+ isStatic,
90
+ } = useMenuContext();
91
+ const colorVariant = propColorVariant ?? contextColorVariant;
92
+ const colors =
93
+ menuVariant === "baseline"
94
+ ? BASELINE_COLORS
95
+ : colorVariant === "vibrant"
96
+ ? VIBRANT_COLORS
97
+ : STANDARD_COLORS;
98
+
99
+ const position = getGroupPosition(index, count);
100
+ const activeShape = getGroupActiveShape(position);
101
+
102
+ const [isHovered, setIsHovered] = React.useState(false);
103
+ const currentShape =
104
+ isStatic || isHovered ? activeShape : GROUP_SHAPES.inactive;
105
+
106
+ const handlePointerEnter = React.useCallback(() => setIsHovered(true), []);
107
+ const handlePointerLeave = React.useCallback(() => setIsHovered(false), []);
108
+
109
+ // Helper to recursively flatten fragments and collect valid elements.
110
+ // This is necessary because cloneElement cannot be used on React.Fragment.
111
+ const flattenChildren = (
112
+ children: React.ReactNode,
113
+ ): React.ReactElement[] => {
114
+ return React.Children.toArray(children).reduce(
115
+ (acc: React.ReactElement[], child) => {
116
+ if (React.isValidElement(child)) {
117
+ if (child.type === React.Fragment) {
118
+ return acc.concat(
119
+ flattenChildren(
120
+ (child as React.ReactElement<{ children?: React.ReactNode }>)
121
+ .props.children,
122
+ ),
123
+ );
124
+ }
125
+ acc.push(child as React.ReactElement);
126
+ }
127
+ return acc;
128
+ },
129
+ [],
130
+ );
131
+ };
132
+
133
+ const validChildren = flattenChildren(children);
134
+ const itemCount = validChildren.length;
135
+
136
+ const enhancedChildren = validChildren.map((child, i) => {
137
+ const itemPosition: MenuItemPosition =
138
+ itemCount === 1
139
+ ? "standalone"
140
+ : i === 0
141
+ ? "leading"
142
+ : i === itemCount - 1
143
+ ? "trailing"
144
+ : "middle";
145
+
146
+ return React.cloneElement(child as React.ReactElement<MenuItemProps>, {
147
+ itemPosition,
148
+ colorVariant,
149
+ });
150
+ });
151
+
152
+ return (
153
+ <m.div
154
+ ref={ref}
155
+ role="group"
156
+ aria-label={label}
157
+ className={cn(
158
+ "relative",
159
+ // In baseline variant, MenuGroup is transparent so it shouldn't clip.
160
+ // In expressive variant, it needs overflow-hidden to clip hover states to its morphing shape.
161
+ menuVariant === "baseline" ? "" : "overflow-hidden",
162
+ // Vertical padding: 2dp for gap variant (to match Figma), 4dp for baseline
163
+ isGapVariant ? "py-0.5" : MENU_GROUP_PADDING_Y,
164
+ // Horizontal padding: 4dp for expressive menus (both static and popup), 0 for baseline
165
+ menuVariant === "expressive" ? "px-1" : "",
166
+ // Gap variant has floating segments, so each group manages its own shadow
167
+ isGapVariant ? "elevation-2" : "",
168
+ // Background based on color variant (transparent for baseline to avoid double-layering)
169
+ menuVariant === "baseline" ? "bg-transparent" : colors.containerBg,
170
+ className,
171
+ )}
172
+ animate={{ borderRadius: currentShape }}
173
+ transition={FAST_SPATIAL_SPRING}
174
+ onPointerEnter={handlePointerEnter}
175
+ onPointerLeave={handlePointerLeave}
176
+ {...rest}
177
+ >
178
+ {/* Optional group label: labelSmall typography, 12dp horizontal padding */}
179
+ {label && (
180
+ <span
181
+ className={cn(
182
+ // Padding: 12dp top, 12dp horizontal, 8dp bottom (MD3 spec)
183
+ "block pt-3 px-3 pb-2",
184
+ "text-label-small",
185
+ menuVariant === "baseline"
186
+ ? "text-m3-on-surface-variant"
187
+ : colorVariant === "vibrant"
188
+ ? "text-m3-on-tertiary-container"
189
+ : "text-m3-on-surface-variant",
190
+ )}
191
+ >
192
+ {label}
193
+ </span>
194
+ )}
195
+ {enhancedChildren}
196
+ </m.div>
197
+ );
198
+ },
199
+ );
200
+ MenuGroup.displayName = "MenuGroup";