@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,206 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ MD3_EXPRESSIVE_FONT_VARIATION,
5
+ Typography,
6
+ TypographyKeyTokens,
7
+ TypographyProvider,
8
+ TypographyTokens,
9
+ useTypography,
10
+ } from "../ui/typography";
11
+
12
+ // ─── TypographyTokens ─────────────────────────────────────────────────────────
13
+
14
+ describe("TypographyTokens", () => {
15
+ it("creates 30 styles (15 baseline + 15 emphasized)", () => {
16
+ const tokens = new TypographyTokens();
17
+ const baseline = [
18
+ "BodyLarge",
19
+ "BodyMedium",
20
+ "BodySmall",
21
+ "DisplayLarge",
22
+ "DisplayMedium",
23
+ "DisplaySmall",
24
+ "HeadlineLarge",
25
+ "HeadlineMedium",
26
+ "HeadlineSmall",
27
+ "LabelLarge",
28
+ "LabelMedium",
29
+ "LabelSmall",
30
+ "TitleLarge",
31
+ "TitleMedium",
32
+ "TitleSmall",
33
+ ] as const;
34
+ const emphasized = baseline.map((k) => `${k}Emphasized` as const);
35
+
36
+ for (const key of baseline) {
37
+ expect(tokens[key]).toBeDefined();
38
+ }
39
+ for (const key of emphasized) {
40
+ expect((tokens as unknown as Record<string, unknown>)[key]).toBeDefined();
41
+ }
42
+ });
43
+
44
+ it("applies ROND 100 fontVariationSettings to all styles", () => {
45
+ const tokens = new TypographyTokens();
46
+ const styles = [
47
+ tokens.BodyLarge,
48
+ tokens.DisplayLarge,
49
+ tokens.HeadlineMedium,
50
+ tokens.LabelSmall,
51
+ tokens.TitleMedium,
52
+ tokens.BodyLargeEmphasized,
53
+ tokens.DisplayLargeEmphasized,
54
+ tokens.LabelSmallEmphasized,
55
+ ];
56
+
57
+ for (const style of styles) {
58
+ expect(style.fontVariationSettings).toBe(MD3_EXPRESSIVE_FONT_VARIATION);
59
+ expect(style.fontVariationSettings).toBe('"ROND" 100');
60
+ }
61
+ });
62
+
63
+ it("uses Google Sans Flex as default font family", () => {
64
+ const tokens = new TypographyTokens();
65
+ expect(tokens.BodyLarge.fontFamily).toContain("Google Sans Flex");
66
+ expect(tokens.DisplayLarge.fontFamily).toContain("Google Sans Flex");
67
+ });
68
+
69
+ it("overrides font family when provided", () => {
70
+ const customFont = "'Inter', sans-serif";
71
+ const tokens = new TypographyTokens(customFont);
72
+ expect(tokens.BodyLarge.fontFamily).toBe(customFont);
73
+ expect(tokens.DisplayLargeEmphasized.fontFamily).toBe(customFont);
74
+ });
75
+
76
+ it("emphasized styles have higher fontWeight than baseline", () => {
77
+ const tokens = new TypographyTokens();
78
+ expect(tokens.BodyLargeEmphasized.fontWeight).toBeGreaterThan(
79
+ tokens.BodyLarge.fontWeight,
80
+ );
81
+ expect(tokens.DisplayLargeEmphasized.fontWeight).toBeGreaterThan(
82
+ tokens.DisplayLarge.fontWeight,
83
+ );
84
+ expect(tokens.LabelLargeEmphasized.fontWeight).toBeGreaterThan(
85
+ tokens.LabelLarge.fontWeight,
86
+ );
87
+ });
88
+
89
+ it("styles are frozen (immutable)", () => {
90
+ const tokens = new TypographyTokens();
91
+ expect(Object.isFrozen(tokens.BodyLarge)).toBe(true);
92
+ expect(Object.isFrozen(tokens.DisplayLargeEmphasized)).toBe(true);
93
+ });
94
+ });
95
+
96
+ // ─── Typography class ─────────────────────────────────────────────────────────
97
+
98
+ describe("Typography", () => {
99
+ it("constructs with default tokens", () => {
100
+ const typography = new Typography();
101
+ expect(typography.bodyLarge).toBeDefined();
102
+ expect(typography.displayLargeEmphasized).toBeDefined();
103
+ });
104
+
105
+ it("fromToken returns correct style for all 30 keys", () => {
106
+ const typography = new Typography();
107
+ const allKeys = Object.values(TypographyKeyTokens);
108
+ expect(allKeys).toHaveLength(30);
109
+
110
+ for (const key of allKeys) {
111
+ const style = typography.fromToken(key);
112
+ expect(style).toBeDefined();
113
+ expect(style.fontVariationSettings).toBe('"ROND" 100');
114
+ }
115
+ });
116
+
117
+ it("fromToken(BodyLarge) matches bodyLarge property", () => {
118
+ const typography = new Typography();
119
+ expect(typography.fromToken(TypographyKeyTokens.BodyLarge)).toBe(
120
+ typography.bodyLarge,
121
+ );
122
+ });
123
+
124
+ it("fromToken(DisplayLargeEmphasized) matches displayLargeEmphasized property", () => {
125
+ const typography = new Typography();
126
+ expect(
127
+ typography.fromToken(TypographyKeyTokens.DisplayLargeEmphasized),
128
+ ).toBe(typography.displayLargeEmphasized);
129
+ });
130
+
131
+ it("copy() creates a new instance with overridden styles", () => {
132
+ const original = new Typography();
133
+ const customStyle = { ...original.bodyLarge, fontWeight: 900 };
134
+ const modified = original.copy({ bodyLarge: customStyle });
135
+
136
+ expect(modified.bodyLarge.fontWeight).toBe(900);
137
+ expect(modified.displayLarge).toBe(original.displayLarge);
138
+ });
139
+ });
140
+
141
+ // ─── useTypography + TypographyProvider ───────────────────────────────────────
142
+
143
+ function TestConsumer() {
144
+ const typography = useTypography();
145
+ return (
146
+ <div>
147
+ <span data-testid="font-size">{typography.bodyLarge.fontSize}</span>
148
+ <span data-testid="font-weight">
149
+ {typography.displayLargeEmphasized.fontWeight}
150
+ </span>
151
+ <span data-testid="variation">
152
+ {typography.bodyMedium.fontVariationSettings}
153
+ </span>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ describe("useTypography", () => {
159
+ it("returns default Typography when no Provider is present", () => {
160
+ render(<TestConsumer />);
161
+ expect(screen.getByTestId("font-size").textContent).toBeTruthy();
162
+ expect(screen.getByTestId("variation").textContent).toBe('"ROND" 100');
163
+ });
164
+
165
+ it("returns Typography from TypographyProvider", () => {
166
+ render(
167
+ <TypographyProvider>
168
+ <TestConsumer />
169
+ </TypographyProvider>,
170
+ );
171
+ expect(screen.getByTestId("font-size").textContent).toBeTruthy();
172
+ expect(screen.getByTestId("variation").textContent).toBe('"ROND" 100');
173
+ });
174
+
175
+ it("allows custom fontFamily via TypographyProvider", () => {
176
+ function CustomConsumer() {
177
+ const typography = useTypography();
178
+ return (
179
+ <span data-testid="custom-family">
180
+ {typography.bodyLarge.fontFamily}
181
+ </span>
182
+ );
183
+ }
184
+
185
+ render(
186
+ <TypographyProvider fontFamily="'Inter', sans-serif">
187
+ <CustomConsumer />
188
+ </TypographyProvider>,
189
+ );
190
+ expect(screen.getByTestId("custom-family").textContent).toBe(
191
+ "'Inter', sans-serif",
192
+ );
193
+ });
194
+
195
+ it("emphasized fontWeight is higher than baseline", () => {
196
+ render(
197
+ <TypographyProvider>
198
+ <TestConsumer />
199
+ </TypographyProvider>,
200
+ );
201
+ const emphasizedWeight = Number(
202
+ screen.getByTestId("font-weight").textContent,
203
+ );
204
+ expect(emphasizedWeight).toBeGreaterThanOrEqual(700);
205
+ });
206
+ });
@@ -0,0 +1,7 @@
1
+ export type {
2
+ MD3ColorStyle,
3
+ MD3Shape,
4
+ MD3Size,
5
+ PolymorphicProps,
6
+ PolymorphicRef,
7
+ } from "./md3";
@@ -0,0 +1,31 @@
1
+ import type {
2
+ ComponentPropsWithoutRef,
3
+ ComponentPropsWithRef,
4
+ ElementType,
5
+ ReactNode,
6
+ } from "react";
7
+
8
+ /** MD3 button color variants */
9
+ export type MD3ColorStyle =
10
+ | "elevated"
11
+ | "filled"
12
+ | "tonal"
13
+ | "outlined"
14
+ | "text";
15
+
16
+ /** MD3 Expressive button sizes */
17
+ export type MD3Size = "xs" | "sm" | "md" | "lg" | "xl";
18
+
19
+ /** MD3 shape families */
20
+ export type MD3Shape = "round" | "square";
21
+
22
+ /** Helper: PolyMorphic component ref */
23
+ export type PolymorphicRef<C extends ElementType> =
24
+ ComponentPropsWithRef<C>["ref"];
25
+
26
+ /** Helper: Props cho polymorphic components */
27
+ export type PolymorphicProps<C extends ElementType, Props = object> = Props &
28
+ Omit<ComponentPropsWithoutRef<C>, keyof Props> & {
29
+ as?: C;
30
+ children?: ReactNode;
31
+ };
@@ -0,0 +1,60 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ const typographyVariants = cva("m-0 p-0 text-m3-on-surface", {
6
+ variants: {
7
+ variant: {
8
+ "display-lg": "text-[57px] leading-[64px] font-normal tracking-[-0.25px]",
9
+ "display-md": "text-[45px] leading-[52px] font-normal tracking-[0px]",
10
+ "display-sm": "text-[36px] leading-[44px] font-normal tracking-[0px]",
11
+ "headline-lg": "text-[32px] leading-[40px] font-normal tracking-[0px]",
12
+ "headline-md": "text-[28px] leading-[36px] font-normal tracking-[0px]",
13
+ "headline-sm": "text-[24px] leading-[32px] font-normal tracking-[0px]",
14
+ "title-lg": "text-[22px] leading-[28px] font-normal tracking-[0px]",
15
+ "title-md": "text-[16px] leading-[24px] font-medium tracking-[0.15px]",
16
+ "title-sm": "text-[14px] leading-[20px] font-medium tracking-[0.1px]",
17
+ "label-lg": "text-[14px] leading-[20px] font-medium tracking-[0.1px]",
18
+ "label-md": "text-[12px] leading-[16px] font-medium tracking-[0.5px]",
19
+ "label-sm": "text-[11px] leading-[16px] font-medium tracking-[0.5px]",
20
+ "body-lg": "text-[16px] leading-[24px] font-normal tracking-[0.5px]",
21
+ "body-md": "text-[14px] leading-[20px] font-normal tracking-[0.25px]",
22
+ "body-sm": "text-[12px] leading-[16px] font-normal tracking-[0.4px]",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ variant: "body-md",
27
+ },
28
+ });
29
+
30
+ export interface TextProps
31
+ extends React.HTMLAttributes<HTMLElement>,
32
+ VariantProps<typeof typographyVariants> {
33
+ as?: React.ElementType;
34
+ }
35
+
36
+ const Text = React.forwardRef<HTMLElement, TextProps>(
37
+ ({ className, variant, as: Component, ...props }, ref) => {
38
+ // Default component based on variant if not provided
39
+ const defaultComponent = React.useMemo(() => {
40
+ if (variant?.startsWith("display") || variant?.startsWith("headline"))
41
+ return "h1";
42
+ if (variant?.startsWith("title")) return "h2";
43
+ return "p";
44
+ }, [variant]);
45
+
46
+ const Tag = Component || defaultComponent;
47
+
48
+ return (
49
+ <Tag
50
+ ref={ref}
51
+ className={cn(typographyVariants({ variant, className }))}
52
+ {...props}
53
+ />
54
+ );
55
+ },
56
+ );
57
+
58
+ Text.displayName = "Text";
59
+
60
+ export { Text, typographyVariants };
@@ -0,0 +1,63 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Divider > snapshots > flat/horizontal (default) 1`] = `
4
+ <div
5
+ aria-orientation="horizontal"
6
+ class="block shrink-0 bg-m3-outline-variant h-px w-full"
7
+ role="separator"
8
+ style="transform-origin: left; transform: scaleX(0);"
9
+ />
10
+ `;
11
+
12
+ exports[`Divider > snapshots > flat/vertical 1`] = `
13
+ <div
14
+ aria-orientation="vertical"
15
+ class="block shrink-0 bg-m3-outline-variant w-px h-full self-stretch"
16
+ role="separator"
17
+ style="transform-origin: top; transform: scaleY(0);"
18
+ />
19
+ `;
20
+
21
+ exports[`Divider > snapshots > inset 1`] = `
22
+ <div
23
+ aria-orientation="horizontal"
24
+ class="block shrink-0 bg-m3-outline-variant h-px w-full ml-4"
25
+ role="separator"
26
+ style="transform-origin: left; transform: scaleX(0);"
27
+ />
28
+ `;
29
+
30
+ exports[`Divider > snapshots > middle-inset 1`] = `
31
+ <div
32
+ aria-orientation="horizontal"
33
+ class="block shrink-0 bg-m3-outline-variant h-px w-full mx-4"
34
+ role="separator"
35
+ style="transform-origin: left; transform: scaleX(0);"
36
+ />
37
+ `;
38
+
39
+ exports[`Divider > snapshots > wavy 1`] = `
40
+ <div
41
+ aria-orientation="horizontal"
42
+ class="block shrink-0 overflow-hidden w-full"
43
+ role="separator"
44
+ style="height: 5px;"
45
+ >
46
+ <svg
47
+ aria-hidden="true"
48
+ class="text-m3-outline-variant block"
49
+ height="100%"
50
+ style="overflow: visible; transform-origin: left center; opacity: 0; transform: scaleX(0);"
51
+ width="100%"
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ >
54
+ <path
55
+ d="M 0.5,2.5 C 5.833333333333333,-0.16666666666666652 11.166666666666668,-0.16666666666666652 16.5,2.5 C 21.833333333333332,5.166666666666666 27.166666666666668,5.166666666666666 32.5,2.5 C 37.833333333333336,-0.16666666666666652 43.166666666666664,-0.16666666666666652 48.5,2.5 C 53.833333333333336,5.166666666666666 59.166666666666664,5.166666666666666 64.5,2.5 C 69.83333333333333,-0.16666666666666652 75.16666666666667,-0.16666666666666652 80.5,2.5 C 85.83333333333333,5.166666666666666 91.16666666666667,5.166666666666666 96.5,2.5 C 101.83333333333333,-0.16666666666666652 107.16666666666667,-0.16666666666666652 112.5,2.5 C 117.83333333333333,5.166666666666666 123.16666666666667,5.166666666666666 128.5,2.5 C 133.83333333333334,-0.16666666666666652 139.16666666666666,-0.16666666666666652 144.5,2.5 C 149.83333333333334,5.166666666666666 155.16666666666666,5.166666666666666 160.5,2.5 C 165.83333333333334,-0.16666666666666652 171.16666666666666,-0.16666666666666652 176.5,2.5 C 181.83333333333334,5.166666666666666 187.16666666666666,5.166666666666666 192.5,2.5 C 197.83333333333334,-0.16666666666666652 203.16666666666666,-0.16666666666666652 208.5,2.5 C 213.83333333333334,5.166666666666666 219.16666666666666,5.166666666666666 224.5,2.5 C 229.83333333333334,-0.16666666666666652 235.16666666666666,-0.16666666666666652 240.5,2.5 C 245.83333333333334,5.166666666666666 251.16666666666666,5.166666666666666 256.5,2.5 C 261.8333333333333,-0.16666666666666652 267.1666666666667,-0.16666666666666652 272.5,2.5 C 277.8333333333333,5.166666666666666 283.1666666666667,5.166666666666666 288.5,2.5 C 293.8333333333333,-0.16666666666666652 299.1666666666667,-0.16666666666666652 304.5,2.5 C 309.8333333333333,5.166666666666666 315.1666666666667,5.166666666666666 320.5,2.5 C 325.8333333333333,-0.16666666666666652 331.1666666666667,-0.16666666666666652 336.5,2.5 C 341.8333333333333,5.166666666666666 347.1666666666667,5.166666666666666 352.5,2.5 C 357.8333333333333,-0.16666666666666652 363.1666666666667,-0.16666666666666652 368.5,2.5 C 373.8333333333333,5.166666666666666 379.1666666666667,5.166666666666666 384.5,2.5 C 389.8333333333333,-0.16666666666666652 395.1666666666667,-0.16666666666666652 400.5,2.5 C 405.8333333333333,5.166666666666666 411.1666666666667,5.166666666666666 416.5,2.5 C 421.8333333333333,-0.16666666666666652 427.1666666666667,-0.16666666666666652 432.5,2.5 C 437.8333333333333,5.166666666666666 443.1666666666667,5.166666666666666 448.5,2.5 C 453.8333333333333,-0.16666666666666652 459.1666666666667,-0.16666666666666652 464.5,2.5 C 469.8333333333333,5.166666666666666 475.1666666666667,5.166666666666666 480.5,2.5 C 485.8333333333333,-0.16666666666666652 491.1666666666667,-0.16666666666666652 496.5,2.5 C 501.8333333333333,5.166666666666666 507.1666666666667,5.166666666666666 512.5,2.5 C 517.8333333333334,-0.16666666666666652 523.1666666666666,-0.16666666666666652 528.5,2.5 C 533.8333333333334,5.166666666666666 539.1666666666666,5.166666666666666 544.5,2.5 C 549.8333333333334,-0.16666666666666652 555.1666666666666,-0.16666666666666652 560.5,2.5 C 565.8333333333334,5.166666666666666 571.1666666666666,5.166666666666666 576.5,2.5 C 581.8333333333334,-0.16666666666666652 587.1666666666666,-0.16666666666666652 592.5,2.5 C 597.8333333333334,5.166666666666666 603.1666666666666,5.166666666666666 608.5,2.5 C 613.8333333333334,-0.16666666666666652 619.1666666666666,-0.16666666666666652 624.5,2.5 C 629.8333333333334,5.166666666666666 635.1666666666666,5.166666666666666 640.5,2.5 C 645.8333333333334,-0.16666666666666652 651.1666666666666,-0.16666666666666652 656.5,2.5 C 661.8333333333334,5.166666666666666 667.1666666666666,5.166666666666666 672.5,2.5 C 677.8333333333334,-0.16666666666666652 683.1666666666666,-0.16666666666666652 688.5,2.5 C 693.8333333333334,5.166666666666666 699.1666666666666,5.166666666666666 704.5,2.5 C 709.8333333333334,-0.16666666666666652 715.1666666666666,-0.16666666666666652 720.5,2.5 C 725.8333333333334,5.166666666666666 731.1666666666666,5.166666666666666 736.5,2.5 C 741.8333333333334,-0.16666666666666652 747.1666666666666,-0.16666666666666652 752.5,2.5 C 757.8333333333334,5.166666666666666 763.1666666666666,5.166666666666666 768.5,2.5 C 773.8333333333334,-0.16666666666666652 779.1666666666666,-0.16666666666666652 784.5,2.5 C 789.8333333333334,5.166666666666666 795.1666666666666,5.166666666666666 800.5,2.5 C 805.8333333333334,-0.16666666666666652 811.1666666666666,-0.16666666666666652 816.5,2.5 C 821.8333333333334,5.166666666666666 827.1666666666666,5.166666666666666 832.5,2.5 C 837.8333333333334,-0.16666666666666652 843.1666666666666,-0.16666666666666652 848.5,2.5 C 853.8333333333334,5.166666666666666 859.1666666666666,5.166666666666666 864.5,2.5 C 869.8333333333334,-0.16666666666666652 875.1666666666666,-0.16666666666666652 880.5,2.5 C 885.8333333333334,5.166666666666666 891.1666666666666,5.166666666666666 896.5,2.5 C 901.8333333333334,-0.16666666666666652 907.1666666666666,-0.16666666666666652 912.5,2.5 C 917.8333333333334,5.166666666666666 923.1666666666666,5.166666666666666 928.5,2.5 C 933.8333333333334,-0.16666666666666652 939.1666666666666,-0.16666666666666652 944.5,2.5 C 949.8333333333334,5.166666666666666 955.1666666666666,5.166666666666666 960.5,2.5 C 965.8333333333334,-0.16666666666666652 971.1666666666666,-0.16666666666666652 976.5,2.5 C 981.8333333333334,5.166666666666666 987.1666666666666,5.166666666666666 992.5,2.5 C 994.8333333333334,-0.16666666666666652 997.1666666666666,-0.16666666666666652 999.5,2.5"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ stroke-linecap="round"
59
+ stroke-width="1"
60
+ />
61
+ </svg>
62
+ </div>
63
+ `;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @file app-bar-column.tsx
3
+ * MD3 Expressive App Bar Column DSL.
4
+ *
5
+ * Displays App Bar action items in a vertical column.
6
+ * Overflow items collapse into a dropdown menu.
7
+ *
8
+ * @see docs/m3/app-bars/AppBarColumn.kt
9
+ */
10
+
11
+ import * as React from "react";
12
+ import { cn } from "../../lib/utils";
13
+ import { AppBarTokens } from "./app-bar.tokens";
14
+ import type { AppBarColumnProps } from "./app-bar.types";
15
+ import { AppBarItemButton } from "./app-bar-item-button";
16
+ import { AppBarOverflowIndicator } from "./app-bar-overflow-indicator";
17
+
18
+ /**
19
+ * MD3 Expressive App Bar Column.
20
+ *
21
+ * Renders action items in a vertical column. Commonly used in
22
+ * side navigation or rail-style App Bars.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <AppBarColumn
27
+ * maxItemCount={3}
28
+ * items={[
29
+ * { type: 'clickable', icon: <Icon>edit</Icon>, label: 'Edit', onClick: handleEdit },
30
+ * { type: 'clickable', icon: <Icon>delete</Icon>, label: 'Delete', onClick: handleDelete },
31
+ * ]}
32
+ * />
33
+ * ```
34
+ */
35
+ export function AppBarColumn({
36
+ items,
37
+ maxItemCount,
38
+ className,
39
+ }: AppBarColumnProps) {
40
+ const containerRef = React.useRef<HTMLDivElement>(null);
41
+ const [visibleCount, setVisibleCount] = React.useState(
42
+ maxItemCount ?? items.length,
43
+ );
44
+
45
+ React.useEffect(() => {
46
+ if (maxItemCount !== undefined) {
47
+ setVisibleCount(Math.min(maxItemCount, items.length));
48
+ return;
49
+ }
50
+
51
+ const container = containerRef.current;
52
+ if (!container) return;
53
+
54
+ let debounceTimer: ReturnType<typeof setTimeout>;
55
+
56
+ const observer = new ResizeObserver((entries) => {
57
+ clearTimeout(debounceTimer);
58
+ debounceTimer = setTimeout(() => {
59
+ const entry = entries[0];
60
+ if (!entry) return;
61
+
62
+ const available = entry.contentRect.height;
63
+ const itemHeight = AppBarTokens.iconButtonTouchTarget;
64
+ const hasOverflow = items.length > Math.floor(available / itemHeight);
65
+ const reservedHeight = hasOverflow ? itemHeight : 0;
66
+ const count = Math.max(
67
+ 0,
68
+ Math.floor((available - reservedHeight) / itemHeight),
69
+ );
70
+ setVisibleCount(Math.min(count, items.length));
71
+ }, 100);
72
+ });
73
+
74
+ observer.observe(container);
75
+ return () => {
76
+ clearTimeout(debounceTimer);
77
+ observer.disconnect();
78
+ };
79
+ }, [items.length, maxItemCount]);
80
+
81
+ const visibleItems = items.slice(0, visibleCount);
82
+ const overflowItems = items.slice(visibleCount);
83
+
84
+ return (
85
+ <div
86
+ ref={containerRef}
87
+ className={cn("flex flex-col items-center", className)}
88
+ >
89
+ {visibleItems.map((item, index) => (
90
+ // biome-ignore lint/suspicious/noArrayIndexKey: static list from props
91
+ <AppBarItemButton key={index} item={item} />
92
+ ))}
93
+
94
+ {overflowItems.length > 0 && (
95
+ <AppBarOverflowIndicator items={overflowItems} />
96
+ )}
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @file app-bar-item-button.tsx
3
+ * Shared icon button renderer for AppBarRow and AppBarColumn items.
4
+ *
5
+ * Extracted from duplicate RowItem/ColumnItem implementations.
6
+ */
7
+
8
+ import { cn } from "../../lib/utils";
9
+ import { APP_BAR_COLORS, AppBarTokens } from "./app-bar.tokens";
10
+ import type { AppBarItem } from "./app-bar.types";
11
+
12
+ interface AppBarItemButtonProps {
13
+ item: AppBarItem;
14
+ }
15
+
16
+ /**
17
+ * Renders a single App Bar action item as an icon button.
18
+ * Handles clickable, toggleable, and custom item types.
19
+ */
20
+ export function AppBarItemButton({ item }: AppBarItemButtonProps) {
21
+ const isDisabled = item.enabled === false;
22
+
23
+ if (item.type === "custom" && item.appBarContent) {
24
+ return <>{item.appBarContent}</>;
25
+ }
26
+
27
+ const buttonClassName = cn(
28
+ "flex items-center justify-center rounded-full",
29
+ "focus-visible:outline-none focus-visible:ring-2",
30
+ isDisabled && "opacity-38 pointer-events-none",
31
+ );
32
+
33
+ const buttonStyle = {
34
+ width: AppBarTokens.iconButtonTouchTarget,
35
+ height: AppBarTokens.iconButtonTouchTarget,
36
+ };
37
+
38
+ if (item.type === "toggleable") {
39
+ return (
40
+ <button
41
+ type="button"
42
+ className={buttonClassName}
43
+ style={{
44
+ ...buttonStyle,
45
+ color: item.checked
46
+ ? APP_BAR_COLORS.navigationIcon
47
+ : APP_BAR_COLORS.actionIcon,
48
+ }}
49
+ aria-label={item.label}
50
+ aria-pressed={item.checked}
51
+ disabled={isDisabled}
52
+ onClick={() => item.onCheckedChange?.(!item.checked)}
53
+ >
54
+ {item.icon}
55
+ </button>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <button
61
+ type="button"
62
+ className={buttonClassName}
63
+ style={{ ...buttonStyle, color: APP_BAR_COLORS.actionIcon }}
64
+ aria-label={item.label}
65
+ disabled={isDisabled}
66
+ onClick={item.onClick}
67
+ >
68
+ {item.icon}
69
+ </button>
70
+ );
71
+ }
@@ -0,0 +1,89 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import type { AppBarItem } from "./app-bar.types";
4
+ import { AppBarColumn } from "./app-bar-column";
5
+ import { AppBarItemButton } from "./app-bar-item-button";
6
+ import { AppBarRow } from "./app-bar-row";
7
+
8
+ const mockItems: AppBarItem[] = [
9
+ {
10
+ type: "clickable",
11
+ icon: <span data-testid="icon-1">I1</span>,
12
+ label: "Action 1",
13
+ onClick: vi.fn(),
14
+ },
15
+ {
16
+ type: "toggleable",
17
+ icon: <span data-testid="icon-2">I2</span>,
18
+ label: "Action 2",
19
+ checked: false,
20
+ onCheckedChange: vi.fn(),
21
+ },
22
+ {
23
+ type: "custom",
24
+ icon: <span>I3</span>,
25
+ label: "Action 3",
26
+ appBarContent: <div data-testid="custom-item">Custom</div>,
27
+ },
28
+ ];
29
+
30
+ describe("AppBar Items", () => {
31
+ describe("AppBarItemButton", () => {
32
+ it("renders clickable item and handles click", () => {
33
+ render(<AppBarItemButton item={mockItems[0]} />);
34
+ const btn = screen.getByLabelText("Action 1");
35
+ expect(btn).toBeInTheDocument();
36
+ fireEvent.click(btn);
37
+ expect(mockItems[0].onClick).toHaveBeenCalled();
38
+ });
39
+
40
+ it("renders toggleable item and handles toggle", () => {
41
+ render(<AppBarItemButton item={mockItems[1]} />);
42
+ const btn = screen.getByLabelText("Action 2");
43
+ expect(btn).toHaveAttribute("aria-pressed", "false");
44
+ fireEvent.click(btn);
45
+ expect(mockItems[1].onCheckedChange).toHaveBeenCalledWith(true);
46
+ });
47
+
48
+ it("renders custom item content", () => {
49
+ render(<AppBarItemButton item={mockItems[2]} />);
50
+ expect(screen.getByTestId("custom-item")).toBeInTheDocument();
51
+ });
52
+ });
53
+
54
+ describe("AppBarRow", () => {
55
+ it("renders all items when maxItemCount is sufficient", () => {
56
+ render(<AppBarRow items={mockItems} />);
57
+ expect(screen.getByLabelText("Action 1")).toBeInTheDocument();
58
+ expect(screen.getByLabelText("Action 2")).toBeInTheDocument();
59
+ expect(screen.getByTestId("custom-item")).toBeInTheDocument();
60
+ });
61
+
62
+ it("shows overflow indicator when items exceed maxItemCount", () => {
63
+ render(<AppBarRow items={mockItems} maxItemCount={2} />);
64
+ expect(screen.getByLabelText("Action 1")).toBeInTheDocument();
65
+ // Action 2 vs Action 3 depending on how maxItemCount slices. Usually shows first N-1 and the indicator.
66
+ // Let's just check for the More actions indicator
67
+ expect(
68
+ screen.getByRole("button", { name: "More actions" }),
69
+ ).toBeInTheDocument();
70
+ });
71
+ });
72
+
73
+ describe("AppBarColumn", () => {
74
+ it("renders items vertically", () => {
75
+ render(<AppBarColumn items={mockItems} />);
76
+ expect(screen.getByLabelText("Action 1")).toBeInTheDocument();
77
+ expect(screen.getByLabelText("Action 2")).toBeInTheDocument();
78
+ expect(screen.getByTestId("custom-item")).toBeInTheDocument();
79
+ });
80
+
81
+ it("shows overflow indicator when items exceed maxItemCount", () => {
82
+ render(<AppBarColumn items={mockItems} maxItemCount={1} />);
83
+ expect(screen.getByLabelText("Action 1")).toBeInTheDocument(); // maybe Action 1 is replaced or first item is passed depending on n-1 rule
84
+ expect(
85
+ screen.getByRole("button", { name: "More actions" }),
86
+ ).toBeInTheDocument();
87
+ });
88
+ });
89
+ });