@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,185 @@
1
+ /**
2
+ * @file tabs.types.ts
3
+ * MD3 Expressive Tabs — TypeScript prop definitions.
4
+ * Spec: https://m3.material.io/components/tabs/overview
5
+ */
6
+
7
+ import type * as React from "react";
8
+
9
+ // ─── Variant & Layout ─────────────────────────────────────────────────────────
10
+
11
+ /** Visual variant: primary (content-width indicator) or secondary (full-width indicator + divider). */
12
+ export type TabsVariant = "primary" | "secondary";
13
+
14
+ // ─── Internal Context ──────────────────────────────────────────────────────────
15
+
16
+ /**
17
+ * Internal context shared across all compound components.
18
+ * @internal
19
+ */
20
+ export interface TabsContextValue {
21
+ /** Currently selected tab value. */
22
+ value: string;
23
+ /** Callback to change the selected tab. */
24
+ onValueChange: (value: string) => void;
25
+ /** Currently keyboard-focused tab value (for roving tabindex). */
26
+ focusedValue: string;
27
+ /** Sets the focused tab value (keyboard nav only — does NOT select). */
28
+ setFocusedValue: (value: string) => void;
29
+ /** Ordered list of all registered tab values (for ArrowKey nav). */
30
+ tabValues: string[];
31
+ /** Register a tab value when a <Tab> mounts. */
32
+ registerTab: (value: string) => void;
33
+ /** Unregister a tab value when a <Tab> unmounts. */
34
+ unregisterTab: (value: string) => void;
35
+ /** Unique layout group ID scoped to this Tabs instance. */
36
+ layoutGroupId: string;
37
+ /**
38
+ * Set of currently disabled tab values.
39
+ * Used by keyboard navigation to skip disabled tabs.
40
+ */
41
+ disabledValues: Set<string>;
42
+ /**
43
+ * Mark or unmark a tab value as disabled.
44
+ * Called by <Tab> on mount and when `disabled` prop changes.
45
+ */
46
+ markTabDisabled: (value: string, disabled: boolean) => void;
47
+ /**
48
+ * When true, focus moving via ArrowKey also selects the tab immediately.
49
+ * Mirrors Google's `autoActivate` attribute on <md-tabs>.
50
+ * @default false
51
+ */
52
+ autoActivate: boolean;
53
+ }
54
+
55
+ // ─── Component Props ───────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Props for the `<Tabs>` root component.
59
+ *
60
+ * Supports both controlled (`value` + `onValueChange`) and
61
+ * uncontrolled (`defaultValue`) usage patterns.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * // Controlled
66
+ * <Tabs value={tab} onValueChange={setTab}>...</Tabs>
67
+ *
68
+ * // Uncontrolled
69
+ * <Tabs defaultValue="flights">...</Tabs>
70
+ *
71
+ * // Auto-activate (focus = select)
72
+ * <Tabs defaultValue="flights" autoActivate>...</Tabs>
73
+ * ```
74
+ */
75
+ export interface TabsProps {
76
+ /** Controlled selected value. Use with `onValueChange`. */
77
+ value?: string;
78
+ /** Initial value for uncontrolled usage. */
79
+ defaultValue?: string;
80
+ /** Called when the selected tab changes. */
81
+ onValueChange?: (value: string) => void;
82
+ /**
83
+ * When true, ArrowKey navigation also selects the focused tab immediately.
84
+ * Mirrors Google's `auto-activate` attribute on `<md-tabs>`.
85
+ * @default false
86
+ */
87
+ autoActivate?: boolean;
88
+ /** Tab compound components as children. */
89
+ children: React.ReactNode;
90
+ /** Additional CSS class names for the root wrapper. */
91
+ className?: string;
92
+ }
93
+
94
+ /**
95
+ * Props for the `<TabsList>` container component.
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * <TabsList variant="primary" scrollable={false}>
100
+ * <Tab value="tab1">Tab 1</Tab>
101
+ * </TabsList>
102
+ * ```
103
+ */
104
+ export interface TabsListProps {
105
+ /** Visual style variant. @required */
106
+ variant: TabsVariant;
107
+ /**
108
+ * When true, tabs scroll horizontally with 52px edge padding (MD3 spec).
109
+ * When false, tabs divide the available width equally (flex-1).
110
+ * @default false
111
+ */
112
+ scrollable?: boolean;
113
+ /**
114
+ * Background color override for the tab bar.
115
+ * @default "var(--md-sys-color-surface)"
116
+ */
117
+ backgroundColor?: string;
118
+ /** Tab components as children. */
119
+ children: React.ReactNode;
120
+ /** Additional CSS class names. */
121
+ className?: string;
122
+ /** Forwarded aria-label for the tablist. */
123
+ "aria-label"?: string;
124
+ }
125
+
126
+ /**
127
+ * Props for an individual `<Tab>` component.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * <Tab value="flights" icon={<Icon name="flight" />}>Flights</Tab>
132
+ * <Tab value="trips" disabled>Trips</Tab>
133
+ * <Tab value="hotels" icon={<Icon name="hotel" />} inlineIcon>Hotels</Tab>
134
+ * ```
135
+ */
136
+ export interface TabProps {
137
+ /** Unique value identifying this tab. Must match a `<TabsContent value>`. */
138
+ value: string;
139
+ /**
140
+ * Optional icon rendered with the label text.
141
+ * - Default (stacked): icon above label, height increases to 64dp.
142
+ * - With `inlineIcon`: icon beside label (same row), height stays 48dp.
143
+ */
144
+ icon?: React.ReactNode;
145
+ /**
146
+ * When true, icon is placed inline (same row) with the label text.
147
+ * Container height stays at 48dp (does NOT increase to 64dp).
148
+ * Mirrors the `inline-icon` attribute on `<md-primary-tab>`.
149
+ * @default false
150
+ */
151
+ inlineIcon?: boolean;
152
+ /**
153
+ * When true, disables interaction.
154
+ * Disabled tabs are skipped entirely in keyboard navigation (ArrowKey).
155
+ */
156
+ disabled?: boolean;
157
+ /** Additional CSS class names. */
158
+ className?: string;
159
+ /**
160
+ * Optional badge element overlaid on the tab content.
161
+ * Handled via `BadgedBox`:
162
+ * - Stacked icon: Overlaps icon's top-trailing corner.
163
+ * - Inline/Text-only: Placed next to the text.
164
+ */
165
+ badge?: React.ReactNode;
166
+ /** Label text rendered inside the tab. */
167
+ children: React.ReactNode;
168
+ }
169
+
170
+ /**
171
+ * Props for the `<TabsContent>` panel component.
172
+ *
173
+ * @example
174
+ * ```tsx
175
+ * <TabsContent value="flights">Flight content here</TabsContent>
176
+ * ```
177
+ */
178
+ export interface TabsContentProps {
179
+ /** Must match the `value` of a sibling `<Tab>`. */
180
+ value: string;
181
+ /** Additional CSS class names. */
182
+ className?: string;
183
+ /** Panel content. */
184
+ children: React.ReactNode;
185
+ }
@@ -7,5 +7,12 @@
7
7
  * import { TextField } from '@bug-on/md3-react';
8
8
  * ```
9
9
  */
10
+
10
11
  export { TextField } from "./text-field";
11
- export type { TextFieldHandle, TextFieldInputType, TextFieldProps, TextFieldTrailingIconMode, TextFieldVariant, } from "./text-field.types";
12
+ export type {
13
+ TextFieldHandle,
14
+ TextFieldInputType,
15
+ TextFieldProps,
16
+ TextFieldTrailingIconMode,
17
+ TextFieldVariant,
18
+ } from "./text-field.types";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file active-indicator.tsx
3
+ * Animated bottom border line for the MD3 Filled TextField.
4
+ * Expands height from 1px → 2px and color changes on focus.
5
+ */
6
+
7
+ import { m } from "motion/react";
8
+ import * as React from "react";
9
+ import {
10
+ MD3_INDICATOR_DURATION,
11
+ MD3_STANDARD_EASING,
12
+ } from "../../shared/constants";
13
+ import { TF_COLORS, TF_SIZE } from "../text-field.tokens";
14
+
15
+ export interface ActiveIndicatorProps {
16
+ isFocused: boolean;
17
+ isError: boolean;
18
+ isDisabled: boolean;
19
+ isHovered: boolean;
20
+ prefersReduced: boolean;
21
+ }
22
+
23
+ /**
24
+ * MD3 Active Indicator — the bottom border line for Filled TextField.
25
+ *
26
+ * Animates:
27
+ * - `height`: 1px (enabled) → 2px (focused)
28
+ * - `backgroundColor`: on-surface-variant → primary (focused) → error
29
+ * - `scaleX`: 0 → 1 expanding from center on focus in
30
+ *
31
+ * @see https://m3.material.io/components/text-fields/specs#filled-text-field
32
+ */
33
+ export const ActiveIndicator = React.memo(function ActiveIndicator({
34
+ isFocused,
35
+ isError,
36
+ isDisabled,
37
+ isHovered,
38
+ prefersReduced,
39
+ }: ActiveIndicatorProps) {
40
+ const duration = prefersReduced ? 0 : MD3_INDICATOR_DURATION;
41
+ const ease = MD3_STANDARD_EASING;
42
+
43
+ const height =
44
+ isFocused || isError ? TF_SIZE.indicatorThick : TF_SIZE.indicatorThin;
45
+
46
+ let backgroundColor: string;
47
+ if (isError) {
48
+ backgroundColor = TF_COLORS.error;
49
+ } else if (isFocused) {
50
+ backgroundColor = TF_COLORS.primary;
51
+ } else if (isHovered && !isDisabled) {
52
+ backgroundColor = TF_COLORS.inputText;
53
+ } else {
54
+ backgroundColor = TF_COLORS.onSurfaceVariant;
55
+ }
56
+
57
+ return (
58
+ <m.div
59
+ aria-hidden="true"
60
+ className="absolute bottom-0 left-0 right-0 origin-center"
61
+ animate={{ height, backgroundColor }}
62
+ transition={{ duration, ease }}
63
+ />
64
+ );
65
+ });
66
+
67
+ ActiveIndicator.displayName = "ActiveIndicator";
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @file floating-label.tsx
3
+ * Animated floating label for MD3 TextField.
4
+ * Animates between inline (body large) and floated (body small) positions.
5
+ */
6
+
7
+ import { AnimatePresence, m } from "motion/react";
8
+ import * as React from "react";
9
+ import { cn } from "../../../lib/utils";
10
+ import {
11
+ MD3_LABEL_FLOAT_DURATION,
12
+ MD3_STANDARD_EASING,
13
+ } from "../../shared/constants";
14
+ import { TF_COLORS, TF_TYPOGRAPHY } from "../text-field.tokens";
15
+
16
+ export interface FloatingLabelProps {
17
+ /** The label text content. */
18
+ text: string;
19
+ /** Whether the label is in the floated (small) position. */
20
+ isFloated: boolean;
21
+ /** Whether the field is currently focused. */
22
+ isFocused: boolean;
23
+ /** Whether the field is in error state. */
24
+ isError: boolean;
25
+ /** Whether the field is disabled. */
26
+ isDisabled: boolean;
27
+ /** 'filled' or 'outlined' — determines vertical y offset. */
28
+ variant: "filled" | "outlined";
29
+ /** Container height in px (56 normal, 48 dense). */
30
+ containerHeight: number;
31
+ /** Whether to skip animations (prefers-reduced-motion). */
32
+ prefersReduced: boolean;
33
+ /** Whether the required asterisk should be shown. */
34
+ showAsterisk: boolean;
35
+ /** ID of the label element, for associating with a containing element. */
36
+ htmlFor?: string;
37
+ /** Ref callback so the parent can measure label width for the outlined notch. */
38
+ labelRef?: React.Ref<HTMLSpanElement>;
39
+ /** Whether there is a leading icon. */
40
+ hasLeading?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Calculates the y-translation (in px) for the floated label position.
45
+ *
46
+ * For FILLED: move up from vertical center to the top inline padding area.
47
+ * For OUTLINED: move up so the label sits on the top border line.
48
+ *
49
+ * The label starts at y=0 (vertically centered by flex parent).
50
+ * When floated, it moves upward by `offset` pixels.
51
+ */
52
+ function getFloatedY(
53
+ variant: "filled" | "outlined",
54
+ containerHeight: number,
55
+ ): number {
56
+ // Label body-large line height ≈ 24px (16px * 1.5)
57
+ // Label body-small line height ≈ 16px (12px * 1.33)
58
+ // Floated container top-padding: 8px from top of container
59
+ const labelSmallHeight = 16;
60
+ const paddingTop = 8;
61
+
62
+ if (variant === "filled") {
63
+ // Center of container → top-padding area
64
+ // Center is at containerHeight/2, floated center is at paddingTop + labelSmallHeight/2
65
+ const floatedCenter = paddingTop + labelSmallHeight / 2;
66
+ const unfloatedCenter = containerHeight / 2;
67
+ return -(unfloatedCenter - floatedCenter);
68
+ }
69
+ // Outlined: label sits on the border line (y = 0 relative to container top)
70
+ // We move up by half the container height to reach the border
71
+ return -(containerHeight / 2);
72
+ }
73
+
74
+ /**
75
+ * Returns the label color based on current state.
76
+ */
77
+ function getLabelColor(
78
+ isFloated: boolean,
79
+ isFocused: boolean,
80
+ isError: boolean,
81
+ isDisabled: boolean,
82
+ ): string {
83
+ if (isDisabled) return TF_COLORS.onSurfaceVariant;
84
+ if (isError) return TF_COLORS.error;
85
+ if (isFloated && isFocused) return TF_COLORS.primary;
86
+ return TF_COLORS.onSurfaceVariant;
87
+ }
88
+
89
+ /**
90
+ * MD3 Expressive Floating Label.
91
+ *
92
+ * Animates y-position, scale, and color when the label floats.
93
+ * Uses `transformOrigin: 'left center'` so scaling anchors at the start.
94
+ *
95
+ * @accessibility
96
+ * Rendered as a `<label>` with `htmlFor` linking to the `<input>`.
97
+ * When floated, visual size changes but the semantic label is unchanged.
98
+ */
99
+ export const FloatingLabel = React.memo(function FloatingLabel({
100
+ text,
101
+ isFloated,
102
+ isFocused,
103
+ isError,
104
+ isDisabled,
105
+ variant,
106
+ containerHeight,
107
+ prefersReduced,
108
+ showAsterisk,
109
+ htmlFor,
110
+ labelRef,
111
+ hasLeading = false,
112
+ }: FloatingLabelProps) {
113
+ const duration = prefersReduced ? 0 : MD3_LABEL_FLOAT_DURATION;
114
+ const ease = MD3_STANDARD_EASING;
115
+
116
+ const y = isFloated ? getFloatedY(variant, containerHeight) : 0;
117
+ const x = variant === "outlined" && isFloated && hasLeading ? -36 : 0;
118
+ const scale = isFloated ? TF_TYPOGRAPHY.labelScaleRatio : 1;
119
+ const color = getLabelColor(isFloated, isFocused, isError, isDisabled);
120
+
121
+ return (
122
+ <m.label
123
+ htmlFor={htmlFor}
124
+ className={cn(
125
+ "absolute pointer-events-none select-none origin-[left_center] leading-6 text-base whitespace-nowrap",
126
+ "px-1 -mx-1", // Clear the notch gap and keep text aligned
127
+ variant === "outlined" && isFloated && "bg-m3-surface",
128
+ "left-4",
129
+ isDisabled && "opacity-[0.38]",
130
+ )}
131
+ animate={{ y, x, scale, color }}
132
+ transition={{ duration, ease }}
133
+ style={{
134
+ top: (containerHeight - 24) / 2, // 24px is the resting line-height
135
+ transformOrigin: "left center",
136
+ zIndex: 1,
137
+ }}
138
+ >
139
+ <span ref={labelRef} className="inline-block relative">
140
+ {text}
141
+ <AnimatePresence>
142
+ {showAsterisk && (
143
+ <m.span
144
+ key="asterisk"
145
+ aria-hidden="true"
146
+ initial={{ opacity: 0 }}
147
+ animate={{ opacity: 1 }}
148
+ exit={{ opacity: 0 }}
149
+ transition={{ duration: prefersReduced ? 0 : 0.1 }}
150
+ className="ml-0.5 text-m3-error"
151
+ >
152
+ *
153
+ </m.span>
154
+ )}
155
+ </AnimatePresence>
156
+ </span>
157
+ </m.label>
158
+ );
159
+ });
160
+
161
+ FloatingLabel.displayName = "FloatingLabel";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @file leading-icon.tsx
3
+ * Leading icon slot for MD3 TextField.
4
+ */
5
+
6
+ import * as React from "react";
7
+ import { TF_COLORS } from "../text-field.tokens";
8
+
9
+ export interface LeadingIconProps {
10
+ /** Icon node — should be 24×24px. */
11
+ children: React.ReactNode;
12
+ /** Whether the field is in error state (changes icon color). */
13
+ isError: boolean;
14
+ /** Whether the field is disabled. */
15
+ isDisabled: boolean;
16
+ }
17
+
18
+ /**
19
+ * MD3 Leading Icon wrapper.
20
+ *
21
+ * Decorative — `aria-hidden="true"`.
22
+ * Color: `on-surface-variant` (default), `error` (error state).
23
+ * Size: 24×24px icon, 48×56px touch target via flex alignment.
24
+ *
25
+ * @see https://m3.material.io/components/text-fields/specs#anatomy
26
+ */
27
+ export const LeadingIcon = React.memo(function LeadingIcon({
28
+ children,
29
+ isError,
30
+ isDisabled,
31
+ }: LeadingIconProps) {
32
+ const color =
33
+ isError && !isDisabled ? TF_COLORS.error : TF_COLORS.onSurfaceVariant;
34
+
35
+ return (
36
+ <div
37
+ aria-hidden="true"
38
+ className="flex items-center justify-center shrink-0 w-6 h-6 ml-3"
39
+ style={{ color }}
40
+ >
41
+ {children}
42
+ </div>
43
+ );
44
+ });
45
+
46
+ LeadingIcon.displayName = "LeadingIcon";
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @file outline-container.tsx
3
+ * MD3-compliant outlined border with animated notch for the Outlined TextField.
4
+ *
5
+ * Implementation: 3-segment approach inspired by Material Web's fieldset/legend pattern.
6
+ * The top border is split into: [left-segment] [notch-gap] [right-segment].
7
+ * The notch-gap width animates from 0 → (labelWidth × scaleRatio + 8px) when label floats.
8
+ *
9
+ * This mirrors Material Web's implementation without requiring <fieldset> semantics.
10
+ *
11
+ * @see https://github.com/material-components/material-web/tree/main/textfield
12
+ * @see https://m3.material.io/components/text-fields/specs#outlined-text-field
13
+ */
14
+
15
+ import { m } from "motion/react";
16
+ import * as React from "react";
17
+ import {
18
+ MD3_INDICATOR_DURATION,
19
+ MD3_LABEL_FLOAT_DURATION,
20
+ MD3_STANDARD_EASING,
21
+ } from "../../shared/constants";
22
+ import { TF_COLORS, TF_SIZE, TF_TYPOGRAPHY } from "../text-field.tokens";
23
+
24
+ export interface OutlineContainerProps {
25
+ /** Whether the label is in the floated position. */
26
+ isFloated: boolean;
27
+ /** Whether the field is focused. */
28
+ isFocused: boolean;
29
+ /** Whether the field is in error state. */
30
+ isError: boolean;
31
+ /** Whether the field is disabled. */
32
+ isDisabled: boolean;
33
+ /** Whether the field is hovered. */
34
+ isHovered: boolean;
35
+ /**
36
+ * Measured width of the label element in its full (unfloated) size.
37
+ * The notch width = labelWidth × scaleRatio + 2×notchPadding.
38
+ */
39
+ labelWidth: number;
40
+ /** Whether to disable animations. */
41
+ prefersReduced: boolean;
42
+ }
43
+
44
+ /**
45
+ * Determines the outline border color based on current state.
46
+ */
47
+ function getOutlineColor(
48
+ isFocused: boolean,
49
+ isError: boolean,
50
+ isHovered: boolean,
51
+ isDisabled: boolean,
52
+ ): string {
53
+ if (isDisabled) return TF_COLORS.onSurfaceVariant;
54
+ if (isError) return TF_COLORS.error;
55
+ if (isFocused) return TF_COLORS.primary;
56
+ if (isHovered) return TF_COLORS.inputText;
57
+ return TF_COLORS.outline;
58
+ }
59
+
60
+ /**
61
+ * MD3 Outlined TextField container with animated notch.
62
+ *
63
+ * The notch gap expands/collapses in sync with the FloatingLabel animation,
64
+ * creating the visual effect of the label breaking through the border.
65
+ *
66
+ * Accessibility: `aria-hidden="true"` — purely decorative border.
67
+ */
68
+ export const OutlineContainer = React.memo(function OutlineContainer({
69
+ isFloated,
70
+ isFocused,
71
+ isError,
72
+ isDisabled,
73
+ isHovered,
74
+ labelWidth,
75
+ prefersReduced,
76
+ }: OutlineContainerProps) {
77
+ const colorDuration = prefersReduced ? 0 : MD3_INDICATOR_DURATION;
78
+ const notchDuration = prefersReduced ? 0 : MD3_LABEL_FLOAT_DURATION;
79
+ const ease = MD3_STANDARD_EASING;
80
+
81
+ const borderColor = getOutlineColor(
82
+ isFocused,
83
+ isError,
84
+ isHovered,
85
+ isDisabled,
86
+ );
87
+ const borderWidth =
88
+ isFocused || isError ? TF_SIZE.outlineThick : TF_SIZE.outlineThin;
89
+
90
+ // Calculate offset for notch (always 16px from the edge in M3 Outlined)
91
+ const leftSegmentWidth = TF_SIZE.paddingStart;
92
+
93
+ /**
94
+ * Notch width calculation:
95
+ * When floated: label is scaled to 0.75, so its displayed width = labelWidth × 0.75
96
+ * Add 2 × notchPadding (4px each side) for spacing inside the notch gap.
97
+ * When unfloated: 0 (no gap in the border).
98
+ */
99
+ const notchWidth = isFloated
100
+ ? labelWidth * TF_TYPOGRAPHY.labelScaleRatio + TF_SIZE.notchPadding * 2
101
+ : 0;
102
+
103
+ const borderTransition = { duration: colorDuration, ease };
104
+ const notchTransition = { duration: notchDuration, ease };
105
+
106
+ return (
107
+ <div
108
+ aria-hidden="true"
109
+ className="absolute inset-0 pointer-events-none flex rounded-[inherit]"
110
+ >
111
+ {/* Left segment — left + bottom + top borders */}
112
+ <m.div
113
+ className="rounded-tl-[inherit] rounded-bl-[inherit]"
114
+ style={{ width: leftSegmentWidth - TF_SIZE.notchPadding }}
115
+ animate={{
116
+ borderColor,
117
+ borderWidth,
118
+ borderStyle: "solid",
119
+ borderRightWidth: 0,
120
+ }}
121
+ transition={borderTransition}
122
+ />
123
+
124
+ {/* Center (notch) segment */}
125
+ <m.div
126
+ className="flex flex-col shrink-0"
127
+ style={{ minWidth: 0 }}
128
+ animate={{ width: notchWidth }}
129
+ transition={notchTransition}
130
+ >
131
+ {/* Top border of notch: transparent when notched */}
132
+ <m.div
133
+ className="shrink-0"
134
+ style={{ height: borderWidth }}
135
+ animate={{
136
+ borderTopColor: borderColor,
137
+ opacity: isFloated ? 0 : 1,
138
+ borderTopWidth: borderWidth,
139
+ borderTopStyle: "solid",
140
+ }}
141
+ transition={borderTransition}
142
+ />
143
+ {/* Bottom border (always present) */}
144
+ <m.div
145
+ className="flex-1"
146
+ animate={{
147
+ borderBottomColor: borderColor,
148
+ borderBottomWidth: borderWidth,
149
+ borderBottomStyle: "solid",
150
+ }}
151
+ transition={borderTransition}
152
+ />
153
+ </m.div>
154
+
155
+ {/* Right segment — full height, right + bottom + top borders */}
156
+ <m.div
157
+ className="flex-1 rounded-tr-[inherit] rounded-br-[inherit]"
158
+ animate={{
159
+ borderColor,
160
+ borderWidth,
161
+ borderStyle: "solid",
162
+ borderLeftWidth: 0,
163
+ }}
164
+ transition={borderTransition}
165
+ />
166
+ </div>
167
+ );
168
+ });
169
+
170
+ OutlineContainer.displayName = "OutlineContainer";
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @file prefix-suffix.tsx
3
+ * Prefix and suffix text for MD3 TextField.
4
+ * Visible only when the label is floated (or when there is no label).
5
+ */
6
+
7
+ import { AnimatePresence, m } from "motion/react";
8
+ import * as React from "react";
9
+ import {
10
+ MD3_LABEL_FLOAT_DURATION,
11
+ MD3_STANDARD_EASING,
12
+ } from "../../shared/constants";
13
+ import { TF_CLASSES } from "../text-field.tokens";
14
+
15
+ export interface PrefixSuffixProps {
16
+ text: string;
17
+ type: "prefix" | "suffix";
18
+ /** Whether the label is floated (controls visibility). */
19
+ visible: boolean;
20
+ /** Disable animations. */
21
+ prefersReduced: boolean;
22
+ }
23
+
24
+ /**
25
+ * MD3 Prefix / Suffix Text.
26
+ *
27
+ * Animates in/out in sync with the floating label using AnimatePresence.
28
+ * Hidden when label is in the inline position (would overlap the label).
29
+ *
30
+ * @accessibility
31
+ * `aria-hidden="true"` — decorative. Screen readers read the full value in context.
32
+ */
33
+ export const PrefixSuffix = React.memo(function PrefixSuffix({
34
+ text,
35
+ type,
36
+ visible,
37
+ prefersReduced,
38
+ }: PrefixSuffixProps) {
39
+ const duration = prefersReduced ? 0 : MD3_LABEL_FLOAT_DURATION;
40
+
41
+ return (
42
+ <AnimatePresence>
43
+ {visible && (
44
+ <m.span
45
+ aria-hidden="true"
46
+ className={`${TF_CLASSES.prefixSuffix} ${type === "suffix" ? "ml-0.5" : "mr-0.5"}`}
47
+ initial={{ opacity: 0 }}
48
+ animate={{ opacity: 1 }}
49
+ exit={{ opacity: 0 }}
50
+ transition={{ duration, ease: MD3_STANDARD_EASING }}
51
+ >
52
+ {text}
53
+ </m.span>
54
+ )}
55
+ </AnimatePresence>
56
+ );
57
+ });
58
+
59
+ PrefixSuffix.displayName = "PrefixSuffix";