@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,288 @@
1
+ /**
2
+ * @file slider.types.ts
3
+ * MD3 Expressive Slider — TypeScript prop definitions.
4
+ * Spec: https://m3.material.io/components/sliders/overview
5
+ * Reference: docs/m3/sliders/Slider.kt
6
+ */
7
+
8
+ import type * as React from "react";
9
+
10
+ // ─── Track Size ───────────────────────────────────────────────────────────────
11
+
12
+ /** Track size variants mapping to physical px values. */
13
+ export type SliderTrackSize = "xs" | "s" | "m" | "l" | "xl";
14
+ export type SliderVariant = "primary" | "secondary" | "tertiary" | "error";
15
+
16
+ export type SliderTrackShape = "md3" | "full" | number;
17
+
18
+ // ─── Orientation ──────────────────────────────────────────────────────────────
19
+
20
+ /** Slider layout direction. */
21
+ export type SliderOrientation = "horizontal" | "vertical";
22
+
23
+ // ─── Internal Context ────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Internal context shared between Slider sub-components.
27
+ * @internal
28
+ */
29
+ export interface SliderContextValue {
30
+ /** Minimum allowed value. */
31
+ min: number;
32
+ /** Maximum allowed value. */
33
+ max: number;
34
+ /**
35
+ * Step size. When > 0, slider is discrete and snaps to multiples of step.
36
+ * When 0, slider is continuous.
37
+ */
38
+ step: number;
39
+ /** Whether the slider is interactive. */
40
+ disabled: boolean;
41
+ /** Layout orientation. */
42
+ orientation: SliderOrientation;
43
+ /** Physical size of the track. */
44
+ trackSize: SliderTrackSize;
45
+ /** Color variant. */
46
+ variant: SliderVariant;
47
+ /**
48
+ * When true, active track originates from center (50%) instead of the min end.
49
+ * Mirrors Compose's `SliderDefaults.Track(drawCenteredTrack = true)`.
50
+ */
51
+ isCentered: boolean;
52
+ /** Show the floating value tooltip on hover/drag. */
53
+ showValueIndicator: boolean;
54
+ /** Ref to the track DOM element — used for drag constraint. */
55
+ trackRef: React.RefObject<HTMLDivElement | null>;
56
+ }
57
+
58
+ // ─── Slider Props ────────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Props for the `<Slider>` component.
62
+ *
63
+ * Supports both controlled (`value` + `onValueChange`) and
64
+ * uncontrolled (`defaultValue`) usage patterns per React standards.
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * // Controlled
69
+ * <Slider value={volume} onValueChange={setVolume} min={0} max={100} />
70
+ *
71
+ * // Uncontrolled
72
+ * <Slider defaultValue={50} />
73
+ *
74
+ * // Discrete (step snapping)
75
+ * <Slider defaultValue={0} step={10} />
76
+ *
77
+ * // Vertical orientation
78
+ * <Slider defaultValue={50} orientation="vertical" />
79
+ *
80
+ * // Centered active track
81
+ * <Slider defaultValue={0} isCentered />
82
+ * ```
83
+ */
84
+ export interface SliderProps {
85
+ /** Controlled current value. Use with `onValueChange`. */
86
+ value?: number;
87
+ /** Initial value for uncontrolled usage. @default midpoint of min/max */
88
+ defaultValue?: number;
89
+ /** Called whenever the value changes during interaction. */
90
+ onValueChange?: (value: number) => void;
91
+ /** Called when the user finishes dragging / commits a keyboard change. */
92
+ onValueChangeEnd?: (value: number) => void;
93
+ /** Minimum value. @default 0 */
94
+ min?: number;
95
+ /** Maximum value. @default 100 */
96
+ max?: number;
97
+ /**
98
+ * Step size. When > 0, slider snaps to multiples of `step` from `min`
99
+ * and renders tick marks. When 0, slider is continuous.
100
+ * @default 0
101
+ */
102
+ step?: number;
103
+ /** Layout orientation. @default "horizontal" */
104
+ orientation?: SliderOrientation;
105
+ /**
106
+ * Physical track size.
107
+ * Horizontal: height. Vertical: width.
108
+ * @default "m"
109
+ */
110
+ trackSize?: SliderTrackSize;
111
+ /**
112
+ * Color variant.
113
+ * @default "primary"
114
+ */
115
+ variant?: SliderVariant;
116
+ /**
117
+ * When true, the active track segment grows from the center (50%)
118
+ * outward toward the thumb position.
119
+ * @default false
120
+ */
121
+ isCentered?: boolean;
122
+ /** Disables all interaction. @default false */
123
+ disabled?: boolean;
124
+ /**
125
+ * When true, shows a floating value indicator tooltip above the thumb.
126
+ * @default false
127
+ */
128
+ showValueIndicator?: boolean;
129
+ /**
130
+ * When true, shows tick marks along the track.
131
+ * Only applicable if `step` > 0.
132
+ * @default false
133
+ */
134
+ showTicks?: boolean;
135
+ /**
136
+ * Track shape configuration for border radius.
137
+ * - "md3": Default MD3 specific border radius per size
138
+ * - "full": Fully rounded ends (pill shape - size/2)
139
+ * - number: Custom border radius in px
140
+ * @default "md3"
141
+ */
142
+ trackShape?: SliderTrackShape;
143
+ /**
144
+ * Icon rendered inside the track (inset icon).
145
+ * MD3 spec: only valid for M, L, XL track sizes.
146
+ * The icon moves from the active track to the inactive track
147
+ * when there's not enough space at low values.
148
+ * Do not use with `isCentered` or `RangeSlider`.
149
+ */
150
+ insetIcon?: React.ReactNode;
151
+ /**
152
+ * Alternate icon shown when value equals `min`.
153
+ * Swaps with `insetIcon` at the minimum value
154
+ * (e.g., a mute icon replacing a volume icon when volume = 0).
155
+ */
156
+ insetIconAtMin?: React.ReactNode;
157
+ /**
158
+ * Icon rendered inside the track at the trailing end (right side).
159
+ * Only valid for track sizes >= 40dp (e.g. XL).
160
+ */
161
+ insetIconTrailing?: React.ReactNode;
162
+ /**
163
+ * Alternate icon shown when value equals `max`.
164
+ * Swaps with `insetIconTrailing` at the maximum value.
165
+ */
166
+ insetIconAtMax?: React.ReactNode;
167
+ /** Additional CSS class applied to the outermost wrapper. */
168
+ className?: string;
169
+ /**
170
+ * Accessible label for the slider when no visible label exists.
171
+ * Required if parent does not have a visible label.
172
+ */
173
+ "aria-label"?: string;
174
+ /** ID of a visible label element. Required if `aria-label` is not provided. */
175
+ "aria-labelledby"?: string;
176
+ /**
177
+ * Format function for the displayed value in the value indicator tooltip.
178
+ * Defaults to `String(value)`.
179
+ */
180
+ formatValue?: (value: number) => string;
181
+ }
182
+
183
+ // ─── Range Slider Props ──────────────────────────────────────────────────────
184
+
185
+ /**
186
+ * Props for the `<RangeSlider>` component.
187
+ *
188
+ * Extends `SliderProps` with tuple-based value API.
189
+ * The two thumbs cannot cross each other.
190
+ *
191
+ * @example
192
+ * ```tsx
193
+ * <RangeSlider
194
+ * value={[20, 80]}
195
+ * onValueChange={([start, end]) => setRange([start, end])}
196
+ * />
197
+ * ```
198
+ */
199
+ export interface RangeSliderProps
200
+ extends Omit<
201
+ SliderProps,
202
+ | "value"
203
+ | "defaultValue"
204
+ | "onValueChange"
205
+ | "onValueChangeEnd"
206
+ | "isCentered"
207
+ > {
208
+ /** Controlled [start, end] tuple. Use with `onValueChange`. */
209
+ value?: [number, number];
210
+ /** Initial [start, end] tuple for uncontrolled usage. */
211
+ defaultValue?: [number, number];
212
+ /** Called whenever [start, end] changes during interaction. */
213
+ onValueChange?: (value: [number, number]) => void;
214
+ /** Called when the user finishes dragging either thumb. */
215
+ onValueChangeEnd?: (value: [number, number]) => void;
216
+ }
217
+
218
+ // ─── Internal Sub-component Props ────────────────────────────────────────────
219
+
220
+ /**
221
+ * Props for `<SliderTrack>`.
222
+ * @internal
223
+ */
224
+ export interface SliderTrackProps {
225
+ /** Current thumb position as 0–1 fraction. */
226
+ percent: number;
227
+ trackSize: SliderTrackSize;
228
+ orientation: SliderOrientation;
229
+ variant: SliderVariant;
230
+ isCentered: boolean;
231
+ /** For discrete mode: step size. 0 = no ticks. */
232
+ step: number;
233
+ min: number;
234
+ max: number;
235
+ disabled: boolean;
236
+ trackShape?: SliderTrackShape;
237
+ /** Ref forwarded to the root track element for drag constraint. */
238
+ trackRef: React.RefObject<HTMLDivElement | null>;
239
+ /** onClick handler on the track for click-to-jump. */
240
+ onTrackPointerDown?: (e: React.PointerEvent<HTMLDivElement>) => void;
241
+ /**
242
+ * Icon rendered inside the track (inset icon).
243
+ * @internal — passed down from Slider after guard check.
244
+ */
245
+ insetIcon?: React.ReactNode;
246
+ /** Alternate icon swapped in when value === min. @internal */
247
+ insetIconAtMin?: React.ReactNode;
248
+ /** Icon rendered at the trailing end (right side). @internal */
249
+ insetIconTrailing?: React.ReactNode;
250
+ /** Alternate icon swapped in when value === max. @internal */
251
+ insetIconAtMax?: React.ReactNode;
252
+ /** Current slider value — used for inset icon swap at min/max. @internal */
253
+ value?: number;
254
+ }
255
+
256
+ /**
257
+ * Props for `<SliderThumb>`.
258
+ * @internal
259
+ */
260
+ export interface SliderThumbProps {
261
+ /** Current value (for ARIA). */
262
+ value: number;
263
+ /** Current 0–1 fraction (for positioning). */
264
+ percent: number;
265
+ min: number;
266
+ max: number;
267
+ step: number;
268
+ disabled: boolean;
269
+ orientation: SliderOrientation;
270
+ showValueIndicator: boolean;
271
+ /** For the drag constraint ref. */
272
+ trackRef: React.RefObject<HTMLDivElement | null>;
273
+ trackSize: SliderTrackSize;
274
+ variant: SliderVariant;
275
+ /** Called during pointer drag with new value. */
276
+ onValueChange: (value: number) => void;
277
+ /** Called on drag end / keyboard commit. */
278
+ onValueChangeEnd?: (value: number) => void;
279
+ /** Value display formatter. */
280
+ formatValue?: (value: number) => string;
281
+ /** Unique ID for ARIA. */
282
+ thumbId?: string;
283
+ /** zIndex for RangeSlider layering. */
284
+ zIndex?: number;
285
+ /** Optional accessible label for this specific thumb. */
286
+ "aria-label"?: string;
287
+ "aria-labelledby"?: string;
288
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file snackbar/index.ts
3
+ * Barrel re-export for the MD3 Expressive Snackbar component system.
4
+ */
5
+ export type {
6
+ SnackbarData,
7
+ SnackbarDuration,
8
+ SnackbarHostProps,
9
+ SnackbarProps,
10
+ SnackbarResult,
11
+ SnackbarVisuals,
12
+ UseSnackbarStateReturn,
13
+ } from "./snackbar";
14
+ export {
15
+ Snackbar,
16
+ SnackbarHost,
17
+ SnackbarProvider,
18
+ useSnackbar,
19
+ useSnackbarState,
20
+ } from "./snackbar";
@@ -0,0 +1,338 @@
1
+ "use client";
2
+
3
+ import { act, render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { SnackbarData } from "./snackbar";
7
+ import {
8
+ Snackbar,
9
+ SnackbarHost,
10
+ SnackbarProvider,
11
+ useSnackbar,
12
+ useSnackbarState,
13
+ } from "./snackbar";
14
+
15
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
16
+
17
+ /** Builds a minimal SnackbarData object for pure Snackbar tests. */
18
+ function makeSnackbarData(overrides: Partial<SnackbarData> = {}): SnackbarData {
19
+ return {
20
+ id: "test-id",
21
+ visuals: { message: "Test message", duration: 9999999 },
22
+ resolve: vi.fn(),
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ // ─── 1. Renders message ───────────────────────────────────────────────────────
28
+
29
+ describe("Snackbar — renders message", () => {
30
+ it("displays the message text", () => {
31
+ const data = makeSnackbarData({
32
+ visuals: { message: "File deleted", duration: 9999999 },
33
+ });
34
+ render(<Snackbar data={data} />);
35
+ expect(screen.getByText("File deleted")).toBeInTheDocument();
36
+ });
37
+ });
38
+
39
+ // ─── 2. Renders action button ─────────────────────────────────────────────────
40
+
41
+ describe("Snackbar — action button", () => {
42
+ it("renders action button when actionLabel is provided", () => {
43
+ const data = makeSnackbarData({
44
+ visuals: { message: "Archived", actionLabel: "Undo", duration: 9999999 },
45
+ });
46
+ render(<Snackbar data={data} />);
47
+ expect(screen.getByRole("button", { name: "Undo" })).toBeInTheDocument();
48
+ });
49
+
50
+ it("does not render action button when actionLabel is absent", () => {
51
+ const data = makeSnackbarData({
52
+ visuals: { message: "No action", duration: 9999999 },
53
+ });
54
+ render(<Snackbar data={data} />);
55
+ // Only the close button might be present — but not an "action"
56
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
57
+ });
58
+ });
59
+
60
+ // ─── 3. Action click resolves 'action-performed' ──────────────────────────────
61
+
62
+ describe("Snackbar — action click resolves promise", () => {
63
+ it("calls resolve with 'action-performed' when action button is clicked", async () => {
64
+ const user = userEvent.setup();
65
+ const resolveMock = vi.fn();
66
+ const data = makeSnackbarData({
67
+ visuals: { message: "Saved", actionLabel: "Undo", duration: 9999999 },
68
+ resolve: resolveMock,
69
+ });
70
+ render(<Snackbar data={data} />);
71
+ await user.click(screen.getByRole("button", { name: "Undo" }));
72
+ expect(resolveMock).toHaveBeenCalledWith("action-performed");
73
+ });
74
+ });
75
+
76
+ // ─── 4. Auto-dismiss ─────────────────────────────────────────────────────────
77
+
78
+ describe("Snackbar — auto-dismiss", () => {
79
+ beforeEach(() => {
80
+ vi.useFakeTimers();
81
+ });
82
+ afterEach(() => {
83
+ vi.useRealTimers();
84
+ });
85
+
86
+ it("calls resolve('dismissed') after short duration (4000ms)", () => {
87
+ const resolveMock = vi.fn();
88
+ const data = makeSnackbarData({
89
+ visuals: { message: "Auto dismiss", duration: "short" },
90
+ resolve: resolveMock,
91
+ });
92
+ render(<Snackbar data={data} />);
93
+ expect(resolveMock).not.toHaveBeenCalled();
94
+ act(() => {
95
+ vi.advanceTimersByTime(4000);
96
+ });
97
+ expect(resolveMock).toHaveBeenCalledWith("dismissed");
98
+ });
99
+
100
+ it("calls resolve after custom duration", () => {
101
+ const resolveMock = vi.fn();
102
+ const data = makeSnackbarData({
103
+ visuals: { message: "Custom timer", duration: 1500 },
104
+ resolve: resolveMock,
105
+ });
106
+ render(<Snackbar data={data} />);
107
+ act(() => {
108
+ vi.advanceTimersByTime(1499);
109
+ });
110
+ expect(resolveMock).not.toHaveBeenCalled();
111
+ act(() => {
112
+ vi.advanceTimersByTime(1);
113
+ });
114
+ expect(resolveMock).toHaveBeenCalledWith("dismissed");
115
+ });
116
+ });
117
+
118
+ // ─── 5. Dismiss button ────────────────────────────────────────────────────────
119
+
120
+ describe("Snackbar — dismiss button", () => {
121
+ it("renders close button when withDismissAction=true", () => {
122
+ const data = makeSnackbarData({
123
+ visuals: {
124
+ message: "Hello",
125
+ withDismissAction: true,
126
+ duration: 9999999,
127
+ },
128
+ });
129
+ render(<Snackbar data={data} />);
130
+ expect(
131
+ screen.getByRole("button", { name: /dismiss/i }),
132
+ ).toBeInTheDocument();
133
+ });
134
+
135
+ it("calls resolve('dismissed') when close button is clicked", async () => {
136
+ const user = userEvent.setup();
137
+ const resolveMock = vi.fn();
138
+ const data = makeSnackbarData({
139
+ visuals: {
140
+ message: "Hello",
141
+ withDismissAction: true,
142
+ duration: 9999999,
143
+ },
144
+ resolve: resolveMock,
145
+ });
146
+ render(<Snackbar data={data} />);
147
+ await user.click(screen.getByRole("button", { name: /dismiss/i }));
148
+ expect(resolveMock).toHaveBeenCalledWith("dismissed");
149
+ });
150
+ });
151
+
152
+ // ─── 6. Queue behavior ────────────────────────────────────────────────────────
153
+
154
+ describe("SnackbarHost — queue behavior", () => {
155
+ it("shows second snackbar after first is dismissed", async () => {
156
+ const user = userEvent.setup();
157
+
158
+ function Wrapper() {
159
+ const state = useSnackbarState();
160
+ return (
161
+ <>
162
+ <button
163
+ type="button"
164
+ onClick={() =>
165
+ state.showSnackbar({
166
+ message: "Snackbar One",
167
+ actionLabel: "Close One",
168
+ duration: 9999999,
169
+ })
170
+ }
171
+ >
172
+ Show One
173
+ </button>
174
+ <button
175
+ type="button"
176
+ onClick={() =>
177
+ state.showSnackbar({
178
+ message: "Snackbar Two",
179
+ duration: 9999999,
180
+ })
181
+ }
182
+ >
183
+ Show Two
184
+ </button>
185
+ <SnackbarHost state={state} />
186
+ </>
187
+ );
188
+ }
189
+
190
+ render(<Wrapper />);
191
+
192
+ // Show first
193
+ await user.click(screen.getByRole("button", { name: "Show One" }));
194
+ expect(screen.getByText("Snackbar One")).toBeInTheDocument();
195
+
196
+ // Queue second immediately (first still showing)
197
+ await user.click(screen.getByRole("button", { name: "Show Two" }));
198
+
199
+ // Second should NOT be visible yet
200
+ expect(screen.queryByText("Snackbar Two")).not.toBeInTheDocument();
201
+
202
+ // Dismiss first via its action button
203
+ await user.click(screen.getByRole("button", { name: "Close One" }));
204
+
205
+ // Second should now appear
206
+ await waitFor(() => {
207
+ expect(screen.getByText("Snackbar Two")).toBeInTheDocument();
208
+ });
209
+ });
210
+ });
211
+
212
+ // ─── 7. actionOnNewLine layout ────────────────────────────────────────────────
213
+
214
+ describe("Snackbar — actionOnNewLine", () => {
215
+ it("applies flex-col when actionOnNewLine=true", () => {
216
+ const data = makeSnackbarData({
217
+ visuals: {
218
+ message: "Long message text here",
219
+ actionLabel: "Action",
220
+ actionOnNewLine: true,
221
+ duration: 9999999,
222
+ },
223
+ });
224
+ const { container } = render(<Snackbar data={data} />);
225
+ // The snackbar root div should have flex-col
226
+ const snackbarEl = container.firstChild as HTMLElement;
227
+ expect(snackbarEl.className).toContain("flex-col");
228
+ });
229
+
230
+ it("applies flex-row by default (actionOnNewLine=false)", () => {
231
+ const data = makeSnackbarData({
232
+ visuals: {
233
+ message: "Message",
234
+ actionLabel: "Action",
235
+ actionOnNewLine: false,
236
+ duration: 9999999,
237
+ },
238
+ });
239
+ const { container } = render(<Snackbar data={data} />);
240
+ const snackbarEl = container.firstChild as HTMLElement;
241
+ expect(snackbarEl.className).toContain("flex-row");
242
+ });
243
+ });
244
+
245
+ // ─── 8. Accessibility ─────────────────────────────────────────────────────────
246
+
247
+ describe("Snackbar — accessibility", () => {
248
+ it("has role='status' on the container", () => {
249
+ const data = makeSnackbarData();
250
+ render(<Snackbar data={data} />);
251
+ expect(screen.getByRole("status")).toBeInTheDocument();
252
+ });
253
+
254
+ it("has aria-live='polite' on the container", () => {
255
+ const data = makeSnackbarData();
256
+ const { container } = render(<Snackbar data={data} />);
257
+ const statusEl = container.querySelector("[role='status']");
258
+ expect(statusEl).toHaveAttribute("aria-live", "polite");
259
+ });
260
+
261
+ it("has aria-atomic='true' on the container", () => {
262
+ const data = makeSnackbarData();
263
+ const { container } = render(<Snackbar data={data} />);
264
+ const statusEl = container.querySelector("[role='status']");
265
+ expect(statusEl).toHaveAttribute("aria-atomic", "true");
266
+ });
267
+ });
268
+
269
+ // ─── 9. Custom className ──────────────────────────────────────────────────────
270
+
271
+ describe("Snackbar — custom className", () => {
272
+ it("applies custom className to the container", () => {
273
+ const data = makeSnackbarData({
274
+ visuals: {
275
+ message: "Styled",
276
+ className: "my-custom-class",
277
+ duration: 9999999,
278
+ },
279
+ });
280
+ const { container } = render(<Snackbar data={data} />);
281
+ const snackbarEl = container.firstChild as HTMLElement;
282
+ expect(snackbarEl.className).toContain("my-custom-class");
283
+ });
284
+ });
285
+
286
+ // ─── 10. SnackbarProvider + useSnackbar ───────────────────────────────────────
287
+
288
+ describe("SnackbarProvider + useSnackbar", () => {
289
+ it("provides showSnackbar via useSnackbar hook", async () => {
290
+ const user = userEvent.setup();
291
+ function TestConsumer() {
292
+ const { showSnackbar } = useSnackbar();
293
+ return (
294
+ <button
295
+ type="button"
296
+ onClick={async () => {
297
+ await showSnackbar({
298
+ message: "Provider test",
299
+ duration: 9999999,
300
+ });
301
+ }}
302
+ >
303
+ Trigger
304
+ </button>
305
+ );
306
+ }
307
+
308
+ render(
309
+ <SnackbarProvider>
310
+ <TestConsumer />
311
+ </SnackbarProvider>,
312
+ );
313
+
314
+ await user.click(screen.getByRole("button", { name: "Trigger" }));
315
+
316
+ // Snackbar should appear in the provider's SnackbarHost
317
+ await waitFor(() => {
318
+ expect(screen.getByText("Provider test")).toBeInTheDocument();
319
+ });
320
+ });
321
+
322
+ it("throws when useSnackbar is used outside SnackbarProvider", () => {
323
+ const originalError = console.error;
324
+ // Suppress React's boundary error log
325
+ console.error = vi.fn();
326
+
327
+ function BadConsumer() {
328
+ useSnackbar();
329
+ return null;
330
+ }
331
+
332
+ expect(() => render(<BadConsumer />)).toThrow(
333
+ "useSnackbar must be used within a <SnackbarProvider>.",
334
+ );
335
+
336
+ console.error = originalError;
337
+ });
338
+ });