@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,108 @@
1
+ /**
2
+ * @file app-bar-overflow-indicator.tsx
3
+ * MD3 Expressive App Bar Overflow Indicator.
4
+ *
5
+ * Renders a "More" (more_vert) icon button that opens a Radix UI
6
+ * DropdownMenu containing overflow App Bar items.
7
+ *
8
+ * Used internally by <AppBarRow> and <AppBarColumn> when items exceed
9
+ * the visible count.
10
+ */
11
+
12
+ import { cn } from "../../lib/utils";
13
+ import { Menu, MenuContent, MenuItem, MenuTrigger } from "../menu";
14
+ import { APP_BAR_COLORS, AppBarTokens } from "./app-bar.tokens";
15
+ import type { AppBarItem, AppBarOverflowIndicatorProps } from "./app-bar.types";
16
+
17
+ /** More vert icon for the overflow trigger button. */
18
+ function MoreVertIcon() {
19
+ return (
20
+ <span
21
+ className="material-symbols-rounded text-[24px] leading-none select-none"
22
+ aria-hidden="true"
23
+ >
24
+ more_vert
25
+ </span>
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Renders a single overflow item in the dropdown based on its type.
31
+ */
32
+ function OverflowItem({ item }: { item: AppBarItem }) {
33
+ if (item.type === "toggleable") {
34
+ return (
35
+ <MenuItem
36
+ role="menuitemcheckbox"
37
+ selected={item.checked ?? false}
38
+ onClick={() => item.onCheckedChange?.(!item.checked)}
39
+ disabled={item.enabled === false}
40
+ >
41
+ {item.label}
42
+ </MenuItem>
43
+ );
44
+ }
45
+
46
+ if (item.type === "custom" && item.menuContent) {
47
+ return (
48
+ <>
49
+ {item.menuContent({
50
+ isOpen: true,
51
+ open: () => {},
52
+ close: () => {},
53
+ })}
54
+ </>
55
+ );
56
+ }
57
+
58
+ // Default: clickable
59
+ return (
60
+ <MenuItem onClick={item.onClick} disabled={item.enabled === false}>
61
+ {item.label}
62
+ </MenuItem>
63
+ );
64
+ }
65
+
66
+ /**
67
+ * MD3 App Bar Overflow Indicator.
68
+ *
69
+ * Renders a "more_vert" button that opens a dropdown menu
70
+ * with overflow action items.
71
+ */
72
+ export function AppBarOverflowIndicator({
73
+ items,
74
+ className,
75
+ }: AppBarOverflowIndicatorProps) {
76
+ if (items.length === 0) return null;
77
+
78
+ return (
79
+ <Menu>
80
+ <MenuTrigger asChild>
81
+ <button
82
+ type="button"
83
+ className={cn(
84
+ "flex items-center justify-center rounded-full",
85
+ "focus-visible:outline-none focus-visible:ring-2",
86
+ className,
87
+ )}
88
+ style={{
89
+ width: AppBarTokens.iconButtonTouchTarget,
90
+ height: AppBarTokens.iconButtonTouchTarget,
91
+ color: APP_BAR_COLORS.actionIcon,
92
+ }}
93
+ aria-label="More actions"
94
+ aria-haspopup="menu"
95
+ >
96
+ <MoreVertIcon />
97
+ </button>
98
+ </MenuTrigger>
99
+
100
+ <MenuContent align="end" sideOffset={4}>
101
+ {items.map((item, index) => (
102
+ // biome-ignore lint/suspicious/noArrayIndexKey: static list from props
103
+ <OverflowItem key={index} item={item} />
104
+ ))}
105
+ </MenuContent>
106
+ </Menu>
107
+ );
108
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @file app-bar-row.tsx
3
+ * MD3 Expressive App Bar Row DSL.
4
+ *
5
+ * Displays App Bar action items in a horizontal row.
6
+ * Items that exceed `maxItemCount` or available width collapse into a
7
+ * dropdown menu via <AppBarOverflowIndicator>.
8
+ *
9
+ * Translated from OverflowMeasurePolicy / AppBarRow.kt in Jetpack Compose M3.
10
+ *
11
+ * @see docs/m3/app-bars/AppBarRow.kt
12
+ */
13
+
14
+ import * as React from "react";
15
+ import { cn } from "../../lib/utils";
16
+ import { AppBarTokens } from "./app-bar.tokens";
17
+ import type { AppBarRowProps } from "./app-bar.types";
18
+ import { AppBarItemButton } from "./app-bar-item-button";
19
+ import { AppBarOverflowIndicator } from "./app-bar-overflow-indicator";
20
+
21
+ /**
22
+ * MD3 Expressive App Bar Row.
23
+ *
24
+ * Renders action items in a row. Compatible with the `actions` prop of any App Bar.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <SmallAppBar
29
+ * title="Messages"
30
+ * actions={
31
+ * <AppBarRow
32
+ * maxItemCount={2}
33
+ * items={[
34
+ * { type: 'clickable', icon: <Icon>search</Icon>, label: 'Search', onClick: handleSearch },
35
+ * { type: 'clickable', icon: <Icon>bookmark</Icon>, label: 'Bookmarks', onClick: handleBookmark },
36
+ * { type: 'clickable', icon: <Icon>settings</Icon>, label: 'Settings', onClick: handleSettings },
37
+ * ]}
38
+ * />
39
+ * }
40
+ * />
41
+ * ```
42
+ */
43
+ export function AppBarRow({ items, maxItemCount, className }: AppBarRowProps) {
44
+ const containerRef = React.useRef<HTMLDivElement>(null);
45
+ const [visibleCount, setVisibleCount] = React.useState(
46
+ maxItemCount ?? items.length,
47
+ );
48
+
49
+ React.useEffect(() => {
50
+ if (maxItemCount !== undefined) {
51
+ setVisibleCount(Math.min(maxItemCount, items.length));
52
+ return;
53
+ }
54
+
55
+ const container = containerRef.current;
56
+ if (!container) return;
57
+
58
+ let debounceTimer: ReturnType<typeof setTimeout>;
59
+
60
+ const observer = new ResizeObserver((entries) => {
61
+ clearTimeout(debounceTimer);
62
+ debounceTimer = setTimeout(() => {
63
+ const entry = entries[0];
64
+ if (!entry) return;
65
+
66
+ const available = entry.contentRect.width;
67
+ const itemWidth = AppBarTokens.iconButtonTouchTarget;
68
+ const hasOverflow = items.length > Math.floor(available / itemWidth);
69
+ const reservedWidth = hasOverflow ? itemWidth : 0;
70
+ const count = Math.max(
71
+ 0,
72
+ Math.floor((available - reservedWidth) / itemWidth),
73
+ );
74
+ setVisibleCount(Math.min(count, items.length));
75
+ }, 100);
76
+ });
77
+
78
+ observer.observe(container);
79
+ return () => {
80
+ clearTimeout(debounceTimer);
81
+ observer.disconnect();
82
+ };
83
+ }, [items.length, maxItemCount]);
84
+
85
+ const visibleItems = items.slice(0, visibleCount);
86
+ const overflowItems = items.slice(visibleCount);
87
+
88
+ return (
89
+ <div
90
+ ref={containerRef}
91
+ className={cn("flex items-center", className)}
92
+ style={{ gap: AppBarTokens.iconButtonSpace }}
93
+ >
94
+ {visibleItems.map((item, index) => (
95
+ // biome-ignore lint/suspicious/noArrayIndexKey: items are static from props
96
+ <AppBarItemButton key={index} item={item} />
97
+ ))}
98
+
99
+ {overflowItems.length > 0 && (
100
+ <AppBarOverflowIndicator items={overflowItems} />
101
+ )}
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,87 @@
1
+ import { fireEvent, render, renderHook, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ BottomAppBar,
5
+ SearchAppBar,
6
+ SmallAppBar,
7
+ useAppBarScroll,
8
+ } from "./index";
9
+
10
+ describe("SmallAppBar", () => {
11
+ it("renders title correctly", () => {
12
+ render(<SmallAppBar title="My Title" />);
13
+ expect(screen.getByText("My Title")).toBeInTheDocument();
14
+ });
15
+
16
+ it("renders subtitle correctly", () => {
17
+ render(<SmallAppBar title="Main" subtitle="Sub" />);
18
+ expect(screen.getByText("Sub")).toBeInTheDocument();
19
+ });
20
+
21
+ it("applies role='banner' to the header", () => {
22
+ render(<SmallAppBar title="Role Test" />);
23
+ expect(screen.getByRole("banner")).toBeInTheDocument();
24
+ });
25
+
26
+ it("renders navigation icon", () => {
27
+ render(
28
+ <SmallAppBar
29
+ title="Nav Test"
30
+ navigationIcon={<button type="button" aria-label="Menu" />}
31
+ />,
32
+ );
33
+ expect(screen.getByRole("button", { name: "Menu" })).toBeInTheDocument();
34
+ });
35
+
36
+ it("renders actions", () => {
37
+ render(
38
+ <SmallAppBar
39
+ title="Actions Test"
40
+ actions={<button type="button" aria-label="Search" />}
41
+ />,
42
+ );
43
+ expect(screen.getByRole("button", { name: "Search" })).toBeInTheDocument();
44
+ });
45
+ });
46
+
47
+ describe("SearchAppBar", () => {
48
+ it("renders search bar with role='search'", () => {
49
+ render(<SearchAppBar searchPlaceholder="Search inside" />);
50
+ const searchContainer = screen.getByRole("search");
51
+ expect(searchContainer).toBeInTheDocument();
52
+ expect(searchContainer).toHaveAttribute("aria-label", "Search inside");
53
+ });
54
+
55
+ it("fires onSearchFocus when clicked", () => {
56
+ const onFocus = vi.fn();
57
+ render(<SearchAppBar onSearchFocus={onFocus} />);
58
+ const searchContainer = screen.getByRole("search");
59
+ fireEvent.click(searchContainer);
60
+ expect(onFocus).toHaveBeenCalled();
61
+ });
62
+ });
63
+
64
+ describe("BottomAppBar", () => {
65
+ it("applies role='navigation' to the container", () => {
66
+ render(<BottomAppBar />);
67
+ expect(screen.getByRole("navigation")).toBeInTheDocument();
68
+ });
69
+
70
+ it("renders FAB correctly", () => {
71
+ render(
72
+ <BottomAppBar
73
+ floatingActionButton={<button type="button" aria-label="Add" />}
74
+ />,
75
+ );
76
+ expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
77
+ });
78
+ });
79
+
80
+ describe("useAppBarScroll", () => {
81
+ it("initializes with not scrolled", () => {
82
+ const { result } = renderHook(() =>
83
+ useAppBarScroll({ behavior: "pinned" }),
84
+ );
85
+ expect(result.current.isScrolled).toBe(false);
86
+ });
87
+ });
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @file app-bar.tokens.ts
3
+ * MD3 Expressive App Bar — Design tokens ported from:
4
+ * - AppBarTokens.kt (shared tokens)
5
+ * - AppBarSmallTokens.kt
6
+ * - AppBarMediumFlexibleTokens.kt
7
+ * - AppBarLargeFlexibleTokens.kt
8
+ * - BottomAppBarTokens.kt
9
+ * - DockedToolbarTokens.kt
10
+ * - FabSecondaryContainerTokens.kt
11
+ *
12
+ * All dimensional values are in px (dp equivalents for web at 1dp = 1px).
13
+ * Colors reference CSS custom properties — do NOT hardcode hex.
14
+ * @see docs/m3/app-bars/
15
+ */
16
+
17
+ // ─── Dimensional Tokens ───────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Height and spacing tokens for all App Bar variants.
21
+ * Maps directly from MD3 Kotlin token files.
22
+ */
23
+ export const AppBarTokens = {
24
+ // ── Heights ─────────────────────────────────────────────────────────────
25
+ heights: {
26
+ /** SmallAppBar height. AppBarSmallTokens.ContainerHeight = 64dp */
27
+ small: 64,
28
+ /** Collapsed height for flexible variants. = SmallAppBar height. */
29
+ flexibleCollapsed: 64,
30
+ /** MediumFlexibleAppBar expanded height (without subtitle). AppBarMediumFlexibleTokens */
31
+ mediumFlexExpanded: 112,
32
+ /** MediumFlexibleAppBar expanded height (with subtitle). */
33
+ mediumFlexWithSubtitleExpanded: 136,
34
+ /** LargeFlexibleAppBar expanded height (without subtitle). AppBarLargeFlexibleTokens */
35
+ largeFlexExpanded: 120,
36
+ /** LargeFlexibleAppBar expanded height (with subtitle). */
37
+ largeFlexWithSubtitleExpanded: 152,
38
+ /** BottomAppBar height. BottomAppBarTokens.ContainerHeight = 80dp */
39
+ bottom: 80,
40
+ /** DockedToolbar height. DockedToolbarTokens.ContainerHeight = 64dp */
41
+ dockedToolbar: 64,
42
+ },
43
+
44
+ // ── Icon and Avatar sizes ──────────────────────────────────────────────
45
+ /** AppBarTokens.IconSize = 24dp */
46
+ iconSize: 24,
47
+ /** AppBarTokens.AvatarSize = 32dp */
48
+ avatarSize: 32,
49
+
50
+ // ── Spacing ──────────────────────────────────────────────────────────────
51
+ /** AppBarTokens.LeadingSpace = 4dp */
52
+ leadingSpace: 4,
53
+ /** AppBarTokens.TrailingSpace = 4dp */
54
+ trailingSpace: 4,
55
+ /** AppBarTokens.IconButtonSpace = 0dp (no gap between icon buttons) */
56
+ iconButtonSpace: 0,
57
+
58
+ // ── Docked Toolbar spacing ────────────────────────────────────────────
59
+ dockedToolbar: {
60
+ /** DockedToolbarTokens.ContainerLeadingSpace = 16dp */
61
+ leadingSpace: 16,
62
+ /** DockedToolbarTokens.ContainerTrailingSpace = 16dp */
63
+ trailingSpace: 16,
64
+ /** DockedToolbarTokens.ContainerMinSpacing = 4dp */
65
+ minSpacing: 4,
66
+ /** DockedToolbarTokens.ContainerMaxSpacing = 32dp */
67
+ maxSpacing: 32,
68
+ },
69
+
70
+ // ── Touch targets ─────────────────────────────────────────────────────
71
+ /** Minimum 48px touch target for icon buttons per MD3 accessibility spec. */
72
+ iconButtonTouchTarget: 48,
73
+ } as const;
74
+
75
+ // ─── Typography Tokens ────────────────────────────────────────────────────────
76
+
77
+ /**
78
+ * MD3 type scale values mapped to Tailwind CSS class strings.
79
+ * Used across App Bar variants for consistent typography.
80
+ *
81
+ * Values derived from MD3 Material Type Scale specification.
82
+ */
83
+ export const appBarTypography = {
84
+ /**
85
+ * SmallAppBar title (collapsed state for flexible variants).
86
+ * AppBarSmallTokens.TitleFont = TitleLarge
87
+ * Spec: 22sp / 28sp line-height / medium weight
88
+ */
89
+ titleLarge: "text-[22px] leading-[28px] font-medium tracking-[0px]",
90
+
91
+ /**
92
+ * SmallAppBar subtitle.
93
+ * AppBarSmallTokens.SubtitleFont = LabelMedium
94
+ * Spec: 12sp / 16sp line-height / medium weight / 0.5px tracking
95
+ */
96
+ labelMedium: "text-[12px] leading-[16px] font-medium tracking-[0.5px]",
97
+
98
+ /**
99
+ * MediumFlexibleAppBar expanded title.
100
+ * AppBarMediumFlexibleTokens.TitleFont = HeadlineMedium
101
+ * Spec: 28sp / 36sp line-height / normal weight
102
+ */
103
+ headlineMedium: "text-[28px] leading-[36px] font-normal tracking-[0px]",
104
+
105
+ /**
106
+ * MediumFlexibleAppBar subtitle.
107
+ * AppBarMediumFlexibleTokens.SubtitleFont = LabelLarge
108
+ * Spec: 14sp / 20sp line-height / medium weight / 0.1px tracking
109
+ */
110
+ labelLarge: "text-[14px] leading-[20px] font-medium tracking-[0.1px]",
111
+
112
+ /**
113
+ * LargeFlexibleAppBar expanded title.
114
+ * AppBarLargeFlexibleTokens.TitleFont = DisplaySmall
115
+ * Spec: 36sp / 44sp line-height / normal weight / -0.25px tracking
116
+ */
117
+ displaySmall: "text-[36px] leading-[44px] font-normal tracking-[-0.25px]",
118
+
119
+ /**
120
+ * LargeFlexibleAppBar subtitle.
121
+ * AppBarLargeFlexibleTokens.SubtitleFont = TitleMedium
122
+ * Spec: 16sp / 24sp line-height / medium weight / 0.15px tracking
123
+ */
124
+ titleMedium: "text-[16px] leading-6 font-medium tracking-[0.15px]",
125
+ } as const;
126
+
127
+ // ─── Color Tokens ─────────────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * CSS custom property references for App Bar colors.
131
+ * Maps to --md-sys-color-* tokens in the MD3 theme system.
132
+ *
133
+ * IMPORTANT: Never hardcode hex/rgba values here — these references
134
+ * automatically adapt to light/dark theme via the MD3ThemeProvider.
135
+ *
136
+ * AppBarTokens.kt spec:
137
+ * - ContainerColor → md-sys-color-surface
138
+ * - OnScrollContainerColor → md-sys-color-surface-container
139
+ */
140
+ export const APP_BAR_COLORS = {
141
+ // ── Container ────────────────────────────────────────────────────────────
142
+ /** Default background. AppBarTokens.ContainerColor → surface */
143
+ container: "var(--md-sys-color-surface)",
144
+ /** Background when content is scrolled. AppBarTokens.OnScrollContainerColor → surface-container */
145
+ scrolledContainer: "var(--md-sys-color-surface-container)",
146
+
147
+ // ── Content ──────────────────────────────────────────────────────────────
148
+ /** Title color. AppBarTokens.TitleColor → on-surface */
149
+ title: "var(--md-sys-color-on-surface)",
150
+ /** Subtitle color. AppBarTokens.SubtitleColor → on-surface-variant */
151
+ subtitle: "var(--md-sys-color-on-surface-variant)",
152
+ /** Navigation icon color. AppBarTokens.LeadingIconColor → on-surface */
153
+ navigationIcon: "var(--md-sys-color-on-surface)",
154
+ /** Action icon color. AppBarTokens.TrailingIconColor → on-surface-variant */
155
+ actionIcon: "var(--md-sys-color-on-surface-variant)",
156
+
157
+ // ── Search Bar ───────────────────────────────────────────────────────────
158
+ /** Search bar pill background. → surface-container-high */
159
+ searchBarBg: "var(--md-sys-color-surface-container-high)",
160
+ /** Search bar text/icon color. */
161
+ searchBarContent: "var(--md-sys-color-on-surface-variant)",
162
+
163
+ // ── Bottom App Bar ───────────────────────────────────────────────────────
164
+ /** BottomAppBarTokens.ContainerColor → surface-container */
165
+ bottomContainer: "var(--md-sys-color-surface-container)",
166
+
167
+ // ── FAB on Bottom App Bar ────────────────────────────────────────────────
168
+ /** FabSecondaryContainerTokens.ContainerColor → secondary-container */
169
+ fabContainer: "var(--md-sys-color-secondary-container)",
170
+ /** FabSecondaryContainerTokens.IconColor → on-secondary-container */
171
+ fabIcon: "var(--md-sys-color-on-secondary-container)",
172
+ } as const;
173
+
174
+ // ─── Animation Constants ──────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Color transition when App Bar background changes on scroll.
178
+ * MD3 Standard easing: cubic-bezier(0.2, 0, 0, 1), 200ms.
179
+ */
180
+ export const APP_BAR_COLOR_TRANSITION = {
181
+ duration: 0.2,
182
+ ease: [0.2, 0, 0, 1] as [number, number, number, number],
183
+ } as const;
184
+
185
+ /**
186
+ * Spring animation for enterAlways behavior (hide/show on scroll direction).
187
+ * Equivalent to MD3 FastSpatial motion scheme.
188
+ */
189
+ export const APP_BAR_ENTER_ALWAYS_SPRING = {
190
+ type: "spring",
191
+ stiffness: 380,
192
+ damping: 40,
193
+ mass: 1,
194
+ } as const;
195
+
196
+ /**
197
+ * Spring animation for Bottom App Bar hide/show.
198
+ * Slightly looser feel for bottom navigation.
199
+ */
200
+ export const APP_BAR_BOTTOM_SPRING = {
201
+ type: "spring",
202
+ stiffness: 300,
203
+ damping: 30,
204
+ } as const;
205
+
206
+ /**
207
+ * SearchView appearance/disappearance transition.
208
+ * Uses spring for natural feel of expanding overlay.
209
+ */
210
+ export const SEARCH_VIEW_SPRING = {
211
+ type: "spring",
212
+ stiffness: 400,
213
+ damping: 35,
214
+ } as const;
215
+
216
+ /**
217
+ * Title crossfade transition for flexible App Bars.
218
+ * Short duration keeps the collapse feeling snappy.
219
+ */
220
+ export const APP_BAR_TITLE_FADE = {
221
+ duration: 0.15,
222
+ ease: "easeInOut",
223
+ } as const;