@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,180 @@
1
+ /**
2
+ * @file text-field.types.ts
3
+ * TypeScript interfaces and types for TextField MD3 Expressive component.
4
+ * @see https://m3.material.io/components/text-fields/overview
5
+ */
6
+
7
+ import type * as React from "react";
8
+
9
+ // ─── Variant & Input Types ───────────────────────────────────────────────────
10
+
11
+ export type TextFieldVariant = "filled" | "outlined";
12
+
13
+ export type TextFieldInputType =
14
+ | "text"
15
+ | "email"
16
+ | "number"
17
+ | "password"
18
+ | "search"
19
+ | "tel"
20
+ | "url"
21
+ | "textarea";
22
+
23
+ export type TextFieldTrailingIconMode =
24
+ | "none"
25
+ | "clear"
26
+ | "password-toggle"
27
+ | "custom";
28
+
29
+ // ─── Imperative Handle ───────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Imperative handle exposed via forwardRef.
33
+ * Allows parent components to programmatically control the text field.
34
+ */
35
+ export interface TextFieldHandle {
36
+ /** Focuses the input element. */
37
+ focus(): void;
38
+ /** Blurs the input element. */
39
+ blur(): void;
40
+ /** Selects all text in the input. */
41
+ select(): void;
42
+ /** Clears the current value and fires onChange with empty string. */
43
+ clear(): void;
44
+ /** Sets a custom validation message on the native input. */
45
+ setCustomValidity(message: string): void;
46
+ /** Returns true if the input is valid. Does not show validation UI. */
47
+ checkValidity(): boolean;
48
+ /** Returns true if the input is valid. Shows validation UI if invalid. */
49
+ reportValidity(): boolean;
50
+ /** Returns the current value string. */
51
+ getValue(): string;
52
+ /** Returns the underlying input or textarea element. */
53
+ getInputElement(): HTMLInputElement | HTMLTextAreaElement | null;
54
+ }
55
+
56
+ // ─── Main Props ──────────────────────────────────────────────────────────────
57
+
58
+ export interface TextFieldProps {
59
+ // ── Core ──────────────────────────────────────────────────────────────────
60
+ /** Filled or outlined variant. @default 'filled' */
61
+ variant?: TextFieldVariant;
62
+ /** Floating label text. Also used as accessible name when no aria-label is set. */
63
+ label?: string;
64
+ /** Controlled value. Use with onChange for controlled mode. */
65
+ value?: string;
66
+ /** Initial value for uncontrolled mode. */
67
+ defaultValue?: string;
68
+ /** Fires when value changes. Receives new value string and native event. */
69
+ onChange?: (
70
+ value: string,
71
+ event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
72
+ ) => void;
73
+
74
+ // ── Input config ──────────────────────────────────────────────────────────
75
+ /** Input type. Use 'textarea' for multi-line input. @default 'text' */
76
+ type?: TextFieldInputType;
77
+ /** Placeholder text — shown only when no label OR label is floated. */
78
+ placeholder?: string;
79
+ name?: string;
80
+ id?: string;
81
+ autoComplete?: string;
82
+ inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
83
+ /** Number of rows for textarea type. @default 2 */
84
+ rows?: number;
85
+ /** Number of columns for textarea type. @default 20 */
86
+ cols?: number;
87
+ /** Whether the textarea should automatically resize to fit its content. @default false */
88
+ autoResize?: boolean;
89
+ /** Maximum number of rows when autoResize is true. */
90
+ maxRows?: number;
91
+ /** CSS direction override for input text. */
92
+ textDirection?: "ltr" | "rtl" | "";
93
+
94
+ // ── Validation ────────────────────────────────────────────────────────────
95
+ /** Marks field as required. Shows asterisk on label. */
96
+ required?: boolean;
97
+ /** Hides the asterisk even when required=true. */
98
+ noAsterisk?: boolean;
99
+ /** Manual error override — forces error visual state. */
100
+ error?: boolean;
101
+ /** Error message shown below the field (replaces supportingText). */
102
+ errorText?: string;
103
+ minLength?: number;
104
+ /** When set, enables character counter display. */
105
+ maxLength?: number;
106
+ min?: string;
107
+ max?: string;
108
+ step?: string;
109
+ pattern?: string;
110
+ /** For type="email" — allows multiple email addresses. */
111
+ multiple?: boolean;
112
+
113
+ // ── Supporting text ───────────────────────────────────────────────────────
114
+ /** Helper text shown below the field. Replaced by errorText when in error state. */
115
+ supportingText?: string;
116
+
117
+ // ── Decorators ────────────────────────────────────────────────────────────
118
+ /** Text displayed before the input value (e.g., "$"). */
119
+ prefixText?: string;
120
+ /** Text displayed after the input value (e.g., ".00"). */
121
+ suffixText?: string;
122
+ /** Icon node for the leading slot. Should be 24×24px. */
123
+ leadingIcon?: React.ReactNode;
124
+ /** Custom trailing icon node. Used when trailingIconMode='custom'. */
125
+ trailingIcon?: React.ReactNode;
126
+
127
+ // ── Trailing icon mode ────────────────────────────────────────────────────
128
+ /**
129
+ * Built-in trailing icon behavior.
130
+ * - 'none' — no trailing icon
131
+ * - 'clear' — ✕ button, clears value when clicked
132
+ * - 'password-toggle' — eye icon, toggles password visibility
133
+ * - 'custom' — uses trailingIcon prop
134
+ * @default 'none'
135
+ */
136
+ trailingIconMode?: TextFieldTrailingIconMode;
137
+
138
+ // ── States ────────────────────────────────────────────────────────────────
139
+ disabled?: boolean;
140
+ readOnly?: boolean;
141
+ /** Hides spinner arrows on type="number". */
142
+ noSpinner?: boolean;
143
+
144
+ // ── Form integration ──────────────────────────────────────────────────────
145
+ form?: string;
146
+
147
+ // ── Accessibility ─────────────────────────────────────────────────────────
148
+ "aria-label"?: string;
149
+ "aria-describedby"?: string;
150
+ "aria-labelledby"?: string;
151
+
152
+ // ── Layout ────────────────────────────────────────────────────────────────
153
+ /** Extra class applied to the root wrapper element. */
154
+ className?: string;
155
+ /** Makes the component fill its container width. */
156
+ fullWidth?: boolean;
157
+ /** Dense variant — reduced height (48px instead of 56px). */
158
+ dense?: boolean;
159
+
160
+ // ── Callbacks ─────────────────────────────────────────────────────────────
161
+ onFocus?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
162
+ onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
163
+ onKeyDown?: React.KeyboardEventHandler<
164
+ HTMLInputElement | HTMLTextAreaElement
165
+ >;
166
+ onKeyUp?: React.KeyboardEventHandler<HTMLInputElement | HTMLTextAreaElement>;
167
+
168
+ // ── Ref ───────────────────────────────────────────────────────────────────
169
+ ref?: React.Ref<TextFieldHandle>;
170
+
171
+ // ── ScrollArea ────────────────────────────────────────────────────────────
172
+ /**
173
+ * Controls when the scrollbars are visible when type="textarea".
174
+ * - `hover`: Show on hover (default)
175
+ * - `scroll`: Show only while scrolling
176
+ * - `always`: Always visible
177
+ * - `none`: Never visible
178
+ */
179
+ scrollAreaType?: "hover" | "scroll" | "always" | "none";
180
+ }
@@ -0,0 +1,190 @@
1
+ "use client";
2
+
3
+ import { domMax, LazyMotion } from "motion/react";
4
+ import {
5
+ createContext,
6
+ type ReactNode,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useState,
11
+ } from "react";
12
+ import { applyTheme, type ThemeMode } from "../../lib/theme-utils";
13
+ import {
14
+ SnackbarContext,
15
+ SnackbarHost,
16
+ useSnackbarState,
17
+ } from "../snackbar/snackbar";
18
+ import { Typography, TypographyContext } from "../typography/typography";
19
+ import {
20
+ type FontVariationAxes,
21
+ TypographyTokens,
22
+ } from "../typography/typography-tokens";
23
+
24
+ // ─── Theme Context ─────────────────────────────────────────────────────────────
25
+
26
+ interface ThemeContextValue {
27
+ sourceColor: string;
28
+ setSourceColor: (color: string) => void;
29
+ mode: ThemeMode;
30
+ setMode: (mode: ThemeMode) => void;
31
+ }
32
+
33
+ const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
34
+
35
+ const STORAGE_KEY_COLOR = "md3-source-color";
36
+ const STORAGE_KEY_MODE = "md3-theme-mode";
37
+
38
+ // ─── Singleton defaults (computed once) ───────────────────────────────────────
39
+
40
+ const defaultTokens = new TypographyTokens();
41
+ const defaultTypography = new Typography(defaultTokens);
42
+
43
+ // ─── MD3ThemeProvider ──────────────────────────────────────────────────────────
44
+
45
+ export interface MD3ThemeProviderProps {
46
+ children: ReactNode;
47
+ // ── Theme ─────────────────────────────────────────────────────────────────
48
+ sourceColor?: string;
49
+ defaultMode?: ThemeMode;
50
+ persistToLocalStorage?: boolean;
51
+ // ── Typography ────────────────────────────────────────────────────────────
52
+ /**
53
+ * A fully custom `Typography` instance.
54
+ * When provided, `fontFamily` and `fontVariationAxes` are ignored.
55
+ */
56
+ typography?: Typography;
57
+ /**
58
+ * Override the CSS `font-family` for all typography styles.
59
+ * Ignored when `typography` prop is provided.
60
+ * @example "'Inter', sans-serif"
61
+ */
62
+ fontFamily?: string;
63
+ /**
64
+ * Variable font axes applied globally via `font-variation-settings`.
65
+ * Merged on top of defaults (`ROND: 100`). Ignored when `typography` is provided.
66
+ * @example { ROND: 50 }
67
+ */
68
+ fontVariationAxes?: FontVariationAxes;
69
+ // ── Snackbar ──────────────────────────────────────────────────────────────
70
+ /**
71
+ * When `true`, mounts `SnackbarHost` inside the provider and exposes
72
+ * `useSnackbar()` to all descendants — no separate `<SnackbarProvider>` needed.
73
+ *
74
+ * Opt-in, default `false`. For advanced usage (e.g., scoped snackbars or
75
+ * custom host positioning), keep this `false` and use `<SnackbarProvider>`
76
+ * or `<SnackbarHost>` directly.
77
+ *
78
+ * @default false
79
+ */
80
+ enableSnackbar?: boolean;
81
+ }
82
+
83
+ export function MD3ThemeProvider({
84
+ children,
85
+ sourceColor: initialSourceColor = "#6750A4",
86
+ defaultMode = "light",
87
+ persistToLocalStorage = false,
88
+ typography: typographyProp,
89
+ fontFamily,
90
+ fontVariationAxes,
91
+ enableSnackbar = false,
92
+ }: MD3ThemeProviderProps) {
93
+ // ── Theme state ──────────────────────────────────────────────────────────
94
+ const [sourceColor, setSourceColor] = useState(initialSourceColor);
95
+ const [mode, setMode] = useState<ThemeMode>(defaultMode);
96
+ const [isHydrated, setIsHydrated] = useState(!persistToLocalStorage);
97
+
98
+ useEffect(() => {
99
+ if (!persistToLocalStorage) return;
100
+
101
+ const savedColor = localStorage.getItem(STORAGE_KEY_COLOR);
102
+ const savedMode = localStorage.getItem(
103
+ STORAGE_KEY_MODE,
104
+ ) as ThemeMode | null;
105
+
106
+ if (savedColor) setSourceColor(savedColor);
107
+ if (savedMode === "light" || savedMode === "dark") setMode(savedMode);
108
+
109
+ setIsHydrated(true);
110
+ }, [persistToLocalStorage]);
111
+
112
+ useEffect(() => {
113
+ if (!isHydrated) return;
114
+
115
+ applyTheme(sourceColor, mode);
116
+
117
+ if (persistToLocalStorage) {
118
+ localStorage.setItem(STORAGE_KEY_COLOR, sourceColor);
119
+ localStorage.setItem(STORAGE_KEY_MODE, mode);
120
+ }
121
+ }, [sourceColor, mode, persistToLocalStorage, isHydrated]);
122
+
123
+ const themeValue = useMemo<ThemeContextValue>(
124
+ () => ({ sourceColor, setSourceColor, mode, setMode }),
125
+ [sourceColor, mode],
126
+ );
127
+
128
+ // ── Typography value ─────────────────────────────────────────────────────
129
+ const typographyValue = useMemo<Typography>(() => {
130
+ if (typographyProp) return typographyProp;
131
+ if (fontFamily ?? fontVariationAxes) {
132
+ return new Typography(
133
+ new TypographyTokens({ fontFamily, fontVariationAxes }),
134
+ );
135
+ }
136
+ return defaultTypography;
137
+ }, [typographyProp, fontFamily, fontVariationAxes]);
138
+
139
+ // ── Snackbar — mounted as isolated subtree so hook only allocates when needed
140
+
141
+ return (
142
+ <LazyMotion features={domMax}>
143
+ <ThemeContext.Provider value={themeValue}>
144
+ <TypographyContext.Provider value={typographyValue}>
145
+ {enableSnackbar ? (
146
+ <SnackbarMountedProvider>{children}</SnackbarMountedProvider>
147
+ ) : (
148
+ children
149
+ )}
150
+ </TypographyContext.Provider>
151
+ </ThemeContext.Provider>
152
+ </LazyMotion>
153
+ );
154
+ }
155
+
156
+ // ─── SnackbarMountedProvider ──────────────────────────────────────────────────
157
+
158
+ /**
159
+ * Internal helper component — renders only when `enableSnackbar={true}`.
160
+ * Isolates `useSnackbarState` so the hook is never allocated unnecessarily.
161
+ */
162
+ function SnackbarMountedProvider({ children }: { children: ReactNode }) {
163
+ const state = useSnackbarState();
164
+ const contextValue = useMemo(
165
+ () => ({ showSnackbar: state.showSnackbar }),
166
+ [state.showSnackbar],
167
+ );
168
+
169
+ return (
170
+ <SnackbarContext.Provider value={contextValue}>
171
+ {children}
172
+ <SnackbarHost state={state} />
173
+ </SnackbarContext.Provider>
174
+ );
175
+ }
176
+
177
+ // ─── Hooks ────────────────────────────────────────────────────────────────────
178
+
179
+ export function useTheme(): ThemeContextValue {
180
+ const context = useContext(ThemeContext);
181
+ if (!context) {
182
+ throw new Error("useTheme must be used within <MD3ThemeProvider>.");
183
+ }
184
+ return context;
185
+ }
186
+
187
+ export function useThemeMode(): Pick<ThemeContextValue, "mode" | "setMode"> {
188
+ const { mode, setMode } = useTheme();
189
+ return { mode, setMode };
190
+ }
@@ -0,0 +1,108 @@
1
+ import {
2
+ act,
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ waitFor,
7
+ } from "@testing-library/react";
8
+ import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
9
+
10
+ // Mock implementation
11
+ let mockObserve: Mock<(element: Element) => void>;
12
+ let mockDisconnect: Mock<() => void>;
13
+ let observerCallback: IntersectionObserverCallback | undefined;
14
+
15
+ class MockObserver implements IntersectionObserver {
16
+ readonly root: Element | null = null;
17
+ readonly rootMargin: string = "";
18
+ readonly thresholds: ReadonlyArray<number> = [];
19
+
20
+ constructor(callback: IntersectionObserverCallback) {
21
+ observerCallback = callback;
22
+ }
23
+
24
+ observe = (element: Element) => mockObserve(element);
25
+ disconnect = () => mockDisconnect();
26
+ unobserve = vi.fn();
27
+ takeRecords = vi.fn(() => []);
28
+ }
29
+
30
+ vi.stubGlobal("IntersectionObserver", MockObserver);
31
+
32
+ import { TableOfContents } from "./toc";
33
+
34
+ const mockItems = [
35
+ { id: "section-1", label: "Section 1" },
36
+ { id: "section-2", label: "Section 2" },
37
+ ];
38
+
39
+ describe("TableOfContents", () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ mockObserve = vi.fn();
43
+ mockDisconnect = vi.fn();
44
+ window.HTMLElement.prototype.scrollIntoView = vi.fn();
45
+ });
46
+
47
+ it("renders correctly", () => {
48
+ render(<TableOfContents items={mockItems} />);
49
+ expect(screen.getByText("On this page")).toBeInTheDocument();
50
+ expect(screen.getByText("Section 1")).toBeInTheDocument();
51
+ });
52
+
53
+ it("handles active state change", async () => {
54
+ render(
55
+ <>
56
+ <div id="section-1">Content 1</div>
57
+ <div id="section-2">Content 2</div>
58
+ <TableOfContents items={mockItems} />
59
+ </>,
60
+ );
61
+
62
+ const firstLink = screen.getByText("Section 1");
63
+
64
+ // Trigger observer inside act to handle state update
65
+ if (observerCallback) {
66
+ await act(async () => {
67
+ const mockEntry = {
68
+ isIntersecting: true,
69
+ target: { id: "section-1" } as unknown as Element,
70
+ time: Date.now(),
71
+ intersectionRatio: 1,
72
+ boundingClientRect: {} as DOMRectReadOnly,
73
+ intersectionRect: {} as DOMRectReadOnly,
74
+ rootBounds: null,
75
+ } as IntersectionObserverEntry;
76
+
77
+ observerCallback?.([mockEntry], {} as IntersectionObserver);
78
+ });
79
+ }
80
+
81
+ await waitFor(() => {
82
+ expect(firstLink).toHaveClass("text-m3-primary");
83
+ expect(firstLink).toHaveClass("font-bold");
84
+ });
85
+ });
86
+
87
+ it("handles click navigation", () => {
88
+ render(
89
+ <>
90
+ <div id="section-1">Content 1</div>
91
+ <TableOfContents items={mockItems} />
92
+ </>,
93
+ );
94
+
95
+ const firstLink = screen.getByText("Section 1");
96
+ const target = document.getElementById("section-1");
97
+
98
+ if (target) {
99
+ const scrollSpy = vi.fn();
100
+ target.scrollIntoView = scrollSpy;
101
+ fireEvent.click(firstLink);
102
+ expect(scrollSpy).toHaveBeenCalledWith({
103
+ behavior: "smooth",
104
+ block: "start",
105
+ });
106
+ }
107
+ });
108
+ });
package/src/ui/toc.tsx ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @file toc.tsx
3
+ *
4
+ * Table of Contents component for long-form documentation pages.
5
+ *
6
+ * Uses `IntersectionObserver` to track which section heading is currently in the
7
+ * viewport and highlights the corresponding link. Smooth-scrolls to the target
8
+ * when a link is clicked (respecting the browser's `prefers-reduced-motion`
9
+ * media query via the native `scrollIntoView` API).
10
+ *
11
+ * @remarks
12
+ * - SSR-safe: `IntersectionObserver` usage is guarded by a `typeof` check.
13
+ * - The observer `rootMargin` is tuned for documentation layout with a fixed header
14
+ * (~100px) and early deactivation (~80% from bottom) so the active item changes
15
+ * before the section scrolls off-screen.
16
+ */
17
+
18
+ import type React from "react";
19
+ import { useCallback, useEffect, useMemo, useState } from "react";
20
+ import { cn } from "../lib/utils";
21
+ import { ScrollArea, type ScrollAreaProps } from "./scroll-area";
22
+
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Types
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * A single entry in the Table of Contents.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const items: ToCItem[] = [
33
+ * { id: "installation", label: "Installation" },
34
+ * { id: "usage", label: "Usage" },
35
+ * ];
36
+ * ```
37
+ */
38
+ export interface ToCItem {
39
+ /** The DOM `id` attribute of the corresponding section heading. */
40
+ id: string;
41
+ /** Human-readable label shown in the ToC. */
42
+ label: string;
43
+ }
44
+
45
+ /**
46
+ * Props for the `TableOfContents` component.
47
+ */
48
+ export interface TableOfContentsProps {
49
+ /**
50
+ * Ordered list of section items to display.
51
+ * Each item must have a matching DOM element with the same `id`.
52
+ */
53
+ items: ToCItem[];
54
+ /**
55
+ * Additional CSS classes applied to the root `<nav>` element.
56
+ * Use this to control positioning (e.g. sticky, fixed) from the consumer.
57
+ */
58
+ className?: string;
59
+ /**
60
+ * Configuration for the internal ScrollArea.
61
+ * @default { type: "hover" }
62
+ */
63
+ scrollAreaProps?: Omit<ScrollAreaProps, "children">;
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // Component
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Table of Contents sidebar component.
72
+ *
73
+ * Renders a `<nav>` sidebar with links to page sections. Tracks the active
74
+ * section using `IntersectionObserver` and applies active styles to the
75
+ * current link.
76
+ *
77
+ * @remarks
78
+ * - Visible only on `xl` screens (`hidden xl:block` — sticky sidebar).
79
+ * - The `aria-current` attribute is set on the active link for screen readers.
80
+ * - Click scroll is smooth: `scrollIntoView({ behavior: "smooth" })`.
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * const toc: ToCItem[] = [
85
+ * { id: "overview", label: "Overview" },
86
+ * { id: "props", label: "Props" },
87
+ * { id: "examples", label: "Examples" },
88
+ * ];
89
+ *
90
+ * <TableOfContents items={toc} />
91
+ * ```
92
+ *
93
+ * @see https://m3.material.io/foundations/content-design/navigation
94
+ */
95
+ export function TableOfContents({
96
+ items,
97
+ className,
98
+ scrollAreaProps,
99
+ }: TableOfContentsProps) {
100
+ const [activeId, setActiveId] = useState("");
101
+
102
+ // Stabilize dependency — re-subscribe only when the item IDs actually change.
103
+ const itemIds = useMemo(() => items.map((i) => i.id), [items]);
104
+
105
+ useEffect(() => {
106
+ // SSR guard: IntersectionObserver is not available in Node.js.
107
+ if (typeof IntersectionObserver === "undefined") return;
108
+
109
+ const observer = new IntersectionObserver(
110
+ (entries) => {
111
+ for (const entry of entries) {
112
+ if (entry.isIntersecting) setActiveId(entry.target.id);
113
+ }
114
+ },
115
+ // rootMargin: top offset ~100px (fixed header), bottom -80% (early switch)
116
+ { rootMargin: "-100px 0% -80% 0%" },
117
+ );
118
+
119
+ for (const id of itemIds) {
120
+ const el = document.getElementById(id);
121
+ if (el) observer.observe(el);
122
+ }
123
+
124
+ return () => observer.disconnect();
125
+ }, [itemIds]);
126
+
127
+ const handleClick = useCallback(
128
+ (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
129
+ e.preventDefault();
130
+ document
131
+ .getElementById(id)
132
+ ?.scrollIntoView({ behavior: "smooth", block: "start" });
133
+ },
134
+ [],
135
+ );
136
+
137
+ return (
138
+ <nav
139
+ aria-label="On this page"
140
+ className={cn("pl-6 flex flex-col h-full", className)}
141
+ >
142
+ <h4 className="text-xs font-bold text-m3-on-surface-variant uppercase tracking-widest mb-4 sm:hidden lg:block">
143
+ On this page
144
+ </h4>
145
+ <ScrollArea
146
+ type="hover"
147
+ {...scrollAreaProps}
148
+ className={cn("flex-1 min-h-0", scrollAreaProps?.className)}
149
+ >
150
+ <ul className="space-y-4 pr-4">
151
+ {items.map((item) => (
152
+ <li key={item.id}>
153
+ <a
154
+ href={`#${item.id}`}
155
+ onClick={(e) => handleClick(e, item.id)}
156
+ aria-current={activeId === item.id ? "true" : undefined}
157
+ className={cn(
158
+ "text-sm transition-colors hover:text-m3-primary block",
159
+ activeId === item.id
160
+ ? "text-m3-primary font-bold"
161
+ : "text-m3-on-surface-variant font-medium",
162
+ )}
163
+ >
164
+ {item.label}
165
+ </a>
166
+ </li>
167
+ ))}
168
+ </ul>
169
+ </ScrollArea>
170
+ </nav>
171
+ );
172
+ }