@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,384 @@
1
+ /**
2
+ * @file dialog.tsx
3
+ *
4
+ * Sub-system Component Dialog theo phong cách hiển thị MD3 Expressive.
5
+ *
6
+ * Được kế thừa trên nền lõi của hệ mã Radix UI Dialog primitives đi kèm gói kén Framer Motion dùng vào việc bổ sung
7
+ * nhip điệu đẩy Spring ở hướng hiện vào (Entrance) cũng như bay lên (Exit); ăn liền với bản thiết kế specs MD3 Expressive siêu quyến rũ.
8
+ * Phục vụ cả ở chế độ Standard (Tiêu chuẩn lọt thỏm) và phiên bản Full-Screen tràn viền toàn diện.
9
+ *
10
+ * @see https://m3.material.io/components/dialogs/overview
11
+ */
12
+
13
+ import * as RadixDialog from "@radix-ui/react-dialog";
14
+ import { AnimatePresence, m } from "motion/react";
15
+ import * as React from "react";
16
+ import { cn } from "../lib/utils";
17
+ import { Icon } from "./icon";
18
+ import { IconButton } from "./icon-button";
19
+ import { ScrollArea } from "./scroll-area";
20
+
21
+ // ─── MD3 Spring Config (Expressive) ──────────────────────────────────────────
22
+ const MD3_SPRING = {
23
+ type: "spring" as const,
24
+ stiffness: 400,
25
+ damping: 30,
26
+ mass: 1,
27
+ };
28
+
29
+ const MD3_OVERLAY_ANIM = {
30
+ initial: { opacity: 0 },
31
+ animate: {
32
+ opacity: 1,
33
+ transition: { duration: 0.2, ease: "easeOut" as const },
34
+ },
35
+ exit: { opacity: 0, transition: { duration: 0.15, ease: "easeIn" as const } },
36
+ };
37
+
38
+ const MD3_CONTENT_ANIM = {
39
+ initial: { opacity: 0, scale: 0.85, y: 24 },
40
+ animate: { opacity: 1, scale: 1, y: 0, transition: MD3_SPRING },
41
+ exit: {
42
+ opacity: 0,
43
+ scale: 0.95,
44
+ y: 8,
45
+ transition: { duration: 0.15, ease: "easeIn" as const },
46
+ },
47
+ };
48
+
49
+ const MD3_FULLSCREEN_ANIM = {
50
+ initial: { y: "100%" },
51
+ animate: { y: 0, transition: MD3_SPRING },
52
+ exit: { y: "100%", transition: { duration: 0.2, ease: "easeIn" as const } },
53
+ };
54
+
55
+ // ─── Types ────────────────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Thuộc tính của cội gốc Root `Dialog`. Chức năng làm gương phản hồi của Radix `Dialog.Root` qua dạng controlled state đóng hay mở.
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const [open, setOpen] = React.useState(false);
63
+ *
64
+ * <Dialog open={open} onOpenChange={setOpen}>
65
+ * <DialogTrigger asChild>
66
+ * <Button>Bấm Mở Dialog</Button>
67
+ * </DialogTrigger>
68
+ * <DialogContent>
69
+ * <DialogHeader>
70
+ * <DialogTitle>Bạn có muốn phiêu lưu không?</DialogTitle>
71
+ * </DialogHeader>
72
+ * <DialogBody>Chuẩn bị lên đồ rời khỏi hang nào.</DialogBody>
73
+ * <DialogFooter>
74
+ * <Button onClick={() => setOpen(false)} variant="text">Đóng</Button>
75
+ * </DialogFooter>
76
+ * </DialogContent>
77
+ * </Dialog>
78
+ * ```
79
+ */
80
+ export interface DialogProps {
81
+ /** Whether the dialog is open (controlled). Omit for uncontrolled. */
82
+ open?: boolean;
83
+ /** Called when the open state should change. */
84
+ onOpenChange?: (open: boolean) => void;
85
+ /** Dialog trigger + content. */
86
+ children: React.ReactNode;
87
+ }
88
+
89
+ /**
90
+ * Các props được tiêm vào component bao ngoài Container `DialogContent` thuộc dạng Normal Standard.
91
+ *
92
+ * @see {@link DialogContent}
93
+ */
94
+ export interface DialogContentProps
95
+ extends React.ComponentPropsWithoutRef<typeof RadixDialog.Content> {
96
+ /** Vô hình đi đi nút Close dấu (X) góc phải trên. @default false */
97
+ hideCloseButton?: boolean;
98
+ className?: string;
99
+ }
100
+
101
+ /**
102
+ * Thuộc tính Props của biến thể `DialogFullScreenContent` chuyên dụng dành riêng cho Mode Full-Screen tràn màn hình.
103
+ *
104
+ * @remarks
105
+ * Những hộp thoại Full-screen có biệt tài tự nới rộng và xâm chiếm cả bề ngang dọc nguyên thiết bị. Nó còn thiết lập một đường Top App bar (thanh ngang đỉnh)
106
+ * kẹp chung cả 1 nhãn title mô tả đỉnh, một nút icon X dẹp ở rìa, thêm luôn hẳn cái nút Confirm cực xịn xò.
107
+ *
108
+ * @see {@link DialogFullScreenContent}
109
+ * @see https://m3.material.io/components/dialogs/guidelines#full-screen
110
+ */
111
+ export interface DialogFullScreenContentProps
112
+ extends React.ComponentPropsWithoutRef<typeof RadixDialog.Content> {
113
+ /** Nhãn Title nằm vùng khu vực thanh ngang Top Bar. */
114
+ title?: string;
115
+ /** Chữ viết đính kèm bên trong cục Nút nhấn thao tác ngay trên góc Top App bar đó (VD: "Lưu lại", "Save"). */
116
+ actionLabel?: string;
117
+ /** Hàm handler phát động cờ để kích chạy tính năng lưu, xác nhận kia. */
118
+ onAction?: () => void;
119
+ /** Rạch một làn kẻ chia cách thân body nội dung cuộn bên dưới và dòng App bar cố thủ bên trên. @default false */
120
+ showDivider?: boolean;
121
+ className?: string;
122
+ }
123
+
124
+ // ─── Re-exports wrapper ───────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Gốc rễ Root của Component Dialog — trạc lấp lên cái module `Dialog.Root` của nhà Radix.
128
+ *
129
+ * @remarks Cơ năng hoạt động ở chế độ Controlled (kiểm soát vòng đời ở Client) xài mảng `open`/`onOpenChange`.
130
+ * Tuy thế bạn nếu không thích thì đừng có xài truyền mấy cái Props tay đôi trên, mà bỏ xài kiểu Untracked thông qua cái cục `DialogTrigger` là đủ.
131
+ */
132
+ const Dialog = ({ open, onOpenChange, children }: DialogProps) => (
133
+ <RadixDialog.Root open={open} onOpenChange={onOpenChange}>
134
+ {children}
135
+ </RadixDialog.Root>
136
+ );
137
+ Dialog.displayName = "Dialog";
138
+
139
+ /** Bộ Trigger gieo phát đà giúp tắt bật cờ State đóng mở của Dialog con, dùng kèm kĩ năng nhét thông qua cái cầu `asChild`. */
140
+ const DialogTrigger = RadixDialog.Trigger;
141
+ DialogTrigger.displayName = "DialogTrigger";
142
+
143
+ // ─── Portal + Overlay + Content ───────────────────────────────────────────────
144
+ const DialogPortal = ({
145
+ open,
146
+ children,
147
+ }: {
148
+ open?: boolean;
149
+ children: React.ReactNode;
150
+ }) => (
151
+ <RadixDialog.Portal forceMount>
152
+ <AnimatePresence>
153
+ {open ? React.Children.toArray(children) : null}
154
+ </AnimatePresence>
155
+ </RadixDialog.Portal>
156
+ );
157
+ DialogPortal.displayName = "DialogPortal";
158
+
159
+ const DialogOverlay = React.forwardRef<
160
+ React.ComponentRef<typeof RadixDialog.Overlay>,
161
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Overlay>
162
+ >(({ className, ...props }, ref) => (
163
+ <RadixDialog.Overlay ref={ref} asChild {...props}>
164
+ <m.div
165
+ className={cn("fixed inset-0 z-50 bg-black/32", className)}
166
+ aria-hidden="true"
167
+ {...MD3_OVERLAY_ANIM}
168
+ />
169
+ </RadixDialog.Overlay>
170
+ ));
171
+ DialogOverlay.displayName = "DialogOverlay";
172
+
173
+ const DialogContent = React.forwardRef<
174
+ React.ComponentRef<typeof RadixDialog.Content>,
175
+ DialogContentProps
176
+ >(({ className, children, hideCloseButton = false, ...props }, ref) => (
177
+ <RadixDialog.Content
178
+ ref={ref}
179
+ asChild
180
+ aria-describedby={undefined}
181
+ {...props}
182
+ >
183
+ <m.div
184
+ className={cn(
185
+ "fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
186
+ "w-[calc(100%-2rem)] max-w-140",
187
+ "rounded-[28px] bg-m3-surface-container-high p-6",
188
+ "shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-m3-primary",
189
+ className,
190
+ )}
191
+ role="dialog"
192
+ {...MD3_CONTENT_ANIM}
193
+ >
194
+ {children}
195
+ {!hideCloseButton && (
196
+ <RadixDialog.Close asChild aria-label="Close dialog">
197
+ <IconButton
198
+ size="sm"
199
+ colorStyle="filled"
200
+ className="absolute right-4 top-4"
201
+ aria-label="Close"
202
+ >
203
+ <Icon name="close" aria-hidden="true" />
204
+ </IconButton>
205
+ </RadixDialog.Close>
206
+ )}
207
+ </m.div>
208
+ </RadixDialog.Content>
209
+ ));
210
+ DialogContent.displayName = "DialogContent";
211
+
212
+ // ─── Header & Related ────────────────────────────────────────────────────────
213
+ const DialogIcon = React.forwardRef<
214
+ HTMLDivElement,
215
+ React.HTMLAttributes<HTMLDivElement>
216
+ >(({ className, children, ...props }, ref) => (
217
+ <div
218
+ ref={ref}
219
+ className={cn("flex justify-center mb-4 text-m3-secondary", className)}
220
+ aria-hidden="true"
221
+ {...props}
222
+ >
223
+ {children}
224
+ </div>
225
+ ));
226
+ DialogIcon.displayName = "DialogIcon";
227
+
228
+ const DialogHeader = ({
229
+ className,
230
+ ...props
231
+ }: React.HTMLAttributes<HTMLDivElement>) => (
232
+ <div className={cn("flex flex-col gap-2 mb-4", className)} {...props} />
233
+ );
234
+ DialogHeader.displayName = "DialogHeader";
235
+
236
+ // RadixDialog.Title surfaces to the accessibility tree — required
237
+ const DialogTitle = React.forwardRef<
238
+ React.ComponentRef<typeof RadixDialog.Title>,
239
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Title>
240
+ >(({ className, asChild, ...props }, ref) => (
241
+ <RadixDialog.Title
242
+ ref={ref}
243
+ asChild={asChild}
244
+ className={cn(
245
+ "text-[24px] leading-8 font-normal text-m3-on-surface tracking-[0em]",
246
+ className,
247
+ )}
248
+ {...props}
249
+ />
250
+ ));
251
+ DialogTitle.displayName = "DialogTitle";
252
+
253
+ const DialogDescription = React.forwardRef<
254
+ React.ComponentRef<typeof RadixDialog.Description>,
255
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Description>
256
+ >(({ className, asChild, ...props }, ref) => (
257
+ <RadixDialog.Description
258
+ ref={ref}
259
+ asChild={asChild}
260
+ className={cn("text-sm text-m3-on-surface-variant leading-5", className)}
261
+ {...props}
262
+ />
263
+ ));
264
+ DialogDescription.displayName = "DialogDescription";
265
+
266
+ // ─── Body & Footer ───────────────────────────────────────────────────────────
267
+ const DialogBody = React.forwardRef<
268
+ HTMLDivElement,
269
+ React.HTMLAttributes<HTMLDivElement>
270
+ >(({ className, children, dir, ...props }, ref) => (
271
+ <ScrollArea
272
+ ref={ref}
273
+ type="hover"
274
+ dir={dir as "ltr" | "rtl" | undefined}
275
+ className={cn("max-h-[calc(85dvh-200px)] -mx-6", className)}
276
+ viewportClassName="px-6"
277
+ {...props}
278
+ >
279
+ {children}
280
+ </ScrollArea>
281
+ ));
282
+ DialogBody.displayName = "DialogBody";
283
+
284
+ const DialogFooter = ({
285
+ className,
286
+ ...props
287
+ }: React.HTMLAttributes<HTMLDivElement>) => (
288
+ <div
289
+ className={cn("flex flex-row justify-end gap-2 mt-6", className)}
290
+ {...props}
291
+ />
292
+ );
293
+ DialogFooter.displayName = "DialogFooter";
294
+
295
+ const DialogClose = RadixDialog.Close;
296
+
297
+ // ─── Full Screen Content Variant ──────────────────────────────────────────────
298
+ const DialogFullScreenContent = React.forwardRef<
299
+ React.ComponentRef<typeof RadixDialog.Content>,
300
+ DialogFullScreenContentProps
301
+ >(
302
+ (
303
+ {
304
+ className,
305
+ children,
306
+ title,
307
+ actionLabel,
308
+ onAction,
309
+ showDivider,
310
+ ...props
311
+ },
312
+ ref,
313
+ ) => (
314
+ <RadixDialog.Content
315
+ ref={ref}
316
+ asChild
317
+ aria-describedby={undefined}
318
+ {...props}
319
+ >
320
+ <m.div
321
+ className={cn(
322
+ "fixed inset-0 z-50 w-full h-full bg-m3-surface flex flex-col",
323
+ "outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-m3-primary",
324
+ className,
325
+ )}
326
+ role="dialog"
327
+ {...MD3_FULLSCREEN_ANIM}
328
+ >
329
+ <div className="flex shrink-0 items-center px-4 h-14 gap-2 bg-m3-surface">
330
+ <RadixDialog.Close asChild aria-label="Close dialog">
331
+ <IconButton size="sm" colorStyle="filled" aria-label="Close">
332
+ <Icon name="close" aria-hidden="true" />
333
+ </IconButton>
334
+ </RadixDialog.Close>
335
+
336
+ {title && (
337
+ <DialogTitle className="flex-1 text-[22px] leading-7 font-medium truncate pr-2">
338
+ {title}
339
+ </DialogTitle>
340
+ )}
341
+
342
+ {actionLabel && onAction && (
343
+ <button
344
+ type="button"
345
+ onClick={onAction}
346
+ className="text-sm font-medium text-m3-primary px-3 py-2 rounded-full hover:bg-m3-primary/8 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-m3-primary transition-colors whitespace-nowrap"
347
+ >
348
+ {actionLabel}
349
+ </button>
350
+ )}
351
+ </div>
352
+
353
+ {showDivider && (
354
+ <hr className="border-m3-outline-variant w-full shrink-0 m-0" />
355
+ )}
356
+
357
+ <ScrollArea
358
+ type="hover"
359
+ className="flex-1 w-full"
360
+ viewportClassName="p-6"
361
+ >
362
+ {children}
363
+ </ScrollArea>
364
+ </m.div>
365
+ </RadixDialog.Content>
366
+ ),
367
+ );
368
+ DialogFullScreenContent.displayName = "DialogFullScreenContent";
369
+
370
+ export {
371
+ Dialog,
372
+ DialogBody,
373
+ DialogClose,
374
+ DialogContent,
375
+ DialogDescription,
376
+ DialogFooter,
377
+ DialogFullScreenContent,
378
+ DialogHeader,
379
+ DialogIcon,
380
+ DialogOverlay,
381
+ DialogPortal,
382
+ DialogTitle,
383
+ DialogTrigger,
384
+ };
@@ -0,0 +1,314 @@
1
+ "use client";
2
+
3
+ import { render, screen } from "@testing-library/react";
4
+ import * as MotionReact from "motion/react";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { buildWavePath, Divider } from "./divider";
7
+
8
+ // Mock motion/react — same pattern as badge.test.tsx / chip.test.tsx
9
+ vi.mock("motion/react", async (importOriginal) => {
10
+ const actual = await importOriginal<typeof import("motion/react")>();
11
+ return {
12
+ ...actual,
13
+ useReducedMotion: () => false,
14
+ };
15
+ });
16
+
17
+ // ── Test Suites ───────────────────────────────────────────────────────────────
18
+
19
+ describe("Divider", () => {
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ // ── Core Accessibility ──────────────────────────────────────────────────────
25
+
26
+ describe("core accessibility", () => {
27
+ it("renders with role='separator' by default", () => {
28
+ render(<Divider />);
29
+ expect(screen.getByRole("separator")).toBeInTheDocument();
30
+ });
31
+
32
+ it("has aria-orientation='horizontal' by default", () => {
33
+ render(<Divider />);
34
+ expect(screen.getByRole("separator")).toHaveAttribute(
35
+ "aria-orientation",
36
+ "horizontal",
37
+ );
38
+ });
39
+
40
+ it("vertical orientation sets aria-orientation='vertical'", () => {
41
+ render(<Divider orientation="vertical" />);
42
+ expect(screen.getByRole("separator")).toHaveAttribute(
43
+ "aria-orientation",
44
+ "vertical",
45
+ );
46
+ });
47
+
48
+ it("decorative=true adds aria-hidden='true'", () => {
49
+ const { container } = render(<Divider decorative />);
50
+ const divider = container.firstChild as HTMLElement;
51
+ expect(divider).toHaveAttribute("aria-hidden", "true");
52
+ });
53
+
54
+ it("decorative=true removes role='separator'", () => {
55
+ render(<Divider decorative />);
56
+ expect(screen.queryByRole("separator")).toBeNull();
57
+ });
58
+ });
59
+
60
+ // ── Variants ────────────────────────────────────────────────────────────────
61
+
62
+ describe("variants", () => {
63
+ it("full-bleed → no indent classes", () => {
64
+ render(<Divider variant="full-bleed" />);
65
+ const el = screen.getByRole("separator");
66
+ expect(el.className).not.toMatch(/ml-|mx-|mt-|my-/);
67
+ });
68
+
69
+ it("inset + insetStart='standard' → has class 'ml-4'", () => {
70
+ render(<Divider variant="inset" insetStart="standard" />);
71
+ const el = screen.getByRole("separator");
72
+ expect(el.className).toContain("ml-4");
73
+ });
74
+
75
+ it("inset + insetStart='icon' → has class 'ml-[72px]'", () => {
76
+ render(<Divider variant="inset" insetStart="icon" />);
77
+ const el = screen.getByRole("separator");
78
+ expect(el.className).toContain("ml-[72px]");
79
+ });
80
+
81
+ it("middle-inset → has class 'mx-4'", () => {
82
+ render(<Divider variant="middle-inset" />);
83
+ const el = screen.getByRole("separator");
84
+ expect(el.className).toContain("mx-4");
85
+ });
86
+
87
+ it("subheader → no indent classes (same as full-bleed)", () => {
88
+ render(<Divider variant="subheader" />);
89
+ const el = screen.getByRole("separator");
90
+ expect(el.className).not.toMatch(/ml-|mx-|mt-|my-/);
91
+ });
92
+ });
93
+
94
+ // ── Shape: flat ─────────────────────────────────────────────────────────────
95
+
96
+ describe("shape: flat", () => {
97
+ it("renders a div element (not svg)", () => {
98
+ const { container } = render(<Divider shape="flat" />);
99
+ // LazyMotion wrapper is present; the animated element is a div
100
+ const svg = container.querySelector("svg");
101
+ expect(svg).toBeNull();
102
+ });
103
+
104
+ it("has class 'bg-m3-outline-variant'", () => {
105
+ render(<Divider shape="flat" />);
106
+ const el = screen.getByRole("separator");
107
+ expect(el.className).toContain("bg-m3-outline-variant");
108
+ });
109
+
110
+ it("horizontal → has class 'h-px'", () => {
111
+ render(<Divider shape="flat" orientation="horizontal" />);
112
+ const el = screen.getByRole("separator");
113
+ expect(el.className).toContain("h-px");
114
+ });
115
+
116
+ it("vertical → has class 'w-px'", () => {
117
+ render(<Divider shape="flat" orientation="vertical" />);
118
+ const el = screen.getByRole("separator");
119
+ expect(el.className).toContain("w-px");
120
+ });
121
+ });
122
+
123
+ // ── Shape: wavy ─────────────────────────────────────────────────────────────
124
+
125
+ describe("shape: wavy", () => {
126
+ it("renders an svg element", () => {
127
+ const { container } = render(<Divider shape="wavy" />);
128
+ const svg = container.querySelector("svg");
129
+ expect(svg).toBeInTheDocument();
130
+ });
131
+
132
+ it("svg contains a path element with d attribute starting with 'M'", () => {
133
+ const { container } = render(<Divider shape="wavy" />);
134
+ const path = container.querySelector("path");
135
+ expect(path).toBeInTheDocument();
136
+ expect(path?.getAttribute("d")).toMatch(/^M/);
137
+ });
138
+
139
+ it("wavy path has strokeLinecap='round' for rounded ends", () => {
140
+ const { container } = render(<Divider shape="wavy" />);
141
+ const path = container.querySelector("path");
142
+ expect(path?.getAttribute("stroke-linecap")).toBe("round");
143
+ });
144
+
145
+ it("wavy renders direct <path> — no <pattern> element", () => {
146
+ const { container } = render(<Divider shape="wavy" />);
147
+ expect(container.querySelector("pattern")).toBeNull();
148
+ expect(container.querySelector("defs")).toBeNull();
149
+ });
150
+
151
+ it("wavy + vertical → falls back to flat (no svg rendered)", () => {
152
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
153
+ const { container } = render(
154
+ <Divider shape="wavy" orientation="vertical" />,
155
+ );
156
+ const svg = container.querySelector("svg");
157
+ expect(svg).toBeNull();
158
+ warnSpy.mockRestore();
159
+ });
160
+
161
+ it("wavy + vertical → emits console.warn", () => {
162
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
163
+ render(<Divider shape="wavy" orientation="vertical" />);
164
+ expect(warnSpy).toHaveBeenCalledWith(
165
+ expect.stringContaining(
166
+ "shape='wavy' is not supported with orientation='vertical'",
167
+ ),
168
+ );
169
+ warnSpy.mockRestore();
170
+ });
171
+
172
+ it("custom waveConfig → renders without error", () => {
173
+ expect(() => {
174
+ render(
175
+ <Divider
176
+ shape="wavy"
177
+ waveConfig={{ amplitude: 5, wavelength: 24 }}
178
+ />,
179
+ );
180
+ }).not.toThrow();
181
+ });
182
+ });
183
+
184
+ // ── buildWavePath Helper ────────────────────────────────────────────────────
185
+
186
+ describe("buildWavePath helper", () => {
187
+ it("returns a string starting with 'M {startX},{yCenter}'", () => {
188
+ // New signature: buildWavePath(startX, endX, amplitude, wavelength, yCenter)
189
+ const d = buildWavePath(0, 64, 3, 16, 4);
190
+ expect(d).toMatch(/^M 0,4/);
191
+ });
192
+
193
+ it("respects non-zero startX", () => {
194
+ const d = buildWavePath(0.5, 100, 2, 32, 4);
195
+ expect(d).toMatch(/^M 0\.5,4/);
196
+ });
197
+
198
+ it("contains 'C' (cubic Bézier curves)", () => {
199
+ const d = buildWavePath(0, 64, 3, 16, 4);
200
+ expect(d).toContain("C");
201
+ });
202
+
203
+ it("produces path with monotonically increasing X coordinates", () => {
204
+ const d = buildWavePath(0, 64, 3, 16, 4);
205
+ const parts = d.replace(/[MC]/g, " ").trim().split(/\s+/);
206
+ const xValues: number[] = [];
207
+ for (let i = 0; i < parts.length; i += 2) {
208
+ const x = parseFloat(parts[i] ?? "");
209
+ if (!Number.isNaN(x)) xValues.push(x);
210
+ }
211
+ expect(xValues.length).toBeGreaterThan(0);
212
+ expect(xValues[xValues.length - 1]).toBeGreaterThan(xValues[0] ?? 0);
213
+ });
214
+
215
+ it("returns empty string when startX >= endX", () => {
216
+ expect(buildWavePath(0, 0)).toBe("");
217
+ expect(buildWavePath(10, 5)).toBe("");
218
+ expect(buildWavePath(0, -10)).toBe("");
219
+ });
220
+
221
+ it("respects custom amplitude and wavelength", () => {
222
+ const d = buildWavePath(0, 32, 5, 8, 5);
223
+ expect(d).toMatch(/^M 0,5/);
224
+ });
225
+
226
+ it("path ends exactly at endX", () => {
227
+ const d = buildWavePath(0, 64, 2, 32, 4);
228
+ // Last coordinate pair in the path should be endX,yCenter
229
+ const match = d.match(/(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)$/);
230
+ expect(Number(match?.[1])).toBe(64);
231
+ expect(Number(match?.[2])).toBe(4);
232
+ });
233
+ });
234
+
235
+ // ── Props Forwarding ────────────────────────────────────────────────────────
236
+
237
+ describe("props forwarding", () => {
238
+ it("custom className is merged via cn()", () => {
239
+ render(<Divider className="my-custom-divider" />);
240
+ const el = screen.getByRole("separator");
241
+ expect(el).toHaveClass("my-custom-divider");
242
+ // Also retains base classes
243
+ expect(el.className).toContain("bg-m3-outline-variant");
244
+ });
245
+
246
+ it("style prop is forwarded to the root element", () => {
247
+ render(<Divider style={{ opacity: 0.5 }} />);
248
+ const el = screen.getByRole("separator");
249
+ expect(el.style.opacity).toBe("0.5");
250
+ });
251
+
252
+ it("ref forwarding works for flat divider (div element)", () => {
253
+ const ref = { current: null };
254
+ render(<Divider ref={ref} />);
255
+ expect(ref.current).not.toBeNull();
256
+ expect((ref.current as unknown as HTMLElement).tagName).toBe("DIV");
257
+ });
258
+
259
+ it("ref forwarding works for wavy divider (outer div element)", () => {
260
+ const ref = { current: null };
261
+ render(<Divider ref={ref} shape="wavy" />);
262
+ expect(ref.current).not.toBeNull();
263
+ // Wavy forwards ref to the outer wrapper div
264
+ expect((ref.current as unknown as HTMLElement).tagName).toBe("DIV");
265
+ });
266
+ });
267
+
268
+ // ── Reduced Motion ──────────────────────────────────────────────────────────
269
+
270
+ describe("prefers-reduced-motion", () => {
271
+ it("renders correctly when useReducedMotion returns true", () => {
272
+ vi.spyOn(MotionReact, "useReducedMotion").mockReturnValue(true);
273
+ render(<Divider />);
274
+ expect(screen.getByRole("separator")).toBeInTheDocument();
275
+ });
276
+
277
+ it("wavy renders correctly when useReducedMotion returns true", () => {
278
+ vi.spyOn(MotionReact, "useReducedMotion").mockReturnValue(true);
279
+ const { container } = render(<Divider shape="wavy" />);
280
+ expect(container.querySelector("svg")).toBeInTheDocument();
281
+ });
282
+ });
283
+
284
+ // ── Snapshots ───────────────────────────────────────────────────────────────
285
+
286
+ describe("snapshots", () => {
287
+ it("flat/horizontal (default)", () => {
288
+ const { container } = render(<Divider />);
289
+ expect(container.firstChild).toMatchSnapshot();
290
+ });
291
+
292
+ it("flat/vertical", () => {
293
+ const { container } = render(<Divider orientation="vertical" />);
294
+ expect(container.firstChild).toMatchSnapshot();
295
+ });
296
+
297
+ it("wavy", () => {
298
+ const { container } = render(<Divider shape="wavy" />);
299
+ expect(container.firstChild).toMatchSnapshot();
300
+ });
301
+
302
+ it("inset", () => {
303
+ const { container } = render(
304
+ <Divider variant="inset" insetStart="standard" />,
305
+ );
306
+ expect(container.firstChild).toMatchSnapshot();
307
+ });
308
+
309
+ it("middle-inset", () => {
310
+ const { container } = render(<Divider variant="middle-inset" />);
311
+ expect(container.firstChild).toMatchSnapshot();
312
+ });
313
+ });
314
+ });