@bug-on/md3-react 2.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/CHANGELOG.md +69 -0
  3. package/dist/index.css +178 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6135 -0
  6. package/dist/index.d.ts +6135 -71
  7. package/dist/index.js +1688 -631
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1600 -564
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/material-symbols-cdn.css.d.ts +2 -0
  12. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  13. package/dist/plugin.d.mts +1 -0
  14. package/dist/plugin.d.ts +1 -0
  15. package/dist/plugin.js +13 -0
  16. package/dist/plugin.js.map +1 -0
  17. package/dist/plugin.mjs +3 -0
  18. package/dist/plugin.mjs.map +1 -0
  19. package/dist/typography.css.d.ts +2 -0
  20. package/package.json +28 -19
  21. package/scripts/copy-assets.js +115 -0
  22. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  23. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  24. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  25. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  26. package/src/assets/loading-indicator.svg +19 -0
  27. package/src/assets/material-symbols-cdn.css +65 -0
  28. package/src/assets/material-symbols-self-hosted.css +90 -0
  29. package/src/css.d.ts +20 -0
  30. package/src/hooks/useClickOutside.ts +37 -0
  31. package/src/hooks/useMediaQuery.ts +28 -0
  32. package/src/hooks/useRipple.ts +88 -0
  33. package/src/index.css +23 -0
  34. package/src/index.ts +349 -0
  35. package/src/lib/material-symbols-preconnect.tsx +82 -0
  36. package/src/lib/theme-utils.ts +195 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/plugin.ts +12 -0
  39. package/src/test/button.test.tsx +59 -0
  40. package/src/test/icon.test.tsx +91 -0
  41. package/src/test/loading-indicator.test.tsx +128 -0
  42. package/src/test/progress-indicator.test.tsx +306 -0
  43. package/src/test/setup.ts +80 -0
  44. package/src/test/typography.test.tsx +206 -0
  45. package/src/types/index.ts +7 -0
  46. package/src/types/md3.ts +31 -0
  47. package/src/ui/Text.tsx +60 -0
  48. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  49. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  50. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  51. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  52. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  53. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  54. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  55. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  56. package/src/ui/app-bar/app-bar.types.ts +441 -0
  57. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  58. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  59. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  60. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  61. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  62. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  63. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  64. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  65. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  66. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  67. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  68. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  69. package/src/ui/app-bar/search-view.tsx +227 -0
  70. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  71. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  72. package/src/ui/badge.test.tsx +345 -0
  73. package/src/ui/badge.tsx +282 -0
  74. package/src/ui/button-group.test.tsx +71 -0
  75. package/src/ui/button-group.tsx +350 -0
  76. package/src/ui/button.test.tsx +306 -0
  77. package/src/ui/button.tsx +665 -0
  78. package/src/ui/card.test.tsx +187 -0
  79. package/src/ui/card.tsx +259 -0
  80. package/src/ui/checkbox.test.tsx +423 -0
  81. package/src/ui/checkbox.tsx +525 -0
  82. package/src/ui/chip.test.tsx +292 -0
  83. package/src/ui/chip.tsx +548 -0
  84. package/src/ui/code-block.tsx +219 -0
  85. package/src/ui/dialog.test.tsx +300 -0
  86. package/src/ui/dialog.tsx +384 -0
  87. package/src/ui/divider.test.tsx +314 -0
  88. package/src/ui/divider.tsx +412 -0
  89. package/src/ui/drawer.tsx +240 -0
  90. package/src/ui/fab-menu.test.tsx +494 -0
  91. package/src/ui/fab-menu.tsx +739 -0
  92. package/src/ui/fab.test.tsx +232 -0
  93. package/src/ui/fab.tsx +505 -0
  94. package/src/ui/icon-button.test.tsx +515 -0
  95. package/src/ui/icon-button.tsx +525 -0
  96. package/src/ui/icon.test.tsx +197 -0
  97. package/src/ui/icon.tsx +179 -0
  98. package/src/ui/loading-indicator.test.tsx +73 -0
  99. package/src/ui/loading-indicator.tsx +312 -0
  100. package/src/ui/menu/context-menu.tsx +275 -0
  101. package/src/ui/menu/index.ts +77 -0
  102. package/src/ui/menu/menu-animations.ts +102 -0
  103. package/src/ui/menu/menu-context.tsx +99 -0
  104. package/src/ui/menu/menu-divider.tsx +47 -0
  105. package/src/ui/menu/menu-group.tsx +200 -0
  106. package/src/ui/menu/menu-item.tsx +294 -0
  107. package/src/ui/menu/menu-tokens.ts +208 -0
  108. package/src/ui/menu/menu-types.ts +313 -0
  109. package/src/ui/menu/menu.test.tsx +624 -0
  110. package/src/ui/menu/menu.tsx +289 -0
  111. package/src/ui/menu/sub-menu.tsx +223 -0
  112. package/src/ui/menu/vertical-menu.tsx +382 -0
  113. package/src/ui/navigation-rail.test.tsx +404 -0
  114. package/src/ui/navigation-rail.tsx +607 -0
  115. package/src/ui/progress-indicator/circular.tsx +248 -0
  116. package/src/ui/progress-indicator/hooks.ts +51 -0
  117. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  118. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  119. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  120. package/src/ui/progress-indicator/linear.tsx +143 -0
  121. package/src/ui/progress-indicator/types.ts +158 -0
  122. package/src/ui/progress-indicator/utils.ts +73 -0
  123. package/src/ui/radio-button.test.tsx +407 -0
  124. package/src/ui/radio-button.tsx +551 -0
  125. package/src/ui/ripple.test.tsx +72 -0
  126. package/src/ui/ripple.tsx +234 -0
  127. package/src/ui/scroll-area.test.tsx +58 -0
  128. package/src/ui/scroll-area.tsx +139 -0
  129. package/src/ui/search/animated-placeholder.tsx +145 -0
  130. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  131. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  132. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  133. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  134. package/src/ui/search/index.ts +44 -0
  135. package/src/ui/search/search-bar.tsx +220 -0
  136. package/src/ui/search/search-context.tsx +42 -0
  137. package/src/ui/search/search-view-docked.tsx +194 -0
  138. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  139. package/src/ui/search/search.test.tsx +233 -0
  140. package/src/ui/search/search.tokens.ts +134 -0
  141. package/src/ui/search/search.tsx +131 -0
  142. package/src/ui/search/search.types.ts +154 -0
  143. package/src/ui/search/trailing-action.tsx +49 -0
  144. package/src/ui/shared/constants.ts +135 -0
  145. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  146. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  147. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  148. package/src/ui/slider/range-slider.tsx +561 -0
  149. package/src/ui/slider/slider-thumb.tsx +379 -0
  150. package/src/ui/slider/slider-track.tsx +912 -0
  151. package/src/ui/slider/slider.tokens.ts +189 -0
  152. package/src/ui/slider/slider.tsx +259 -0
  153. package/src/ui/slider/slider.types.ts +288 -0
  154. package/src/ui/snackbar/index.ts +20 -0
  155. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  156. package/src/ui/snackbar/snackbar.tsx +476 -0
  157. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  158. package/src/ui/switch/switch.stories.tsx +309 -0
  159. package/src/ui/switch/switch.test.tsx +243 -0
  160. package/src/ui/switch/switch.tokens.ts +89 -0
  161. package/src/ui/switch/switch.tsx +504 -0
  162. package/src/ui/switch/switch.types.ts +62 -0
  163. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  164. package/src/ui/tabs/tab.tsx +407 -0
  165. package/src/ui/tabs/tabs-content.tsx +89 -0
  166. package/src/ui/tabs/tabs-list.tsx +146 -0
  167. package/src/ui/tabs/tabs.test.tsx +290 -0
  168. package/src/ui/tabs/tabs.tokens.ts +121 -0
  169. package/src/ui/tabs/tabs.tsx +229 -0
  170. package/src/ui/tabs/tabs.types.ts +185 -0
  171. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  172. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  173. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  174. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  175. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  176. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  177. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  178. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  179. package/src/ui/text-field/text-field.test.tsx +454 -0
  180. package/src/ui/text-field/text-field.tokens.ts +104 -0
  181. package/src/ui/text-field/text-field.tsx +548 -0
  182. package/src/ui/text-field/text-field.types.ts +180 -0
  183. package/src/ui/theme-provider/index.tsx +215 -0
  184. package/src/ui/toc.test.tsx +108 -0
  185. package/src/ui/toc.tsx +172 -0
  186. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  187. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  188. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  189. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  190. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  191. package/src/ui/tooltip/tooltip.types.ts +70 -0
  192. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  193. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  194. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  195. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  196. package/src/ui/typography/type-scale-tokens.ts +205 -0
  197. package/src/ui/typography/typography-key-tokens.ts +43 -0
  198. package/src/ui/typography/typography-tokens.ts +360 -0
  199. package/src/ui/typography/typography.css +22 -0
  200. package/src/ui/typography/typography.tsx +559 -0
  201. package/test-render.tsx +4 -0
  202. package/test-shadow.html +26 -0
  203. package/test_output.txt +164 -0
  204. package/test_output_v2.txt +5 -0
  205. package/tsconfig.build.json +10 -0
  206. package/tsconfig.json +18 -0
  207. package/tsup.config.ts +20 -0
  208. package/vitest.config.ts +11 -0
  209. package/dist/hooks/useClickOutside.d.ts +0 -8
  210. package/dist/hooks/useMediaQuery.d.ts +0 -11
  211. package/dist/hooks/useRipple.d.ts +0 -26
  212. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  213. package/dist/lib/theme-utils.d.ts +0 -63
  214. package/dist/lib/utils.d.ts +0 -2
  215. package/dist/types/index.d.ts +0 -1
  216. package/dist/types/md3.d.ts +0 -14
  217. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  218. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  219. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  220. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  221. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  222. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  223. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  224. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  225. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  226. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  227. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  228. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  229. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  230. package/dist/ui/app-bar/search-view.d.ts +0 -54
  231. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  232. package/dist/ui/badge.d.ts +0 -125
  233. package/dist/ui/button-group.d.ts +0 -59
  234. package/dist/ui/button.d.ts +0 -148
  235. package/dist/ui/card.d.ts +0 -62
  236. package/dist/ui/checkbox.d.ts +0 -82
  237. package/dist/ui/chip.d.ts +0 -110
  238. package/dist/ui/code-block.d.ts +0 -14
  239. package/dist/ui/dialog.d.ts +0 -111
  240. package/dist/ui/divider.d.ts +0 -164
  241. package/dist/ui/drawer.d.ts +0 -39
  242. package/dist/ui/dropdown.d.ts +0 -29
  243. package/dist/ui/fab-menu.d.ts +0 -204
  244. package/dist/ui/fab.d.ts +0 -162
  245. package/dist/ui/icon-button.d.ts +0 -131
  246. package/dist/ui/icon.d.ts +0 -88
  247. package/dist/ui/loading-indicator.d.ts +0 -42
  248. package/dist/ui/navigation-rail.d.ts +0 -29
  249. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  250. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  251. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  252. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  253. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  254. package/dist/ui/progress-indicator/types.d.ts +0 -151
  255. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  256. package/dist/ui/radio-button.d.ts +0 -106
  257. package/dist/ui/ripple.d.ts +0 -126
  258. package/dist/ui/scroll-area.d.ts +0 -27
  259. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  260. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  261. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  262. package/dist/ui/search/index.d.ts +0 -27
  263. package/dist/ui/search/search-bar.d.ts +0 -32
  264. package/dist/ui/search/search-context.d.ts +0 -24
  265. package/dist/ui/search/search-view-docked.d.ts +0 -25
  266. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  267. package/dist/ui/search/search.d.ts +0 -50
  268. package/dist/ui/search/search.tokens.d.ts +0 -112
  269. package/dist/ui/search/search.types.d.ts +0 -131
  270. package/dist/ui/search/trailing-action.d.ts +0 -9
  271. package/dist/ui/shared/constants.d.ts +0 -86
  272. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  273. package/dist/ui/slider/range-slider.d.ts +0 -47
  274. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  275. package/dist/ui/slider/slider-track.d.ts +0 -25
  276. package/dist/ui/slider/slider.d.ts +0 -60
  277. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  278. package/dist/ui/slider/slider.types.d.ts +0 -259
  279. package/dist/ui/snackbar/index.d.ts +0 -6
  280. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  281. package/dist/ui/switch/switch.d.ts +0 -30
  282. package/dist/ui/switch/switch.stories.d.ts +0 -48
  283. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  284. package/dist/ui/switch/switch.types.d.ts +0 -59
  285. package/dist/ui/tabs/tab.d.ts +0 -43
  286. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  287. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  288. package/dist/ui/tabs/tabs.d.ts +0 -60
  289. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  290. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  291. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  292. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  293. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  294. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  295. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  296. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  297. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  298. package/dist/ui/text-field/text-field.d.ts +0 -49
  299. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  300. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  301. package/dist/ui/theme-provider/index.d.ts +0 -48
  302. package/dist/ui/toc.d.ts +0 -80
  303. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  304. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  305. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  306. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  307. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  308. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  309. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  310. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  311. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  312. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  313. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  314. package/dist/ui/typography/typography.d.ts +0 -265
  315. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  316. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,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";