@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,145 @@
1
+ /**
2
+ * @file supporting-text.tsx
3
+ * Supporting text, error text, and character counter for MD3 TextField.
4
+ *
5
+ * Animates in/out using AnimatePresence. Uses aria-live for accessibility.
6
+ */
7
+
8
+ import { AnimatePresence, m } from "motion/react";
9
+ import * as React from "react";
10
+ import {
11
+ MD3_STANDARD_EASING,
12
+ MD3_SUPPORTING_DURATION,
13
+ } from "../../shared/constants";
14
+ import { TF_COLORS } from "../text-field.tokens";
15
+
16
+ export interface SupportingTextProps {
17
+ /** Helper text shown in normal state. */
18
+ supportingText?: string;
19
+ /** Error message — shown instead of supportingText when isError=true. */
20
+ errorText?: string;
21
+ /** Whether field is in error state. */
22
+ isError: boolean;
23
+ /** Current character count (value.length). */
24
+ charCount?: number;
25
+ /** Maximum character limit. Counter shown only when maxLength is set. */
26
+ maxLength?: number;
27
+ /** ID for aria-describedby linking from the input. */
28
+ id: string;
29
+ /** Disable animations. */
30
+ prefersReduced: boolean;
31
+ }
32
+
33
+ /**
34
+ * AnimatePresence wrapper for text content changes.
35
+ * Fades out old text, fades in new text.
36
+ */
37
+ function AnimatedText({
38
+ text,
39
+ motionKey,
40
+ className,
41
+ ariaLive,
42
+ duration,
43
+ }: {
44
+ text: string;
45
+ motionKey: string;
46
+ className: string;
47
+ ariaLive?: "polite" | "off";
48
+ duration: number;
49
+ }) {
50
+ return (
51
+ <m.span
52
+ key={motionKey}
53
+ className={className}
54
+ initial={{ opacity: 0, y: -4 }}
55
+ animate={{ opacity: 1, y: 0 }}
56
+ exit={{ opacity: 0, y: -4 }}
57
+ transition={{ duration, ease: MD3_STANDARD_EASING }}
58
+ aria-live={ariaLive}
59
+ >
60
+ {text}
61
+ </m.span>
62
+ );
63
+ }
64
+
65
+ /**
66
+ * MD3 Supporting Text area.
67
+ *
68
+ * Layout: [helper/error text] [character counter]
69
+ * - Error text replaces supporting text when isError=true.
70
+ * - Character counter shows only when maxLength is provided.
71
+ * - Both use aria-live="polite" to announce changes to screen readers.
72
+ * - Animates in/out with opacity + y-offset via AnimatePresence.
73
+ *
74
+ * @accessibility
75
+ * - Error text: `aria-live="polite"` — screen readers announce when error appears.
76
+ * - Counter: `aria-live="polite"` — announces count changes.
77
+ */
78
+ export const SupportingText = React.memo(function SupportingText({
79
+ supportingText,
80
+ errorText,
81
+ isError,
82
+ charCount,
83
+ maxLength,
84
+ id,
85
+ prefersReduced,
86
+ }: SupportingTextProps) {
87
+ const duration = prefersReduced ? 0 : MD3_SUPPORTING_DURATION;
88
+
89
+ const activeText = isError && errorText ? errorText : supportingText;
90
+ const isOverLimit = maxLength !== undefined && (charCount ?? 0) > maxLength;
91
+
92
+ // Determine counter color
93
+ const counterColor = isOverLimit
94
+ ? TF_COLORS.error
95
+ : TF_COLORS.onSurfaceVariant;
96
+
97
+ if (!activeText && maxLength === undefined) return null;
98
+
99
+ return (
100
+ <m.div
101
+ className="flex items-start justify-between gap-2 mt-1 px-4"
102
+ initial={{ opacity: 0, height: 0 }}
103
+ animate={{ opacity: 1, height: "auto" }}
104
+ exit={{ opacity: 0, height: 0 }}
105
+ transition={{ duration, ease: MD3_STANDARD_EASING }}
106
+ >
107
+ {/* Left: helper or error text */}
108
+ <div id={id} className="flex-1 min-w-0">
109
+ <AnimatePresence mode="wait">
110
+ {activeText && (
111
+ <AnimatedText
112
+ key={isError && errorText ? "error" : "helper"}
113
+ motionKey={
114
+ isError && errorText
115
+ ? `error-${errorText}`
116
+ : `helper-${supportingText}`
117
+ }
118
+ text={activeText}
119
+ className={`text-xs leading-4 block ${
120
+ isError ? "text-m3-error" : "text-m3-on-surface-variant"
121
+ }`}
122
+ ariaLive="polite"
123
+ duration={duration}
124
+ />
125
+ )}
126
+ </AnimatePresence>
127
+ </div>
128
+
129
+ {/* Right: character counter */}
130
+ {maxLength !== undefined && typeof charCount === "number" && (
131
+ <m.span
132
+ className="text-xs leading-4 tabular-nums shrink-0"
133
+ animate={{ color: counterColor }}
134
+ transition={{ duration, ease: MD3_STANDARD_EASING }}
135
+ aria-live="polite"
136
+ aria-atomic="true"
137
+ >
138
+ {charCount} / {maxLength}
139
+ </m.span>
140
+ )}
141
+ </m.div>
142
+ );
143
+ });
144
+
145
+ SupportingText.displayName = "SupportingText";
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @file trailing-icon.tsx
3
+ * Trailing icon slot for MD3 TextField.
4
+ *
5
+ * Supports three built-in modes:
6
+ * - 'clear': ✕ button, visible when field has value
7
+ * - 'password-toggle': eye icon, toggles password visibility
8
+ * - 'custom': renders the `children` prop
9
+ */
10
+
11
+ import { AnimatePresence, m } from "motion/react";
12
+ import * as React from "react";
13
+ import {
14
+ MD3_ICON_SWAP_DURATION,
15
+ MD3_STANDARD_EASING,
16
+ } from "../../shared/constants";
17
+ import { TF_COLORS } from "../text-field.tokens";
18
+ import type { TextFieldTrailingIconMode } from "../text-field.types";
19
+
20
+ export interface TrailingIconProps {
21
+ mode: TextFieldTrailingIconMode;
22
+ /** Custom icon content (used when mode='custom'). */
23
+ children?: React.ReactNode;
24
+ /** Current input value — used to determine if clear button is visible. */
25
+ value: string;
26
+ /** Whether password is currently visible (for password-toggle mode). */
27
+ showPassword?: boolean;
28
+ /** Fires when clear button is clicked. */
29
+ onClear?: () => void;
30
+ /** Fires when password visibility toggle is clicked. */
31
+ onPasswordToggle?: () => void;
32
+ /** Whether the field is in error state. */
33
+ isError: boolean;
34
+ /** Whether the field is disabled. */
35
+ isDisabled: boolean;
36
+ /** Disable animations. */
37
+ prefersReduced: boolean;
38
+ }
39
+
40
+ /** Animated icon wrapper with scale + opacity transition. */
41
+ function AnimatedIconSlot({
42
+ children,
43
+ motionKey,
44
+ duration,
45
+ }: {
46
+ children: React.ReactNode;
47
+ motionKey: string;
48
+ duration: number;
49
+ }) {
50
+ return (
51
+ <m.span
52
+ key={motionKey}
53
+ className="flex items-center justify-center"
54
+ initial={{ opacity: 0, scale: 0 }}
55
+ animate={{ opacity: 1, scale: 1 }}
56
+ exit={{ opacity: 0, scale: 0 }}
57
+ transition={{ duration, ease: MD3_STANDARD_EASING }}
58
+ >
59
+ {children}
60
+ </m.span>
61
+ );
62
+ }
63
+
64
+ /** Eye icon (password visible). */
65
+ function EyeOpenIcon() {
66
+ return (
67
+ <svg
68
+ width="24"
69
+ height="24"
70
+ viewBox="0 0 24 24"
71
+ fill="currentColor"
72
+ aria-hidden="true"
73
+ >
74
+ <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
75
+ </svg>
76
+ );
77
+ }
78
+
79
+ /** Eye-off icon (password hidden). */
80
+ function EyeOffIcon() {
81
+ return (
82
+ <svg
83
+ width="24"
84
+ height="24"
85
+ viewBox="0 0 24 24"
86
+ fill="currentColor"
87
+ aria-hidden="true"
88
+ >
89
+ <path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
90
+ </svg>
91
+ );
92
+ }
93
+
94
+ /** ✕ Clear icon. */
95
+ function ClearIcon() {
96
+ return (
97
+ <svg
98
+ width="20"
99
+ height="20"
100
+ viewBox="0 0 24 24"
101
+ fill="currentColor"
102
+ aria-hidden="true"
103
+ >
104
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
105
+ </svg>
106
+ );
107
+ }
108
+
109
+ /**
110
+ * MD3 Trailing Icon.
111
+ *
112
+ * Touch target: 48×48px (padding extends the hit area beyond the 24×24 icon).
113
+ *
114
+ * @accessibility
115
+ * - Clear button: `aria-label="Clear input"`
116
+ * - Password toggle: `aria-label="Show password"` / `"Hide password"`
117
+ * - Custom: no aria — consumer provides accessible markup
118
+ */
119
+ export const TrailingIcon = React.memo(function TrailingIcon({
120
+ mode,
121
+ children,
122
+ value,
123
+ showPassword = false,
124
+ onClear,
125
+ onPasswordToggle,
126
+ isError,
127
+ isDisabled,
128
+ prefersReduced,
129
+ }: TrailingIconProps) {
130
+ const duration = prefersReduced ? 0 : MD3_ICON_SWAP_DURATION;
131
+
132
+ const iconColor =
133
+ isError && !isDisabled ? TF_COLORS.error : TF_COLORS.onSurfaceVariant;
134
+
135
+ // Touch target button styles: 48×48px, centered icon
136
+ const btnClass =
137
+ "relative flex items-center justify-center w-12 h-12 -mr-1 rounded-full cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-m3-primary)] transition-colors hover:bg-[var(--color-m3-on-surface)]/8 active:bg-[var(--color-m3-on-surface)]/12 disabled:pointer-events-none";
138
+
139
+ if (mode === "clear") {
140
+ const hasValue = value.length > 0;
141
+ return (
142
+ <AnimatePresence>
143
+ {hasValue && (
144
+ <AnimatedIconSlot motionKey="clear" duration={duration}>
145
+ <button
146
+ type="button"
147
+ aria-label="Clear input"
148
+ onClick={onClear}
149
+ disabled={isDisabled}
150
+ tabIndex={isDisabled ? -1 : 0}
151
+ className={btnClass}
152
+ style={{ color: iconColor }}
153
+ >
154
+ <ClearIcon />
155
+ </button>
156
+ </AnimatedIconSlot>
157
+ )}
158
+ </AnimatePresence>
159
+ );
160
+ }
161
+
162
+ if (mode === "password-toggle") {
163
+ return (
164
+ <AnimatePresence mode="wait">
165
+ <AnimatedIconSlot
166
+ motionKey={showPassword ? "eye-off" : "eye-on"}
167
+ duration={duration}
168
+ >
169
+ <button
170
+ type="button"
171
+ aria-label={showPassword ? "Hide password" : "Show password"}
172
+ onClick={onPasswordToggle}
173
+ disabled={isDisabled}
174
+ tabIndex={isDisabled ? -1 : 0}
175
+ className={btnClass}
176
+ style={{ color: iconColor }}
177
+ >
178
+ {showPassword ? <EyeOffIcon /> : <EyeOpenIcon />}
179
+ </button>
180
+ </AnimatedIconSlot>
181
+ </AnimatePresence>
182
+ );
183
+ }
184
+
185
+ if (mode === "custom" && children) {
186
+ return (
187
+ <div
188
+ className="flex items-center justify-center w-6 h-6 mr-3"
189
+ style={{ color: iconColor }}
190
+ >
191
+ {children}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ return null;
197
+ });
198
+
199
+ TrailingIcon.displayName = "TrailingIcon";