@bug-on/md3-react 2.0.3 → 3.0.1

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 (316) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/CHANGELOG.md +69 -0
  3. package/dist/index.css +178 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6135 -0
  6. package/dist/index.d.ts +6135 -71
  7. package/dist/index.js +1688 -631
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1600 -564
  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/plugin.d.mts +1 -0
  14. package/dist/plugin.d.ts +1 -0
  15. package/dist/plugin.js +13 -0
  16. package/dist/plugin.js.map +1 -0
  17. package/dist/plugin.mjs +3 -0
  18. package/dist/plugin.mjs.map +1 -0
  19. package/dist/typography.css.d.ts +2 -0
  20. package/package.json +28 -19
  21. package/scripts/copy-assets.js +115 -0
  22. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  23. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  24. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  25. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  26. package/src/assets/loading-indicator.svg +19 -0
  27. package/src/assets/material-symbols-cdn.css +65 -0
  28. package/src/assets/material-symbols-self-hosted.css +90 -0
  29. package/src/css.d.ts +20 -0
  30. package/src/hooks/useClickOutside.ts +37 -0
  31. package/src/hooks/useMediaQuery.ts +28 -0
  32. package/src/hooks/useRipple.ts +88 -0
  33. package/src/index.css +23 -0
  34. package/src/index.ts +349 -0
  35. package/src/lib/material-symbols-preconnect.tsx +82 -0
  36. package/src/lib/theme-utils.ts +195 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/plugin.ts +12 -0
  39. package/src/test/button.test.tsx +59 -0
  40. package/src/test/icon.test.tsx +91 -0
  41. package/src/test/loading-indicator.test.tsx +128 -0
  42. package/src/test/progress-indicator.test.tsx +306 -0
  43. package/src/test/setup.ts +80 -0
  44. package/src/test/typography.test.tsx +206 -0
  45. package/src/types/index.ts +7 -0
  46. package/src/types/md3.ts +31 -0
  47. package/src/ui/Text.tsx +60 -0
  48. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  49. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  50. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  51. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  52. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  53. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  54. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  55. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  56. package/src/ui/app-bar/app-bar.types.ts +441 -0
  57. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  58. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  59. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  60. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  61. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  62. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  63. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  64. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  65. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  66. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  67. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  68. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  69. package/src/ui/app-bar/search-view.tsx +227 -0
  70. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  71. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  72. package/src/ui/badge.test.tsx +345 -0
  73. package/src/ui/badge.tsx +282 -0
  74. package/src/ui/button-group.test.tsx +71 -0
  75. package/src/ui/button-group.tsx +350 -0
  76. package/src/ui/button.test.tsx +306 -0
  77. package/src/ui/button.tsx +665 -0
  78. package/src/ui/card.test.tsx +187 -0
  79. package/src/ui/card.tsx +259 -0
  80. package/src/ui/checkbox.test.tsx +423 -0
  81. package/src/ui/checkbox.tsx +525 -0
  82. package/src/ui/chip.test.tsx +292 -0
  83. package/src/ui/chip.tsx +548 -0
  84. package/src/ui/code-block.tsx +219 -0
  85. package/src/ui/dialog.test.tsx +300 -0
  86. package/src/ui/dialog.tsx +384 -0
  87. package/src/ui/divider.test.tsx +314 -0
  88. package/src/ui/divider.tsx +412 -0
  89. package/src/ui/drawer.tsx +240 -0
  90. package/src/ui/fab-menu.test.tsx +494 -0
  91. package/src/ui/fab-menu.tsx +739 -0
  92. package/src/ui/fab.test.tsx +232 -0
  93. package/src/ui/fab.tsx +505 -0
  94. package/src/ui/icon-button.test.tsx +515 -0
  95. package/src/ui/icon-button.tsx +525 -0
  96. package/src/ui/icon.test.tsx +197 -0
  97. package/src/ui/icon.tsx +179 -0
  98. package/src/ui/loading-indicator.test.tsx +73 -0
  99. package/src/ui/loading-indicator.tsx +312 -0
  100. package/src/ui/menu/context-menu.tsx +275 -0
  101. package/src/ui/menu/index.ts +77 -0
  102. package/src/ui/menu/menu-animations.ts +102 -0
  103. package/src/ui/menu/menu-context.tsx +99 -0
  104. package/src/ui/menu/menu-divider.tsx +47 -0
  105. package/src/ui/menu/menu-group.tsx +200 -0
  106. package/src/ui/menu/menu-item.tsx +294 -0
  107. package/src/ui/menu/menu-tokens.ts +208 -0
  108. package/src/ui/menu/menu-types.ts +313 -0
  109. package/src/ui/menu/menu.test.tsx +624 -0
  110. package/src/ui/menu/menu.tsx +289 -0
  111. package/src/ui/menu/sub-menu.tsx +223 -0
  112. package/src/ui/menu/vertical-menu.tsx +382 -0
  113. package/src/ui/navigation-rail.test.tsx +404 -0
  114. package/src/ui/navigation-rail.tsx +607 -0
  115. package/src/ui/progress-indicator/circular.tsx +248 -0
  116. package/src/ui/progress-indicator/hooks.ts +51 -0
  117. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  118. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  119. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  120. package/src/ui/progress-indicator/linear.tsx +143 -0
  121. package/src/ui/progress-indicator/types.ts +158 -0
  122. package/src/ui/progress-indicator/utils.ts +73 -0
  123. package/src/ui/radio-button.test.tsx +407 -0
  124. package/src/ui/radio-button.tsx +551 -0
  125. package/src/ui/ripple.test.tsx +72 -0
  126. package/src/ui/ripple.tsx +234 -0
  127. package/src/ui/scroll-area.test.tsx +58 -0
  128. package/src/ui/scroll-area.tsx +139 -0
  129. package/src/ui/search/animated-placeholder.tsx +145 -0
  130. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  131. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  132. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  133. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  134. package/src/ui/search/index.ts +44 -0
  135. package/src/ui/search/search-bar.tsx +220 -0
  136. package/src/ui/search/search-context.tsx +42 -0
  137. package/src/ui/search/search-view-docked.tsx +194 -0
  138. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  139. package/src/ui/search/search.test.tsx +233 -0
  140. package/src/ui/search/search.tokens.ts +134 -0
  141. package/src/ui/search/search.tsx +131 -0
  142. package/src/ui/search/search.types.ts +154 -0
  143. package/src/ui/search/trailing-action.tsx +49 -0
  144. package/src/ui/shared/constants.ts +135 -0
  145. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  146. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  147. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  148. package/src/ui/slider/range-slider.tsx +561 -0
  149. package/src/ui/slider/slider-thumb.tsx +379 -0
  150. package/src/ui/slider/slider-track.tsx +912 -0
  151. package/src/ui/slider/slider.tokens.ts +189 -0
  152. package/src/ui/slider/slider.tsx +259 -0
  153. package/src/ui/slider/slider.types.ts +288 -0
  154. package/src/ui/snackbar/index.ts +20 -0
  155. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  156. package/src/ui/snackbar/snackbar.tsx +476 -0
  157. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  158. package/src/ui/switch/switch.stories.tsx +309 -0
  159. package/src/ui/switch/switch.test.tsx +243 -0
  160. package/src/ui/switch/switch.tokens.ts +89 -0
  161. package/src/ui/switch/switch.tsx +504 -0
  162. package/src/ui/switch/switch.types.ts +62 -0
  163. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  164. package/src/ui/tabs/tab.tsx +407 -0
  165. package/src/ui/tabs/tabs-content.tsx +89 -0
  166. package/src/ui/tabs/tabs-list.tsx +146 -0
  167. package/src/ui/tabs/tabs.test.tsx +290 -0
  168. package/src/ui/tabs/tabs.tokens.ts +121 -0
  169. package/src/ui/tabs/tabs.tsx +229 -0
  170. package/src/ui/tabs/tabs.types.ts +185 -0
  171. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  172. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  173. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  174. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  175. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  176. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  177. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  178. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  179. package/src/ui/text-field/text-field.test.tsx +454 -0
  180. package/src/ui/text-field/text-field.tokens.ts +104 -0
  181. package/src/ui/text-field/text-field.tsx +548 -0
  182. package/src/ui/text-field/text-field.types.ts +180 -0
  183. package/src/ui/theme-provider/index.tsx +215 -0
  184. package/src/ui/toc.test.tsx +108 -0
  185. package/src/ui/toc.tsx +172 -0
  186. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  187. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  188. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  189. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  190. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  191. package/src/ui/tooltip/tooltip.types.ts +70 -0
  192. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  193. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  194. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  195. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  196. package/src/ui/typography/type-scale-tokens.ts +205 -0
  197. package/src/ui/typography/typography-key-tokens.ts +43 -0
  198. package/src/ui/typography/typography-tokens.ts +360 -0
  199. package/src/ui/typography/typography.css +22 -0
  200. package/src/ui/typography/typography.tsx +559 -0
  201. package/test-render.tsx +4 -0
  202. package/test-shadow.html +26 -0
  203. package/test_output.txt +164 -0
  204. package/test_output_v2.txt +5 -0
  205. package/tsconfig.build.json +10 -0
  206. package/tsconfig.json +18 -0
  207. package/tsup.config.ts +20 -0
  208. package/vitest.config.ts +11 -0
  209. package/dist/hooks/useClickOutside.d.ts +0 -8
  210. package/dist/hooks/useMediaQuery.d.ts +0 -11
  211. package/dist/hooks/useRipple.d.ts +0 -26
  212. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  213. package/dist/lib/theme-utils.d.ts +0 -63
  214. package/dist/lib/utils.d.ts +0 -2
  215. package/dist/types/index.d.ts +0 -1
  216. package/dist/types/md3.d.ts +0 -14
  217. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  218. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  219. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  220. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  221. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  222. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  223. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  224. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  225. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  226. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  227. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  228. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  229. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  230. package/dist/ui/app-bar/search-view.d.ts +0 -54
  231. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  232. package/dist/ui/badge.d.ts +0 -125
  233. package/dist/ui/button-group.d.ts +0 -59
  234. package/dist/ui/button.d.ts +0 -148
  235. package/dist/ui/card.d.ts +0 -62
  236. package/dist/ui/checkbox.d.ts +0 -82
  237. package/dist/ui/chip.d.ts +0 -110
  238. package/dist/ui/code-block.d.ts +0 -14
  239. package/dist/ui/dialog.d.ts +0 -111
  240. package/dist/ui/divider.d.ts +0 -164
  241. package/dist/ui/drawer.d.ts +0 -39
  242. package/dist/ui/dropdown.d.ts +0 -29
  243. package/dist/ui/fab-menu.d.ts +0 -204
  244. package/dist/ui/fab.d.ts +0 -162
  245. package/dist/ui/icon-button.d.ts +0 -131
  246. package/dist/ui/icon.d.ts +0 -88
  247. package/dist/ui/loading-indicator.d.ts +0 -42
  248. package/dist/ui/navigation-rail.d.ts +0 -29
  249. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  250. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  251. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  252. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  253. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  254. package/dist/ui/progress-indicator/types.d.ts +0 -151
  255. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  256. package/dist/ui/radio-button.d.ts +0 -106
  257. package/dist/ui/ripple.d.ts +0 -126
  258. package/dist/ui/scroll-area.d.ts +0 -27
  259. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  260. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  261. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  262. package/dist/ui/search/index.d.ts +0 -27
  263. package/dist/ui/search/search-bar.d.ts +0 -32
  264. package/dist/ui/search/search-context.d.ts +0 -24
  265. package/dist/ui/search/search-view-docked.d.ts +0 -25
  266. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  267. package/dist/ui/search/search.d.ts +0 -50
  268. package/dist/ui/search/search.tokens.d.ts +0 -112
  269. package/dist/ui/search/search.types.d.ts +0 -131
  270. package/dist/ui/search/trailing-action.d.ts +0 -9
  271. package/dist/ui/shared/constants.d.ts +0 -86
  272. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  273. package/dist/ui/slider/range-slider.d.ts +0 -47
  274. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  275. package/dist/ui/slider/slider-track.d.ts +0 -25
  276. package/dist/ui/slider/slider.d.ts +0 -60
  277. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  278. package/dist/ui/slider/slider.types.d.ts +0 -259
  279. package/dist/ui/snackbar/index.d.ts +0 -6
  280. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  281. package/dist/ui/switch/switch.d.ts +0 -30
  282. package/dist/ui/switch/switch.stories.d.ts +0 -48
  283. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  284. package/dist/ui/switch/switch.types.d.ts +0 -59
  285. package/dist/ui/tabs/tab.d.ts +0 -43
  286. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  287. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  288. package/dist/ui/tabs/tabs.d.ts +0 -60
  289. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  290. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  291. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  292. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  293. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  294. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  295. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  296. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  297. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  298. package/dist/ui/text-field/text-field.d.ts +0 -49
  299. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  300. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  301. package/dist/ui/theme-provider/index.d.ts +0 -48
  302. package/dist/ui/toc.d.ts +0 -80
  303. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  304. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  305. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  306. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  307. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  308. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  309. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  310. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  311. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  312. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  313. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  314. package/dist/ui/typography/typography.d.ts +0 -265
  315. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  316. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,294 @@
1
+ // ─── MD3 Expressive Menu — MenuItem ─────────────────────────────────────────
2
+ // Shape morphing + Standard/Vibrant color variants + selection state
3
+ // Animation: only selectable items animate the leading icon slot
4
+ import * as ContextMenu from "@radix-ui/react-context-menu";
5
+ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
6
+ import { Slot } from "@radix-ui/react-slot";
7
+ import { AnimatePresence, m } from "motion/react";
8
+ import * as React from "react";
9
+ import { cn } from "../../lib/utils";
10
+ import { Icon } from "../icon";
11
+ import { CHECK_ICON_VARIANTS, MENU_CHECK_ICON_SIZE } from "./menu-animations";
12
+ import { useMenuContext } from "./menu-context";
13
+ import {
14
+ BASELINE_COLORS,
15
+ BASELINE_ITEM_SHAPE,
16
+ ITEM_SHAPE_CLASSES,
17
+ MENU_BASELINE_ITEM_HORIZONTAL_PADDING,
18
+ STANDARD_COLORS,
19
+ VIBRANT_COLORS,
20
+ } from "./menu-tokens";
21
+ import type {
22
+ MenuItemPosition,
23
+ MenuItemProps,
24
+ MenuVariant,
25
+ } from "./menu-types";
26
+
27
+ // ─── Shape helper ─────────────────────────────────────────────────────────────
28
+
29
+ function getItemShapeClass(
30
+ position: MenuItemPosition,
31
+ selected: boolean,
32
+ isStatic: boolean = false,
33
+ menuVariant: MenuVariant = "expressive",
34
+ ): string {
35
+ if (menuVariant === "baseline") return BASELINE_ITEM_SHAPE;
36
+ if (selected) return ITEM_SHAPE_CLASSES.selected;
37
+ // Vertical Menu standalone items have 12px border radius
38
+ if (isStatic && position === "standalone") return "rounded-[12px]";
39
+ return ITEM_SHAPE_CLASSES[position];
40
+ }
41
+
42
+ // ─── MenuItem ─────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * An interactive item within a Menu or MenuGroup.
46
+ *
47
+ * ### Shape morphing
48
+ * `itemPosition` (auto-set by MenuGroup) controls border-radius. When `selected=true`,
49
+ * shape overrides to `CornerMedium (12px)` with a CSS transition.
50
+ *
51
+ * ### Leading icon animation
52
+ * - **Selectable items** (`selected` prop defined): `AnimatePresence` swaps between
53
+ * a check icon and the user `leadingIcon` using FastSpatial expand + FastEffects fade.
54
+ * - **Static items** (`selected` prop undefined): `leadingIcon` renders at fixed width
55
+ * with no enter/exit animation (matches Android's static rendering).
56
+ *
57
+ * ### Selected state
58
+ * Container color transitions with `transition-colors duration-150` (maps to
59
+ * Android's `animateColorAsState` with `FastEffects` spec).
60
+ */
61
+ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
62
+ (
63
+ {
64
+ children,
65
+ onClick,
66
+ leadingIcon,
67
+ trailingIcon,
68
+ supportingText,
69
+ trailingText,
70
+ selected,
71
+ disabled = false,
72
+ itemPosition = "standalone",
73
+ colorVariant: propColorVariant,
74
+ keepOpen = false,
75
+ className,
76
+ isSubTrigger,
77
+ value,
78
+ role,
79
+ ...rest
80
+ },
81
+ ref,
82
+ ) => {
83
+ const {
84
+ menuVariant,
85
+ colorVariant: contextColorVariant,
86
+ menuPrimitive,
87
+ } = useMenuContext();
88
+ const colorVariant = propColorVariant ?? contextColorVariant;
89
+ const colors =
90
+ menuVariant === "baseline"
91
+ ? BASELINE_COLORS
92
+ : colorVariant === "vibrant"
93
+ ? VIBRANT_COLORS
94
+ : STANDARD_COLORS;
95
+
96
+ const isStaticMenu = menuPrimitive === "static";
97
+ const shapeClass = getItemShapeClass(
98
+ itemPosition,
99
+ !!selected,
100
+ isStaticMenu,
101
+ menuVariant,
102
+ );
103
+
104
+ // A selectable item is one where `selected` prop is explicitly passed
105
+ // (even if false). Non-selectable items do not animate the leading slot.
106
+ const isSelectable = selected !== undefined && !isSubTrigger;
107
+
108
+ // Determine which Radix primitive to use based on selection state and role
109
+ const isCheckbox =
110
+ role === "menuitemcheckbox" ||
111
+ (selected !== undefined && !role && !isSubTrigger);
112
+ const isRadio = role === "menuitemradio";
113
+
114
+ // Select the correct Radix primitive based on which menu family is active.
115
+ // static → Slot (plain HTML, manages own ARIA)
116
+ // context → @radix-ui/react-context-menu primitives
117
+ // dropdown (default) → @radix-ui/react-dropdown-menu primitives
118
+ const ItemPrimitive =
119
+ isStaticMenu || isSubTrigger
120
+ ? Slot
121
+ : menuPrimitive === "context"
122
+ ? ((isCheckbox
123
+ ? ContextMenu.CheckboxItem
124
+ : isRadio
125
+ ? ContextMenu.RadioItem
126
+ : ContextMenu.Item) as React.ElementType)
127
+ : ((isCheckbox
128
+ ? DropdownMenu.CheckboxItem
129
+ : isRadio
130
+ ? DropdownMenu.RadioItem
131
+ : DropdownMenu.Item) as React.ElementType);
132
+
133
+ return (
134
+ <ItemPrimitive
135
+ ref={ref}
136
+ {...(isStaticMenu || isSubTrigger
137
+ ? {
138
+ role:
139
+ role ||
140
+ (isCheckbox
141
+ ? "menuitemcheckbox"
142
+ : isRadio
143
+ ? "menuitemradio"
144
+ : "menuitem"),
145
+ "aria-checked": isCheckbox || isRadio ? !!selected : undefined,
146
+ "aria-disabled": disabled ? true : undefined,
147
+ tabIndex: disabled ? -1 : 0,
148
+ onKeyDown: (e: React.KeyboardEvent) => {
149
+ if (e.key === "Enter" || e.key === " ") {
150
+ e.preventDefault();
151
+ onClick?.(e as unknown as React.MouseEvent<HTMLDivElement>);
152
+ }
153
+ },
154
+ onClick,
155
+ }
156
+ : {
157
+ disabled,
158
+ onSelect: keepOpen ? (e: Event) => e.preventDefault() : undefined,
159
+ onClick,
160
+ ...(isCheckbox || isRadio ? { checked: !!selected } : {}),
161
+ ...(isRadio ? { value: value ?? "" } : {}),
162
+ asChild: true,
163
+ })}
164
+ >
165
+ <div
166
+ // Role provided by Radix primitives via asChild, or manually set when static
167
+ className={cn(
168
+ // Layout
169
+ "relative flex w-full cursor-pointer select-none items-center outline-none",
170
+ // Sizing: min-h 48dp, min-w 112dp, max-w 280dp
171
+ "min-h-12 min-w-28 max-w-70",
172
+ // Horizontal padding
173
+ menuVariant === "baseline"
174
+ ? MENU_BASELINE_ITEM_HORIZONTAL_PADDING
175
+ : "px-4",
176
+ // Spacing between items
177
+ isStaticMenu ? "my-0.5" : "",
178
+ // Shape morphing (position-based + selected override)
179
+ shapeClass,
180
+ // Animate border-radius AND background-color together (FastEffects: 150ms)
181
+ "transition-[border-radius,background-color] duration-150 ease-in",
182
+ // Colors based on variant + selection
183
+ selected
184
+ ? cn(colors.selectedBg, colors.selectedText)
185
+ : cn(colors.labelText),
186
+ // State layers (only on unselected items)
187
+ !selected && colors.hoverLayer,
188
+ !selected && colors.focusLayer,
189
+ // Focus visible ring (WCAG 2.4.11 — visible focus indicator)
190
+ // Uses ring-inset so the ring doesn't overflow the item bounds.
191
+ "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-m3-primary",
192
+ // Disabled
193
+ disabled && "pointer-events-none opacity-[0.38]",
194
+ className,
195
+ )}
196
+ {...rest}
197
+ >
198
+ {/* ── Leading slot ── */}
199
+ {(isSelectable || leadingIcon) && (
200
+ <div
201
+ className="flex h-5 w-5 shrink-0 items-center justify-center mr-3"
202
+ aria-hidden="true"
203
+ >
204
+ {isSelectable ? (
205
+ <AnimatePresence initial={false} mode="wait">
206
+ {selected ? (
207
+ <m.span
208
+ key="check"
209
+ className={cn(
210
+ "flex h-full w-full items-center justify-center overflow-hidden",
211
+ colors.selectedIcon,
212
+ )}
213
+ variants={CHECK_ICON_VARIANTS}
214
+ initial="hidden"
215
+ animate="visible"
216
+ exit="exit"
217
+ >
218
+ <Icon name="check" fill={1} size={MENU_CHECK_ICON_SIZE} />
219
+ </m.span>
220
+ ) : leadingIcon ? (
221
+ <m.span
222
+ key="icon"
223
+ className={cn(
224
+ "flex h-full w-full items-center justify-center overflow-hidden",
225
+ colors.iconColor,
226
+ )}
227
+ variants={CHECK_ICON_VARIANTS}
228
+ initial="hidden"
229
+ animate="visible"
230
+ exit="exit"
231
+ >
232
+ {leadingIcon}
233
+ </m.span>
234
+ ) : (
235
+ // Spacer for selectable items with no icon, to keep text aligned
236
+ <div className="w-5" />
237
+ )}
238
+ </AnimatePresence>
239
+ ) : (
240
+ // Static icon for non-selectable items
241
+ <span
242
+ className={cn(
243
+ "flex h-full w-full items-center justify-center",
244
+ colors.iconColor,
245
+ )}
246
+ >
247
+ {leadingIcon}
248
+ </span>
249
+ )}
250
+ </div>
251
+ )}
252
+
253
+ {/* ── Label + Supporting Text ── */}
254
+ <span className="flex flex-1 flex-col">
255
+ <span className="text-body-large leading-snug">{children}</span>
256
+ {supportingText && (
257
+ <span
258
+ className={cn(
259
+ "text-body-medium leading-snug",
260
+ // Source: StandardMenuTokens.ItemSupportingTextColor / VibrantMenuTokens
261
+ selected ? colors.selectedText : colors.supportingTextColor,
262
+ )}
263
+ >
264
+ {supportingText}
265
+ </span>
266
+ )}
267
+ </span>
268
+
269
+ {/* ── Trailing: shortcut text OR trailing icon ── */}
270
+ {(trailingText || trailingIcon) && (
271
+ <span
272
+ className={cn(
273
+ // Minimum 12dp gap from label column (ListTokens)
274
+ "ml-3 flex shrink-0 items-center",
275
+ // Source: StandardMenuTokens.ItemTrailingIconColor / VibrantMenuTokens
276
+ selected ? colors.selectedText : colors.trailingIconColor,
277
+ )}
278
+ aria-hidden={trailingIcon ? "true" : undefined}
279
+ >
280
+ {trailingText ? (
281
+ <span className="text-label-small tracking-wider">
282
+ {trailingText}
283
+ </span>
284
+ ) : (
285
+ trailingIcon
286
+ )}
287
+ </span>
288
+ )}
289
+ </div>
290
+ </ItemPrimitive>
291
+ );
292
+ },
293
+ );
294
+ MenuItem.displayName = "MenuItem";
@@ -0,0 +1,208 @@
1
+ // ─── MD3 Expressive Menu — Design Token Constants ─────────────────────────────
2
+ // Sourced from: SegmentedMenuTokens.kt, StandardMenuTokens.kt, VibrantMenuTokens.kt,
3
+ // MenuTokens.kt, MenuDefaults.kt, ListTokens.kt
4
+
5
+ // ─── Spacing (px → dp) ────────────────────────────────────────────────────────
6
+
7
+ /** Horizontal padding for menu items: 16dp (ItemLeadingSpace / ItemTrailingSpace) */
8
+ export const MENU_ITEM_HORIZONTAL_PADDING = "px-4"; // 16dp
9
+
10
+ /** Horizontal padding for baseline menu items: 12dp */
11
+ export const MENU_BASELINE_ITEM_HORIZONTAL_PADDING = "px-3"; // 12dp
12
+
13
+ /** Min height for selectable items: 44dp (SegmentedMenuTokens.Item) */
14
+ export const MENU_ITEM_MIN_HEIGHT = "min-h-11"; // 44dp = 11 * 4px
15
+
16
+ /** Min height for standard list items: 48dp (MenuListItemContainerHeight) */
17
+ export const MENU_LIST_ITEM_MIN_HEIGHT = "min-h-12"; // 48dp
18
+
19
+ /** Min width of menu container: 112dp (DropdownMenuItemDefaultMinWidth) */
20
+ export const MENU_MIN_WIDTH = "min-w-28"; // 112dp
21
+
22
+ /** Max width of menu container: 280dp (DropdownMenuItemDefaultMaxWidth) */
23
+ export const MENU_MAX_WIDTH = "max-w-70"; // 280dp
24
+
25
+ /** Gap between MenuGroup segments: 2dp (SegmentedMenuTokens.SegmentedGap) */
26
+ export const MENU_GROUP_GAP = "gap-0.5"; // 2dp
27
+
28
+ /** Internal group vertical padding: 4dp (DropdownMenuGroupVerticalPadding) */
29
+ export const MENU_GROUP_PADDING_Y = "py-1"; // 4dp
30
+
31
+ /** Popup container vertical padding: 8dp (DropdownMenuVerticalPadding) */
32
+ export const MENU_POPUP_PADDING_Y = "py-2"; // 8dp
33
+
34
+ /** Leading icon size: 20dp (SegmentedMenuTokens.ItemLeadingIconSize) */
35
+ export const MENU_ICON_SIZE = 20;
36
+
37
+ // ─── Container Shape ──────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Menu popup container shape: CornerExtraSmall = 4px.
41
+ * Source: MenuTokens.ContainerShape = ShapeKeyTokens.CornerExtraSmall
42
+ */
43
+ export const MENU_CONTAINER_SHAPE = "rounded-[4px]"; // CornerExtraSmall
44
+
45
+ /**
46
+ * Shape for Baseline menu item (no shape morphing, edge-to-edge state layer).
47
+ */
48
+ export const BASELINE_ITEM_SHAPE = "rounded-none";
49
+
50
+ // ─── Shape: CSS border-radius values (used with motion/react `animate`) ────────
51
+ // These are used for animated shape morphing via Framer Motion's `borderRadius`
52
+ // property — NOT as Tailwind classes (Tailwind cannot animate between values).
53
+
54
+ /**
55
+ * Shape values for MenuGroup container (borderRadius CSS shorthand string).
56
+ * Format: "topLeft topRight bottomRight bottomLeft"
57
+ *
58
+ * Source: MenuDefaults.kt groupShape() + SegmentedMenuTokens/ShapeTokens
59
+ * - ContainerShape (standalone) = CornerLarge (16px) all corners
60
+ * - InactiveContainerShape = CornerSmall (8px) all corners
61
+ * - Leading group: topStart=CornerLarge, topEnd=CornerLarge, bottomStart=CornerSmall, bottomEnd=CornerSmall
62
+ * - Middle group: GroupShape = CornerSmall (8px)
63
+ * - Trailing group: topStart=CornerSmall, topEnd=CornerSmall, bottomStart=CornerLarge, bottomEnd=CornerLarge
64
+ */
65
+ export const GROUP_SHAPES = {
66
+ /** Active standalone group shape: CornerLarge all corners (16px) */
67
+ standaloneActive: "16px",
68
+ /**
69
+ * Active leading group shape: top=CornerLarge(16px), bottom=CornerSmall(8px)
70
+ * Source: SegmentedMenuTokens — LeadingContainerShape:
71
+ * topStart=CornerLarge, topEnd=CornerLarge, bottomStart=CornerSmall, bottomEnd=CornerSmall
72
+ */
73
+ leadingActive: "16px 16px 8px 8px",
74
+ /** Active middle group shape: CornerExtraSmall all corners (4px) */
75
+ middleActive: "4px",
76
+ /**
77
+ * Active trailing group shape: top=CornerSmall(8px), bottom=CornerLarge(16px)
78
+ * Source: SegmentedMenuTokens — TrailingContainerShape:
79
+ * topStart=CornerSmall, topEnd=CornerSmall, bottomStart=CornerLarge, bottomEnd=CornerLarge
80
+ */
81
+ trailingActive: "8px 8px 16px 16px",
82
+ /** Inactive (default, pre-hover) shape for all groups: CornerExtraSmall (4px) */
83
+ inactive: "4px",
84
+ } as const;
85
+
86
+ /**
87
+ * Shape values for MenuItem (borderRadius CSS shorthand string).
88
+ * Used as Tailwind classes via arbitrary values for static rendering.
89
+ * Animated shape (selected ↔ unselected) is handled via `transition-[border-radius]`.
90
+ *
91
+ * Source: MenuDefaults.kt itemShape() + SegmentedMenuTokens/ShapeTokens
92
+ * - leading item: topStart/topEnd=CornerMedium(12px), bottomStart/bottomEnd=CornerExtraSmall(4px)
93
+ * - middle item: CornerExtraSmall all (ItemShape = 4px)
94
+ * - trailing item: topStart/topEnd=CornerExtraSmall(4px), bottomStart/bottomEnd=CornerMedium(12px)
95
+ * - standalone item: same as middle (ItemShape = 4px)
96
+ * - selected (all positions): CornerMedium all (ItemSelectedShape = 12px)
97
+ */
98
+ export const ITEM_SHAPE_CLASSES = {
99
+ leading: "rounded-t-[12px] rounded-b-[4px]",
100
+ middle: "rounded-[4px]",
101
+ trailing: "rounded-t-[4px] rounded-b-[12px]",
102
+ standalone: "rounded-[4px]",
103
+ selected: "rounded-[12px]",
104
+ } as const;
105
+
106
+ // ─── Color token Tailwind classes ─────────────────────────────────────────────
107
+
108
+ /**
109
+ * Standard color variant tokens (SurfaceContainerLow-based).
110
+ * Source: StandardMenuTokens.kt
111
+ *
112
+ * Container: SurfaceContainerLow
113
+ * Text: OnSurface
114
+ * Icons: OnSurfaceVariant
115
+ * Selected container: TertiaryContainer
116
+ * Selected text/icons: OnTertiaryContainer
117
+ */
118
+ export const STANDARD_COLORS = {
119
+ /** Group/popup container background (StandardMenuTokens.ContainerColor) */
120
+ containerBg: "bg-m3-surface-container-low",
121
+ /** Label text color (StandardMenuTokens.ItemLabelTextColor) */
122
+ labelText: "text-m3-on-surface",
123
+ /** Leading/trailing icon color (StandardMenuTokens.ItemLeadingIconColor) */
124
+ iconColor: "text-m3-on-surface-variant",
125
+ /** Supporting text below label (StandardMenuTokens.ItemSupportingTextColor) */
126
+ supportingTextColor: "text-m3-on-surface-variant",
127
+ /** Trailing supporting text (StandardMenuTokens.ItemTrailingSupportingTextColor) */
128
+ trailingSupportingTextColor: "text-m3-on-surface-variant",
129
+ /** Trailing icon color (StandardMenuTokens.ItemTrailingIconColor) */
130
+ trailingIconColor: "text-m3-on-surface-variant",
131
+ /** Hover state layer (OnSurface @ 8% opacity) */
132
+ hoverLayer: "hover:bg-m3-on-surface/8",
133
+ /** Focus state layer (OnSurface @ 12% opacity) */
134
+ focusLayer: "focus:bg-m3-on-surface/12",
135
+ /** Selected item background (StandardMenuTokens.ItemSelectedContainerColor) */
136
+ selectedBg: "bg-m3-tertiary-container",
137
+ /** Selected item text (StandardMenuTokens.ItemSelectedLabelTextColor) */
138
+ selectedText: "text-m3-on-tertiary-container",
139
+ /** Selected item icon (StandardMenuTokens.ItemSelectedLeadingIconColor) */
140
+ selectedIcon: "text-m3-on-tertiary-container",
141
+ /** Disabled opacity: 38% (StandardMenuTokens.ItemDisabledLabelTextOpacity) */
142
+ disabledOpacity: "data-disabled:opacity-[0.38]",
143
+ } as const;
144
+
145
+ /**
146
+ * Baseline color variant tokens.
147
+ * Container uses SurfaceContainer, Selected uses SecondaryContainer.
148
+ */
149
+ export const BASELINE_COLORS = {
150
+ containerBg: "bg-m3-surface-container",
151
+ labelText: "text-m3-on-surface",
152
+ iconColor: "text-m3-on-surface-variant",
153
+ supportingTextColor: "text-m3-on-surface-variant",
154
+ trailingSupportingTextColor: "text-m3-on-surface-variant",
155
+ trailingIconColor: "text-m3-on-surface-variant",
156
+ hoverLayer: "hover:bg-m3-on-surface/8",
157
+ focusLayer: "focus:bg-m3-on-surface/12",
158
+ selectedBg: "bg-m3-secondary-container",
159
+ selectedText: "text-m3-on-secondary-container",
160
+ selectedIcon: "text-m3-on-secondary-container",
161
+ disabledOpacity: "data-disabled:opacity-[0.38]",
162
+ } as const;
163
+
164
+ /**
165
+ * Vibrant color variant tokens (TertiaryContainer-based).
166
+ * Source: VibrantMenuTokens.kt
167
+ *
168
+ * Container: TertiaryContainer
169
+ * Text: OnTertiaryContainer
170
+ * Icons: OnTertiaryContainer
171
+ * Selected container: Tertiary
172
+ * Selected text/icons: OnTertiary
173
+ */
174
+ export const VIBRANT_COLORS = {
175
+ /** Group/popup container background (VibrantMenuTokens.ContainerColor) */
176
+ containerBg: "bg-m3-tertiary-container",
177
+ /** Label text color (VibrantMenuTokens.ItemLabelTextColor) */
178
+ labelText: "text-m3-on-tertiary-container",
179
+ /** Leading/trailing icon color (VibrantMenuTokens.ItemLeadingIconColor) */
180
+ iconColor: "text-m3-on-tertiary-container",
181
+ /** Supporting text below label (VibrantMenuTokens.ItemSupportingTextColor) */
182
+ supportingTextColor: "text-m3-on-tertiary-container",
183
+ /** Trailing supporting text (VibrantMenuTokens.ItemTrailingSupportingTextColor) */
184
+ trailingSupportingTextColor: "text-m3-on-tertiary-container",
185
+ /** Trailing icon color (VibrantMenuTokens.ItemTrailingIconColor) */
186
+ trailingIconColor: "text-m3-on-tertiary-container",
187
+ /** Hover state layer (OnTertiaryContainer @ 8% opacity) */
188
+ hoverLayer: "hover:bg-m3-on-tertiary-container/8",
189
+ /** Focus state layer (OnTertiaryContainer @ 12% opacity) */
190
+ focusLayer: "focus:bg-m3-on-tertiary-container/12",
191
+ /** Selected item background (VibrantMenuTokens.ItemSelectedContainerColor = Tertiary) */
192
+ selectedBg: "bg-m3-tertiary",
193
+ /** Selected item text (VibrantMenuTokens.ItemSelectedLabelTextColor = OnTertiary) */
194
+ selectedText: "text-m3-on-tertiary",
195
+ /** Selected item icon (VibrantMenuTokens.ItemSelectedLeadingIconColor = OnTertiary) */
196
+ selectedIcon: "text-m3-on-tertiary",
197
+ /** Disabled opacity: 38% (VibrantMenuTokens.ItemDisabledLabelTextOpacity) */
198
+ disabledOpacity: "data-disabled:opacity-[0.38]",
199
+ } as const;
200
+
201
+ // ─── Divider ──────────────────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * HorizontalDivider padding: horizontal=12dp, vertical=2dp.
205
+ * Source: MenuDefaults.HorizontalDividerPadding
206
+ */
207
+ export const DIVIDER_PADDING = "mx-3 my-0.5"; // 12dp horizontal, 2dp vertical
208
+ export const DIVIDER_COLOR = "bg-m3-outline-variant";