@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,309 @@
1
+ /**
2
+ * @file switch.stories.tsx
3
+ * MD3 Expressive Switch — all usage patterns and states.
4
+ *
5
+ * Can be used as:
6
+ * 1. Storybook stories (if Storybook is configured)
7
+ * 2. Standalone demo component for the docs app
8
+ *
9
+ * Covers all 7 patterns from the MD3 Switch specification:
10
+ * 1. Basic toggle
11
+ * 2. With label
12
+ * 3. With icons (both states)
13
+ * 4. With only selected icon
14
+ * 5. Disabled (checked)
15
+ * 6. Disabled (unchecked)
16
+ * 7. All states grid
17
+ */
18
+
19
+ "use client";
20
+
21
+ import * as React from "react";
22
+ import { Switch } from "./switch";
23
+
24
+ // ─── Icon helper ──────────────────────────────────────────────────────────────
25
+
26
+ /** Simple check icon using SVG (no external icon dependency). @internal */
27
+ function CheckIcon() {
28
+ return (
29
+ <svg
30
+ viewBox="0 0 16 16"
31
+ width={16}
32
+ height={16}
33
+ fill="none"
34
+ stroke="currentColor"
35
+ strokeWidth={2.5}
36
+ strokeLinecap="round"
37
+ strokeLinejoin="round"
38
+ aria-hidden="true"
39
+ >
40
+ <polyline points="3,8 6.5,11.5 13,5" />
41
+ </svg>
42
+ );
43
+ }
44
+
45
+ /** Simple close/X icon for unselected state. @internal */
46
+ function CloseIcon() {
47
+ return (
48
+ <svg
49
+ viewBox="0 0 16 16"
50
+ width={16}
51
+ height={16}
52
+ fill="none"
53
+ stroke="currentColor"
54
+ strokeWidth={2.5}
55
+ strokeLinecap="round"
56
+ aria-hidden="true"
57
+ >
58
+ <line x1="4" y1="4" x2="12" y2="12" />
59
+ <line x1="12" y1="4" x2="4" y2="12" />
60
+ </svg>
61
+ );
62
+ }
63
+
64
+ // ─── Story 1: Basic ───────────────────────────────────────────────────────────
65
+
66
+ /** Basic stateful switch — no label, no icons. */
67
+ export function Basic() {
68
+ const [checked, setChecked] = React.useState(false);
69
+ return (
70
+ <div className="flex flex-col gap-4">
71
+ <p className="text-sm text-neutral-500">
72
+ State: <strong>{checked ? "On" : "Off"}</strong>
73
+ </p>
74
+ <Switch
75
+ checked={checked}
76
+ onCheckedChange={setChecked}
77
+ ariaLabel="Basic switch"
78
+ />
79
+ </div>
80
+ );
81
+ }
82
+
83
+ // ─── Story 2: With Label ──────────────────────────────────────────────────────
84
+
85
+ /** Switch with a visible text label using htmlFor linkage. */
86
+ export function WithLabel() {
87
+ const [checked, setChecked] = React.useState(true);
88
+ return (
89
+ <Switch checked={checked} onCheckedChange={setChecked} label="Wi-Fi" />
90
+ );
91
+ }
92
+
93
+ // ─── Story 3: With Icons (both states) ───────────────────────────────────────
94
+
95
+ /**
96
+ * Switch showing icons in both checked and unchecked states.
97
+ * Uses check icon when on, close icon when off.
98
+ */
99
+ export function WithIcons() {
100
+ const [checked, setChecked] = React.useState(false);
101
+ return (
102
+ <div className="flex flex-col gap-4">
103
+ <Switch
104
+ checked={checked}
105
+ onCheckedChange={setChecked}
106
+ icons
107
+ thumbContent={checked ? <CheckIcon /> : <CloseIcon />}
108
+ label="Notifications"
109
+ />
110
+ </div>
111
+ );
112
+ }
113
+
114
+ // ─── Story 4: Only Selected Icon ─────────────────────────────────────────────
115
+
116
+ /**
117
+ * Switch showing the icon only in the checked (selected) state.
118
+ * Unselected state has no icon.
119
+ */
120
+ export function WithOnlySelectedIcon() {
121
+ const [checked, setChecked] = React.useState(false);
122
+ return (
123
+ <Switch
124
+ checked={checked}
125
+ onCheckedChange={setChecked}
126
+ showOnlySelectedIcon
127
+ thumbContent={<CheckIcon />}
128
+ label="Dark mode"
129
+ />
130
+ );
131
+ }
132
+
133
+ // ─── Story 5: Disabled (checked) ─────────────────────────────────────────────
134
+
135
+ /** Disabled switch in the checked (on) state. */
136
+ export function DisabledChecked() {
137
+ return (
138
+ <Switch
139
+ checked
140
+ onCheckedChange={() => {}}
141
+ disabled
142
+ label="Always on (disabled)"
143
+ />
144
+ );
145
+ }
146
+
147
+ // ─── Story 6: Disabled (unchecked) ───────────────────────────────────────────
148
+
149
+ /** Disabled switch in the unchecked (off) state. */
150
+ export function DisabledUnchecked() {
151
+ return (
152
+ <Switch
153
+ checked={false}
154
+ onCheckedChange={() => {}}
155
+ disabled
156
+ label="Unavailable feature (disabled)"
157
+ />
158
+ );
159
+ }
160
+
161
+ // ─── Story 7: All States Grid ─────────────────────────────────────────────────
162
+
163
+ /**
164
+ * Grid displaying all Switch state combinations:
165
+ * - Enabled: checked / unchecked
166
+ * - Disabled: checked / unchecked
167
+ * - With icons: checked / unchecked
168
+ * - With only selected icon: checked / unchecked
169
+ */
170
+ export function AllStatesGrid() {
171
+ const [checkedMap, setCheckedMap] = React.useState<Record<string, boolean>>({
172
+ enabled_on: true,
173
+ enabled_off: false,
174
+ icons_on: true,
175
+ icons_off: false,
176
+ selected_icon_on: true,
177
+ selected_icon_off: false,
178
+ });
179
+
180
+ const toggle = (key: string) =>
181
+ setCheckedMap((prev) => ({ ...prev, [key]: !prev[key] }));
182
+
183
+ const Row = ({
184
+ title,
185
+ children,
186
+ }: {
187
+ title: string;
188
+ children: React.ReactNode;
189
+ }) => (
190
+ <div className="flex items-center gap-6">
191
+ <span className="w-48 text-sm text-neutral-500 shrink-0">{title}</span>
192
+ {children}
193
+ </div>
194
+ );
195
+
196
+ return (
197
+ <div className="flex flex-col gap-6 p-4">
198
+ <Row title="Enabled (unchecked)">
199
+ <Switch
200
+ checked={checkedMap.enabled_off}
201
+ onCheckedChange={() => toggle("enabled_off")}
202
+ ariaLabel="Enabled unchecked"
203
+ />
204
+ </Row>
205
+ <Row title="Enabled (checked)">
206
+ <Switch
207
+ checked={checkedMap.enabled_on}
208
+ onCheckedChange={() => toggle("enabled_on")}
209
+ ariaLabel="Enabled checked"
210
+ />
211
+ </Row>
212
+ <Row title="Disabled (unchecked)">
213
+ <Switch
214
+ checked={false}
215
+ onCheckedChange={() => {}}
216
+ disabled
217
+ ariaLabel="Disabled unchecked"
218
+ />
219
+ </Row>
220
+ <Row title="Disabled (checked)">
221
+ <Switch
222
+ checked
223
+ onCheckedChange={() => {}}
224
+ disabled
225
+ ariaLabel="Disabled checked"
226
+ />
227
+ </Row>
228
+ <Row title="With icons (off)">
229
+ <Switch
230
+ checked={checkedMap.icons_off}
231
+ onCheckedChange={() => toggle("icons_off")}
232
+ icons
233
+ thumbContent={checkedMap.icons_off ? <CheckIcon /> : <CloseIcon />}
234
+ ariaLabel="With icons unchecked"
235
+ />
236
+ </Row>
237
+ <Row title="With icons (on)">
238
+ <Switch
239
+ checked={checkedMap.icons_on}
240
+ onCheckedChange={() => toggle("icons_on")}
241
+ icons
242
+ thumbContent={checkedMap.icons_on ? <CheckIcon /> : <CloseIcon />}
243
+ ariaLabel="With icons checked"
244
+ />
245
+ </Row>
246
+ <Row title="Selected icon only (off)">
247
+ <Switch
248
+ checked={checkedMap.selected_icon_off}
249
+ onCheckedChange={() => toggle("selected_icon_off")}
250
+ showOnlySelectedIcon
251
+ thumbContent={<CheckIcon />}
252
+ ariaLabel="Selected icon only unchecked"
253
+ />
254
+ </Row>
255
+ <Row title="Selected icon only (on)">
256
+ <Switch
257
+ checked={checkedMap.selected_icon_on}
258
+ onCheckedChange={() => toggle("selected_icon_on")}
259
+ showOnlySelectedIcon
260
+ thumbContent={<CheckIcon />}
261
+ ariaLabel="Selected icon only checked"
262
+ />
263
+ </Row>
264
+ </div>
265
+ );
266
+ }
267
+
268
+ // ─── Master Demo ──────────────────────────────────────────────────────────────
269
+
270
+ /**
271
+ * All switch stories in one scrollable demo page.
272
+ * Used by the docs app to showcase all variants.
273
+ */
274
+ export function SwitchDemo() {
275
+ return (
276
+ <div className="flex flex-col gap-10 p-8 max-w-2xl">
277
+ <section>
278
+ <h2 className="text-xl font-semibold mb-4">1. Basic</h2>
279
+ <Basic />
280
+ </section>
281
+ <section>
282
+ <h2 className="text-xl font-semibold mb-4">2. With Label</h2>
283
+ <WithLabel />
284
+ </section>
285
+ <section>
286
+ <h2 className="text-xl font-semibold mb-4">
287
+ 3. With Icons (Both States)
288
+ </h2>
289
+ <WithIcons />
290
+ </section>
291
+ <section>
292
+ <h2 className="text-xl font-semibold mb-4">4. Only Selected Icon</h2>
293
+ <WithOnlySelectedIcon />
294
+ </section>
295
+ <section>
296
+ <h2 className="text-xl font-semibold mb-4">5. Disabled (Checked)</h2>
297
+ <DisabledChecked />
298
+ </section>
299
+ <section>
300
+ <h2 className="text-xl font-semibold mb-4">6. Disabled (Unchecked)</h2>
301
+ <DisabledUnchecked />
302
+ </section>
303
+ <section>
304
+ <h2 className="text-xl font-semibold mb-4">7. All States Grid</h2>
305
+ <AllStatesGrid />
306
+ </section>
307
+ </div>
308
+ );
309
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @file switch.test.tsx
3
+ *
4
+ * Comprehensive test suite for the MD3 Expressive Switch component.
5
+ * Tests cover: rendering, controlled state, disabled, accessibility,
6
+ * and keyboard interaction.
7
+ */
8
+
9
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
10
+ import * as React from "react";
11
+ import { afterEach, describe, expect, it, vi } from "vitest";
12
+ import { Switch } from "./switch";
13
+
14
+ afterEach(cleanup);
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // Rendering
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ describe("Switch — Rendering", () => {
21
+ it("renders unchecked by default", () => {
22
+ const onCheckedChange = vi.fn();
23
+ render(
24
+ <Switch
25
+ checked={false}
26
+ onCheckedChange={onCheckedChange}
27
+ ariaLabel="Test switch"
28
+ />,
29
+ );
30
+ const toggle = screen.getByRole("switch", { name: "Test switch" });
31
+ expect(toggle).toHaveAttribute("aria-checked", "false");
32
+ });
33
+
34
+ it("renders checked when checked=true", () => {
35
+ const onCheckedChange = vi.fn();
36
+ render(
37
+ <Switch
38
+ checked={true}
39
+ onCheckedChange={onCheckedChange}
40
+ ariaLabel="Test switch"
41
+ />,
42
+ );
43
+ const toggle = screen.getByRole("switch", { name: "Test switch" });
44
+ expect(toggle).toHaveAttribute("aria-checked", "true");
45
+ });
46
+
47
+ it("renders with label text", () => {
48
+ const onCheckedChange = vi.fn();
49
+ render(
50
+ <Switch
51
+ checked={false}
52
+ onCheckedChange={onCheckedChange}
53
+ label="Notifications"
54
+ />,
55
+ );
56
+ expect(screen.getByText("Notifications")).toBeInTheDocument();
57
+ // Label should be associated with the switch
58
+ const toggle = screen.getByRole("switch", { name: "Notifications" });
59
+ expect(toggle).toBeInTheDocument();
60
+ });
61
+
62
+ it("applies custom className to the button (when no label)", () => {
63
+ const onCheckedChange = vi.fn();
64
+ render(
65
+ <Switch
66
+ checked={false}
67
+ onCheckedChange={onCheckedChange}
68
+ ariaLabel="Test"
69
+ className="custom-class"
70
+ />,
71
+ );
72
+ const toggle = screen.getByRole("switch");
73
+ expect(toggle).toHaveClass("custom-class");
74
+ });
75
+
76
+ it("applies custom className to the label wrapper (when label exists)", () => {
77
+ const onCheckedChange = vi.fn();
78
+ render(
79
+ <Switch
80
+ checked={false}
81
+ onCheckedChange={onCheckedChange}
82
+ label="Test Label"
83
+ className="wrapper-class"
84
+ />,
85
+ );
86
+ const label = screen.getByText("Test Label").closest("label");
87
+ expect(label).toHaveClass("wrapper-class");
88
+ });
89
+ });
90
+
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ // Interaction
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+
95
+ describe("Switch — Interaction", () => {
96
+ it("calls onCheckedChange with true when unchecked switch is clicked", () => {
97
+ const onCheckedChange = vi.fn();
98
+ render(
99
+ <Switch
100
+ checked={false}
101
+ onCheckedChange={onCheckedChange}
102
+ ariaLabel="Test"
103
+ />,
104
+ );
105
+ const toggle = screen.getByRole("switch");
106
+ fireEvent.click(toggle);
107
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
108
+ });
109
+
110
+ it("calls onCheckedChange with false when checked switch is clicked", () => {
111
+ const onCheckedChange = vi.fn();
112
+ render(
113
+ <Switch
114
+ checked={true}
115
+ onCheckedChange={onCheckedChange}
116
+ ariaLabel="Test"
117
+ />,
118
+ );
119
+ const toggle = screen.getByRole("switch");
120
+ fireEvent.click(toggle);
121
+ expect(onCheckedChange).toHaveBeenCalledWith(false);
122
+ });
123
+
124
+ it("does not call onCheckedChange when disabled", () => {
125
+ const onCheckedChange = vi.fn();
126
+ render(
127
+ <Switch
128
+ checked={false}
129
+ onCheckedChange={onCheckedChange}
130
+ disabled
131
+ ariaLabel="Test"
132
+ />,
133
+ );
134
+ const toggle = screen.getByRole("switch");
135
+ fireEvent.click(toggle);
136
+ expect(onCheckedChange).not.toHaveBeenCalled();
137
+ });
138
+ });
139
+
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ // Accessibility
142
+ // ─────────────────────────────────────────────────────────────────────────────
143
+
144
+ describe("Switch — Accessibility", () => {
145
+ it("has role='switch'", () => {
146
+ render(
147
+ <Switch checked={false} onCheckedChange={() => {}} ariaLabel="Test" />,
148
+ );
149
+ expect(screen.getByRole("switch")).toBeInTheDocument();
150
+ });
151
+
152
+ it("has correct aria-checked state", () => {
153
+ const { rerender } = render(
154
+ <Switch checked={false} onCheckedChange={() => {}} ariaLabel="Test" />,
155
+ );
156
+ expect(screen.getByRole("switch")).toHaveAttribute("aria-checked", "false");
157
+
158
+ rerender(
159
+ <Switch checked={true} onCheckedChange={() => {}} ariaLabel="Test" />,
160
+ );
161
+ expect(screen.getByRole("switch")).toHaveAttribute("aria-checked", "true");
162
+ });
163
+
164
+ it("has aria-disabled=true when disabled=true", () => {
165
+ render(
166
+ <Switch
167
+ disabled
168
+ checked={false}
169
+ onCheckedChange={() => {}}
170
+ ariaLabel="Test"
171
+ />,
172
+ );
173
+ expect(screen.getByRole("switch")).toHaveAttribute("aria-disabled", "true");
174
+ });
175
+
176
+ it("forwards ref to the button element", () => {
177
+ const ref = React.createRef<HTMLButtonElement>();
178
+ render(
179
+ <Switch
180
+ ref={ref}
181
+ checked={false}
182
+ onCheckedChange={() => {}}
183
+ ariaLabel="Test"
184
+ />,
185
+ );
186
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
187
+ });
188
+ });
189
+
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+ // Keyboard Interaction
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+
194
+ describe("Switch — Keyboard Interaction", () => {
195
+ it("toggles on Space key press", () => {
196
+ const onCheckedChange = vi.fn();
197
+ render(
198
+ <Switch
199
+ checked={false}
200
+ onCheckedChange={onCheckedChange}
201
+ ariaLabel="Space test"
202
+ />,
203
+ );
204
+ const toggle = screen.getByRole("switch");
205
+ toggle.focus();
206
+ fireEvent.keyDown(toggle, { key: " ", code: "Space" });
207
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
208
+ });
209
+
210
+ it("toggles on Enter key press", () => {
211
+ const onCheckedChange = vi.fn();
212
+ render(
213
+ <Switch
214
+ checked={false}
215
+ onCheckedChange={onCheckedChange}
216
+ ariaLabel="Enter test"
217
+ />,
218
+ );
219
+ const toggle = screen.getByRole("switch");
220
+ toggle.focus();
221
+ fireEvent.keyDown(toggle, { key: "Enter", code: "Enter" });
222
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
223
+ });
224
+ });
225
+
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+ // Visual Props
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+
230
+ describe("Switch — Visual Props", () => {
231
+ it("renders thumbContent when provided", () => {
232
+ render(
233
+ <Switch
234
+ checked={true}
235
+ onCheckedChange={() => {}}
236
+ thumbContent={<span data-testid="custom-icon">✓</span>}
237
+ icons={true}
238
+ ariaLabel="Icon test"
239
+ />,
240
+ );
241
+ expect(screen.getByTestId("custom-icon")).toBeInTheDocument();
242
+ });
243
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @file switch.tokens.ts
3
+ * MD3 Expressive Switch — Design tokens ported from SwitchTokens.kt.
4
+ * All dimensional values are in px (dp equivalent for web).
5
+ * @see docs/m3/switch/SwitchTokens.kt
6
+ */
7
+
8
+ /**
9
+ * Design tokens for the MD3 Expressive Switch component.
10
+ *
11
+ * Maps directly from `SwitchTokens.kt` (v0_210) to CSS/JS values.
12
+ * Use these as the single source of truth for sizing and opacity.
13
+ *
14
+ * Color tokens are NOT included here — they reference CSS custom properties
15
+ * from the project's MD3 theme system (`--md-sys-color-*`).
16
+ */
17
+ export const SwitchTokens = {
18
+ // ── Track ─────────────────────────────────────────────────────────────────
19
+ /** SwitchTokens.TrackWidth = 52dp */
20
+ trackWidth: 52,
21
+ /** SwitchTokens.TrackHeight = 32dp */
22
+ trackHeight: 32,
23
+ /** SwitchTokens.TrackOutlineWidth = 2dp */
24
+ trackOutlineWidth: 2,
25
+
26
+ // ── Handle (Thumb) ────────────────────────────────────────────────────────
27
+ /** SwitchTokens.SelectedHandleWidth/Height = 24dp */
28
+ selectedHandleSize: 24,
29
+ /** SwitchTokens.UnselectedHandleWidth/Height = 16dp */
30
+ unselectedHandleSize: 16,
31
+ /** SwitchTokens.IconHandleWidth/Height = 24dp (when thumb has icon content) */
32
+ iconHandleSize: 24,
33
+ /** SwitchTokens.PressedHandleWidth/Height = 28dp */
34
+ pressedHandleSize: 28,
35
+
36
+ // ── State Layer ───────────────────────────────────────────────────────────
37
+ /** SwitchTokens.StateLayerSize = 40dp */
38
+ stateLayerSize: 40,
39
+
40
+ // ── Icon ──────────────────────────────────────────────────────────────────
41
+ /** SwitchTokens.SelectedIconSize / UnselectedIconSize = 16dp */
42
+ iconSize: 16,
43
+
44
+ // ── Opacity (disabled states) ─────────────────────────────────────────────
45
+ /** SwitchTokens.DisabledTrackOpacity = 0.12 */
46
+ disabledTrackOpacity: 0.12,
47
+ /** SwitchTokens.DisabledSelectedHandleOpacity = 1.0 */
48
+ disabledSelectedHandleOpacity: 1.0,
49
+ /** SwitchTokens.DisabledUnselectedHandleOpacity = 0.38 */
50
+ disabledUnselectedHandleOpacity: 0.38,
51
+ /** SwitchTokens.DisabledSelectedIconOpacity = 0.38 */
52
+ disabledSelectedIconOpacity: 0.38,
53
+ /** SwitchTokens.DisabledUnselectedIconOpacity = 0.38 */
54
+ disabledUnselectedIconOpacity: 0.38,
55
+ } as const;
56
+
57
+ // ── MD3 Color token references (CSS custom properties) ────────────────────────
58
+
59
+ /**
60
+ * CSS custom property references for Switch colors.
61
+ * These map to the project's `--md-sys-color-*` tokens in `colors.css`.
62
+ *
63
+ * DO NOT hardcode hex values — always use these references so the
64
+ * component automatically adapts to light/dark theme switching.
65
+ */
66
+ export const SwitchColors = {
67
+ // Track
68
+ checkedTrack: "var(--md-sys-color-primary)",
69
+ uncheckedTrack: "var(--md-sys-color-surface-container-highest)",
70
+ uncheckedTrackOutline: "var(--md-sys-color-outline)",
71
+
72
+ // Thumb
73
+ checkedThumb: "var(--md-sys-color-on-primary)",
74
+ uncheckedThumb: "var(--md-sys-color-outline)",
75
+ hoverCheckedThumb: "var(--md-sys-color-primary-container)",
76
+ hoverUncheckedThumb: "var(--md-sys-color-on-surface-variant)",
77
+ disabledCheckedThumb: "var(--md-sys-color-surface)",
78
+
79
+ // Icon
80
+ checkedIcon: "var(--md-sys-color-on-primary-container)",
81
+ uncheckedIcon: "var(--md-sys-color-surface-container-highest)",
82
+
83
+ // State layer
84
+ checkedStateLayer: "var(--md-sys-color-primary)",
85
+ uncheckedStateLayer: "var(--md-sys-color-on-surface)",
86
+
87
+ // Focus indicator
88
+ focusIndicator: "var(--md-sys-color-secondary)",
89
+ } as const;