@bug-on/md3-react 2.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/CHANGELOG.md +69 -0
  3. package/dist/index.css +178 -0
  4. package/dist/index.css.d.ts +2 -0
  5. package/dist/index.d.mts +6135 -0
  6. package/dist/index.d.ts +6135 -71
  7. package/dist/index.js +1688 -631
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1600 -564
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/material-symbols-cdn.css.d.ts +2 -0
  12. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  13. package/dist/plugin.d.mts +1 -0
  14. package/dist/plugin.d.ts +1 -0
  15. package/dist/plugin.js +13 -0
  16. package/dist/plugin.js.map +1 -0
  17. package/dist/plugin.mjs +3 -0
  18. package/dist/plugin.mjs.map +1 -0
  19. package/dist/typography.css.d.ts +2 -0
  20. package/package.json +28 -19
  21. package/scripts/copy-assets.js +115 -0
  22. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  23. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  24. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  25. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  26. package/src/assets/loading-indicator.svg +19 -0
  27. package/src/assets/material-symbols-cdn.css +65 -0
  28. package/src/assets/material-symbols-self-hosted.css +90 -0
  29. package/src/css.d.ts +20 -0
  30. package/src/hooks/useClickOutside.ts +37 -0
  31. package/src/hooks/useMediaQuery.ts +28 -0
  32. package/src/hooks/useRipple.ts +88 -0
  33. package/src/index.css +23 -0
  34. package/src/index.ts +349 -0
  35. package/src/lib/material-symbols-preconnect.tsx +82 -0
  36. package/src/lib/theme-utils.ts +195 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/plugin.ts +12 -0
  39. package/src/test/button.test.tsx +59 -0
  40. package/src/test/icon.test.tsx +91 -0
  41. package/src/test/loading-indicator.test.tsx +128 -0
  42. package/src/test/progress-indicator.test.tsx +306 -0
  43. package/src/test/setup.ts +80 -0
  44. package/src/test/typography.test.tsx +206 -0
  45. package/src/types/index.ts +7 -0
  46. package/src/types/md3.ts +31 -0
  47. package/src/ui/Text.tsx +60 -0
  48. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  49. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  50. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  51. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  52. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  53. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  54. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  55. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  56. package/src/ui/app-bar/app-bar.types.ts +441 -0
  57. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  58. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  59. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  60. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  61. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  62. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  63. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  64. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  65. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  66. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  67. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  68. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  69. package/src/ui/app-bar/search-view.tsx +227 -0
  70. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  71. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  72. package/src/ui/badge.test.tsx +345 -0
  73. package/src/ui/badge.tsx +282 -0
  74. package/src/ui/button-group.test.tsx +71 -0
  75. package/src/ui/button-group.tsx +350 -0
  76. package/src/ui/button.test.tsx +306 -0
  77. package/src/ui/button.tsx +665 -0
  78. package/src/ui/card.test.tsx +187 -0
  79. package/src/ui/card.tsx +259 -0
  80. package/src/ui/checkbox.test.tsx +423 -0
  81. package/src/ui/checkbox.tsx +525 -0
  82. package/src/ui/chip.test.tsx +292 -0
  83. package/src/ui/chip.tsx +548 -0
  84. package/src/ui/code-block.tsx +219 -0
  85. package/src/ui/dialog.test.tsx +300 -0
  86. package/src/ui/dialog.tsx +384 -0
  87. package/src/ui/divider.test.tsx +314 -0
  88. package/src/ui/divider.tsx +412 -0
  89. package/src/ui/drawer.tsx +240 -0
  90. package/src/ui/fab-menu.test.tsx +494 -0
  91. package/src/ui/fab-menu.tsx +739 -0
  92. package/src/ui/fab.test.tsx +232 -0
  93. package/src/ui/fab.tsx +505 -0
  94. package/src/ui/icon-button.test.tsx +515 -0
  95. package/src/ui/icon-button.tsx +525 -0
  96. package/src/ui/icon.test.tsx +197 -0
  97. package/src/ui/icon.tsx +179 -0
  98. package/src/ui/loading-indicator.test.tsx +73 -0
  99. package/src/ui/loading-indicator.tsx +312 -0
  100. package/src/ui/menu/context-menu.tsx +275 -0
  101. package/src/ui/menu/index.ts +77 -0
  102. package/src/ui/menu/menu-animations.ts +102 -0
  103. package/src/ui/menu/menu-context.tsx +99 -0
  104. package/src/ui/menu/menu-divider.tsx +47 -0
  105. package/src/ui/menu/menu-group.tsx +200 -0
  106. package/src/ui/menu/menu-item.tsx +294 -0
  107. package/src/ui/menu/menu-tokens.ts +208 -0
  108. package/src/ui/menu/menu-types.ts +313 -0
  109. package/src/ui/menu/menu.test.tsx +624 -0
  110. package/src/ui/menu/menu.tsx +289 -0
  111. package/src/ui/menu/sub-menu.tsx +223 -0
  112. package/src/ui/menu/vertical-menu.tsx +382 -0
  113. package/src/ui/navigation-rail.test.tsx +404 -0
  114. package/src/ui/navigation-rail.tsx +607 -0
  115. package/src/ui/progress-indicator/circular.tsx +248 -0
  116. package/src/ui/progress-indicator/hooks.ts +51 -0
  117. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  118. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  119. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  120. package/src/ui/progress-indicator/linear.tsx +143 -0
  121. package/src/ui/progress-indicator/types.ts +158 -0
  122. package/src/ui/progress-indicator/utils.ts +73 -0
  123. package/src/ui/radio-button.test.tsx +407 -0
  124. package/src/ui/radio-button.tsx +551 -0
  125. package/src/ui/ripple.test.tsx +72 -0
  126. package/src/ui/ripple.tsx +234 -0
  127. package/src/ui/scroll-area.test.tsx +58 -0
  128. package/src/ui/scroll-area.tsx +139 -0
  129. package/src/ui/search/animated-placeholder.tsx +145 -0
  130. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  131. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  132. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  133. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  134. package/src/ui/search/index.ts +44 -0
  135. package/src/ui/search/search-bar.tsx +220 -0
  136. package/src/ui/search/search-context.tsx +42 -0
  137. package/src/ui/search/search-view-docked.tsx +194 -0
  138. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  139. package/src/ui/search/search.test.tsx +233 -0
  140. package/src/ui/search/search.tokens.ts +134 -0
  141. package/src/ui/search/search.tsx +131 -0
  142. package/src/ui/search/search.types.ts +154 -0
  143. package/src/ui/search/trailing-action.tsx +49 -0
  144. package/src/ui/shared/constants.ts +135 -0
  145. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  146. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  147. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  148. package/src/ui/slider/range-slider.tsx +561 -0
  149. package/src/ui/slider/slider-thumb.tsx +379 -0
  150. package/src/ui/slider/slider-track.tsx +912 -0
  151. package/src/ui/slider/slider.tokens.ts +189 -0
  152. package/src/ui/slider/slider.tsx +259 -0
  153. package/src/ui/slider/slider.types.ts +288 -0
  154. package/src/ui/snackbar/index.ts +20 -0
  155. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  156. package/src/ui/snackbar/snackbar.tsx +476 -0
  157. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  158. package/src/ui/switch/switch.stories.tsx +309 -0
  159. package/src/ui/switch/switch.test.tsx +243 -0
  160. package/src/ui/switch/switch.tokens.ts +89 -0
  161. package/src/ui/switch/switch.tsx +504 -0
  162. package/src/ui/switch/switch.types.ts +62 -0
  163. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  164. package/src/ui/tabs/tab.tsx +407 -0
  165. package/src/ui/tabs/tabs-content.tsx +89 -0
  166. package/src/ui/tabs/tabs-list.tsx +146 -0
  167. package/src/ui/tabs/tabs.test.tsx +290 -0
  168. package/src/ui/tabs/tabs.tokens.ts +121 -0
  169. package/src/ui/tabs/tabs.tsx +229 -0
  170. package/src/ui/tabs/tabs.types.ts +185 -0
  171. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  172. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  173. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  174. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  175. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  176. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  177. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  178. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  179. package/src/ui/text-field/text-field.test.tsx +454 -0
  180. package/src/ui/text-field/text-field.tokens.ts +104 -0
  181. package/src/ui/text-field/text-field.tsx +548 -0
  182. package/src/ui/text-field/text-field.types.ts +180 -0
  183. package/src/ui/theme-provider/index.tsx +215 -0
  184. package/src/ui/toc.test.tsx +108 -0
  185. package/src/ui/toc.tsx +172 -0
  186. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  187. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  188. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  189. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  190. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  191. package/src/ui/tooltip/tooltip.types.ts +70 -0
  192. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  193. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  194. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  195. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  196. package/src/ui/typography/type-scale-tokens.ts +205 -0
  197. package/src/ui/typography/typography-key-tokens.ts +43 -0
  198. package/src/ui/typography/typography-tokens.ts +360 -0
  199. package/src/ui/typography/typography.css +22 -0
  200. package/src/ui/typography/typography.tsx +559 -0
  201. package/test-render.tsx +4 -0
  202. package/test-shadow.html +26 -0
  203. package/test_output.txt +164 -0
  204. package/test_output_v2.txt +5 -0
  205. package/tsconfig.build.json +10 -0
  206. package/tsconfig.json +18 -0
  207. package/tsup.config.ts +20 -0
  208. package/vitest.config.ts +11 -0
  209. package/dist/hooks/useClickOutside.d.ts +0 -8
  210. package/dist/hooks/useMediaQuery.d.ts +0 -11
  211. package/dist/hooks/useRipple.d.ts +0 -26
  212. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  213. package/dist/lib/theme-utils.d.ts +0 -63
  214. package/dist/lib/utils.d.ts +0 -2
  215. package/dist/types/index.d.ts +0 -1
  216. package/dist/types/md3.d.ts +0 -14
  217. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  218. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  219. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  220. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  221. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  222. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  223. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  224. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  225. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  226. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  227. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  228. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  229. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  230. package/dist/ui/app-bar/search-view.d.ts +0 -54
  231. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  232. package/dist/ui/badge.d.ts +0 -125
  233. package/dist/ui/button-group.d.ts +0 -59
  234. package/dist/ui/button.d.ts +0 -148
  235. package/dist/ui/card.d.ts +0 -62
  236. package/dist/ui/checkbox.d.ts +0 -82
  237. package/dist/ui/chip.d.ts +0 -110
  238. package/dist/ui/code-block.d.ts +0 -14
  239. package/dist/ui/dialog.d.ts +0 -111
  240. package/dist/ui/divider.d.ts +0 -164
  241. package/dist/ui/drawer.d.ts +0 -39
  242. package/dist/ui/dropdown.d.ts +0 -29
  243. package/dist/ui/fab-menu.d.ts +0 -204
  244. package/dist/ui/fab.d.ts +0 -162
  245. package/dist/ui/icon-button.d.ts +0 -131
  246. package/dist/ui/icon.d.ts +0 -88
  247. package/dist/ui/loading-indicator.d.ts +0 -42
  248. package/dist/ui/navigation-rail.d.ts +0 -29
  249. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  250. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  251. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  252. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  253. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  254. package/dist/ui/progress-indicator/types.d.ts +0 -151
  255. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  256. package/dist/ui/radio-button.d.ts +0 -106
  257. package/dist/ui/ripple.d.ts +0 -126
  258. package/dist/ui/scroll-area.d.ts +0 -27
  259. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  260. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  261. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  262. package/dist/ui/search/index.d.ts +0 -27
  263. package/dist/ui/search/search-bar.d.ts +0 -32
  264. package/dist/ui/search/search-context.d.ts +0 -24
  265. package/dist/ui/search/search-view-docked.d.ts +0 -25
  266. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  267. package/dist/ui/search/search.d.ts +0 -50
  268. package/dist/ui/search/search.tokens.d.ts +0 -112
  269. package/dist/ui/search/search.types.d.ts +0 -131
  270. package/dist/ui/search/trailing-action.d.ts +0 -9
  271. package/dist/ui/shared/constants.d.ts +0 -86
  272. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  273. package/dist/ui/slider/range-slider.d.ts +0 -47
  274. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  275. package/dist/ui/slider/slider-track.d.ts +0 -25
  276. package/dist/ui/slider/slider.d.ts +0 -60
  277. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  278. package/dist/ui/slider/slider.types.d.ts +0 -259
  279. package/dist/ui/snackbar/index.d.ts +0 -6
  280. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  281. package/dist/ui/switch/switch.d.ts +0 -30
  282. package/dist/ui/switch/switch.stories.d.ts +0 -48
  283. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  284. package/dist/ui/switch/switch.types.d.ts +0 -59
  285. package/dist/ui/tabs/tab.d.ts +0 -43
  286. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  287. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  288. package/dist/ui/tabs/tabs.d.ts +0 -60
  289. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  290. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  291. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  292. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  293. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  294. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  295. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  296. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  297. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  298. package/dist/ui/text-field/text-field.d.ts +0 -49
  299. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  300. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  301. package/dist/ui/theme-provider/index.d.ts +0 -48
  302. package/dist/ui/toc.d.ts +0 -80
  303. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  304. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  305. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  306. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  307. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  308. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  309. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  310. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  311. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  312. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  313. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  314. package/dist/ui/typography/typography.d.ts +0 -265
  315. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  316. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * @file divider.tsx
3
+ *
4
+ * MD3 Expressive Divider component.
5
+ *
6
+ * - `Divider` → A thin line that groups content in lists and layouts.
7
+ *
8
+ * @remarks
9
+ * Token references (Kotlin source — DividerTokens.kt v0_117):
10
+ * Color = ColorSchemeKeyTokens.OutlineVariant → bg-m3-outline-variant / text-m3-outline-variant
11
+ * Thickness = 1.0.dp → h-px (flat horizontal) | w-px (flat vertical) | strokeWidth={1} (wavy SVG)
12
+ *
13
+ * Variants:
14
+ * - "full-bleed" → spans full width, no indent
15
+ * - "inset" → leading indent (16dp standard / 72dp after-icon via insetStart)
16
+ * - "middle-inset" → 16dp indent both sides
17
+ * - "subheader" → same as full-bleed (label provided externally)
18
+ *
19
+ * Shapes:
20
+ * - "flat" → straight 1px line rendered as <m.div> (default)
21
+ * - "wavy" → sinusoidal SVG wave via <m.svg> + buildWavePath helper (horizontal only)
22
+ *
23
+ * Architecture:
24
+ * - Styling: `cn` (clsx/tailwind-merge) + static Tailwind classes only
25
+ * - Animation: Framer Motion LazyMotion/domMax, scaleX/scaleY spring entrance
26
+ * - Wavy: SVG path generated by pure-JS `buildWavePath()` (cubic Bézier sine approx)
27
+ * Uses ResizeObserver (via useContainerWidth) to measure container width so
28
+ * the path has precise bounds and leading/trailing ends are properly rounded.
29
+ * - A11y: role="separator", aria-orientation, aria-hidden when decorative
30
+ *
31
+ * @see https://m3.material.io/components/divider/overview
32
+ */
33
+
34
+ import { domMax, LazyMotion, m, useReducedMotion } from "motion/react";
35
+ import * as React from "react";
36
+ import { cn } from "../lib/utils";
37
+ import { useContainerWidth, useMergedRef } from "./progress-indicator/hooks";
38
+
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+ // Types
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+
43
+ // Exclude onDrag-family event handlers that conflict between React's HTMLAttributes
44
+ // and Framer Motion's MotionProps (different DragEvent signatures).
45
+ type SafeHTMLDivAttrs = Omit<
46
+ React.HTMLAttributes<HTMLDivElement>,
47
+ | "onDrag"
48
+ | "onDragStart"
49
+ | "onDragEnd"
50
+ | "onDragEnter"
51
+ | "onDragLeave"
52
+ | "onDragOver"
53
+ | "onDrop"
54
+ >;
55
+
56
+ export interface DividerProps extends SafeHTMLDivAttrs {
57
+ /**
58
+ * Visual variant controlling indentation.
59
+ * - "full-bleed" → no indent, spans full container width/height (default)
60
+ * - "inset" → leading indent only (use `insetStart` to control amount)
61
+ * - "middle-inset" → 16px indent on both sides
62
+ * - "subheader" → alias for full-bleed, used before section labels
63
+ * @default "full-bleed"
64
+ */
65
+ variant?: "full-bleed" | "inset" | "middle-inset" | "subheader";
66
+
67
+ /**
68
+ * Orientation of the divider line.
69
+ * - "horizontal" → renders as `h-px w-full` bar (default)
70
+ * - "vertical" → renders as `w-px h-full` column
71
+ * Note: `shape="wavy"` is not supported with orientation="vertical".
72
+ * @default "horizontal"
73
+ */
74
+ orientation?: "horizontal" | "vertical";
75
+
76
+ /**
77
+ * Shape of the divider line.
78
+ * - "flat" → straight 1px line rendered as `<div>` (default)
79
+ * - "wavy" → sinusoidal SVG wave (horizontal orientation only)
80
+ *
81
+ * When `shape="wavy"` and `orientation="vertical"`, silently falls back to
82
+ * `shape="flat"` and emits a `console.warn`.
83
+ * @default "flat"
84
+ */
85
+ shape?: "flat" | "wavy";
86
+
87
+ /**
88
+ * Leading inset size for the "inset" variant.
89
+ * - "standard" → 16px (`ml-4`)
90
+ * - "icon" → 72px (`ml-[72px]`) — use when list items have leading icons/avatars
91
+ * Only applies when `variant="inset"`.
92
+ * @default "standard"
93
+ */
94
+ insetStart?: "standard" | "icon";
95
+
96
+ /**
97
+ * Wavy shape visual parameters. Only applies when `shape="wavy"`.
98
+ * All values are in pixels.
99
+ */
100
+ waveConfig?: {
101
+ /**
102
+ * Peak displacement from the center line.
103
+ * @default 2
104
+ */
105
+ amplitude?: number;
106
+ /**
107
+ * Pixels per full sine cycle.
108
+ * @default 32
109
+ */
110
+ wavelength?: number;
111
+ /**
112
+ * Thickness of the wave stroke in pixels.
113
+ * @default 1
114
+ */
115
+ strokeWidth?: number;
116
+ };
117
+
118
+ /**
119
+ * When true, marks the divider as decorative (`aria-hidden="true"`).
120
+ * Use when the divider is purely visual with no semantic meaning.
121
+ * @default false
122
+ */
123
+ decorative?: boolean;
124
+
125
+ /**
126
+ * When true, plays an entrance animation (scaleX/scaleY from 0→1).
127
+ * Automatically disabled when user prefers reduced motion.
128
+ * @default true
129
+ */
130
+ animate?: boolean;
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+ // Helpers
135
+ // ─────────────────────────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Generates an SVG path `d` string for a sinusoidal wave between two X bounds.
139
+ * Uses cubic Bézier approximation of a sine curve for smooth rendering.
140
+ * The error vs. a true sine is < 0.2%.
141
+ *
142
+ * Each half-cycle alternates between crest (+amplitude) and trough (-amplitude).
143
+ * Control points are placed at 1/3 and 2/3 of each half-cycle for accuracy.
144
+ *
145
+ * @param startX - X coordinate where the path begins (e.g. capWidth = strokeWidth/2)
146
+ * @param endX - X coordinate where the path ends (e.g. containerWidth - capWidth)
147
+ * @param amplitude - Peak displacement from center line (px). Default: 2
148
+ * @param wavelength - Pixels per full sine cycle. Default: 32
149
+ * @param yCenter - Y coordinate of the wave center line. Default: 4
150
+ * @returns SVG path `d` attribute string, or "" when startX >= endX
151
+ *
152
+ * @example
153
+ * buildWavePath(0.5, 499.5, 2, 32, 4)
154
+ * // → "M 0.5,4 C 11.17,... 500,4 ..."
155
+ */
156
+ export function buildWavePath(
157
+ startX: number,
158
+ endX: number,
159
+ amplitude = 2,
160
+ wavelength = 32,
161
+ yCenter = 4,
162
+ ): string {
163
+ if (startX >= endX) return "";
164
+
165
+ const halfCycle = wavelength / 2;
166
+ const numHalfCycles = Math.ceil((endX - startX) / halfCycle);
167
+
168
+ // MD3 precise bezier approximation: offset multiplier (4/3) lets control points
169
+ // visually reach exactly the given amplitude.
170
+ const cpDistanceY = amplitude * (4 / 3);
171
+
172
+ let d = `M ${startX},${yCenter}`;
173
+
174
+ for (let i = 0; i < numHalfCycles; i++) {
175
+ const x0 = startX + i * halfCycle;
176
+ const x1 = Math.min(startX + (i + 1) * halfCycle, endX);
177
+ // Alternate crest and trough
178
+ const peak = i % 2 === 0 ? yCenter - cpDistanceY : yCenter + cpDistanceY;
179
+ // 1/3 and 2/3 ratio for smoother sine approximation (Google MD3 style)
180
+ const segLen = x1 - x0;
181
+ const cp1x = x0 + segLen / 3;
182
+ const cp2x = x1 - segLen / 3;
183
+ d += ` C ${cp1x},${peak} ${cp2x},${peak} ${x1},${yCenter}`;
184
+ }
185
+
186
+ return d;
187
+ }
188
+
189
+ // ─────────────────────────────────────────────────────────────────────────────
190
+ // Internal implementation
191
+ // ─────────────────────────────────────────────────────────────────────────────
192
+
193
+ const DividerImpl = React.forwardRef<HTMLDivElement, DividerProps>(
194
+ (
195
+ {
196
+ variant = "full-bleed",
197
+ orientation = "horizontal",
198
+ shape = "flat",
199
+ insetStart = "standard",
200
+ waveConfig,
201
+ decorative = false,
202
+ animate = true,
203
+ className,
204
+ style,
205
+ ...props
206
+ },
207
+ ref,
208
+ ) => {
209
+ const reducedMotion = useReducedMotion();
210
+ const shouldAnimate = animate && !reducedMotion;
211
+
212
+ // Wavy is only supported for horizontal orientation
213
+ const effectiveShape =
214
+ shape === "wavy" && orientation === "vertical" ? "flat" : shape;
215
+
216
+ if (shape === "wavy" && orientation === "vertical") {
217
+ console.warn(
218
+ "[Divider] shape='wavy' is not supported with orientation='vertical'. Falling back to shape='flat'.",
219
+ );
220
+ }
221
+
222
+ // Container width measurement — needed for direct SVG path rendering so
223
+ // leading/trailing ends land at exact bounds (enabling rounded caps).
224
+ const [containerRef, containerWidth] = useContainerWidth();
225
+
226
+ // ── A11y attrs ──────────────────────────────────────────────────────────
227
+ const a11yProps = decorative
228
+ ? { "aria-hidden": "true" as const }
229
+ : {
230
+ role: "separator" as const,
231
+ "aria-orientation": orientation,
232
+ };
233
+
234
+ // ── Ref Merging ────────────────────────────────────────────────────────
235
+ // We need to merge the internal containerRef (for ResizeObserver) with
236
+ // the user-provided external ref.
237
+ const mergedRef = useMergedRef(containerRef, ref);
238
+
239
+ // ── Inset class mapping (all static) ────────────────────────────────────
240
+ const isHorizontal = orientation === "horizontal";
241
+ const insetClass = cn(
242
+ variant === "inset" &&
243
+ isHorizontal &&
244
+ insetStart === "standard" &&
245
+ "ml-4",
246
+ variant === "inset" &&
247
+ isHorizontal &&
248
+ insetStart === "icon" &&
249
+ "ml-[72px]",
250
+ variant === "inset" && !isHorizontal && "mt-4",
251
+ variant === "middle-inset" && isHorizontal && "mx-4",
252
+ variant === "middle-inset" && !isHorizontal && "my-4",
253
+ );
254
+
255
+ // ── Spring transition ────────────────────────────────────────────────────
256
+ const springTransition = shouldAnimate
257
+ ? ({ type: "spring", stiffness: 300, damping: 30, mass: 0.6 } as const)
258
+ : ({ duration: 0 } as const);
259
+
260
+ // ── Wavy amplitude / wavelength / thickness ───────────────────────────
261
+ const amplitude = waveConfig?.amplitude ?? 2;
262
+ const wavelength = waveConfig?.wavelength ?? 32;
263
+ const strokeWidth = waveConfig?.strokeWidth ?? 1;
264
+
265
+ // ────────────────────────────────────────────────────────────────────────
266
+ // RENDER: Wavy
267
+ // ────────────────────────────────────────────────────────────────────────
268
+ if (effectiveShape === "wavy") {
269
+ // Canvas explicitly accommodates the curve's exact bounding box.
270
+ const canvasHeight = Math.ceil(amplitude * 2 + strokeWidth);
271
+ const yCenter = canvasHeight / 2;
272
+
273
+ // Half stroke-width inset so rounded caps are not clipped by the SVG edge.
274
+ const capWidth = strokeWidth / 2;
275
+ const pathStartX = capWidth;
276
+ const pathEndX = Math.max(capWidth, containerWidth - capWidth);
277
+
278
+ return (
279
+ <div
280
+ ref={mergedRef}
281
+ className={cn(
282
+ "block shrink-0 overflow-hidden",
283
+ isHorizontal ? "w-full" : "h-full self-stretch",
284
+ insetClass,
285
+ className,
286
+ )}
287
+ style={{ height: `${canvasHeight}px`, ...style }}
288
+ {...a11yProps}
289
+ // biome-ignore lint/suspicious/noExplicitAny: spread safe HTMLDiv attrs
290
+ {...(props as any)}
291
+ >
292
+ <LazyMotion features={domMax} strict>
293
+ <m.svg
294
+ xmlns="http://www.w3.org/2000/svg"
295
+ width="100%"
296
+ height="100%"
297
+ className="text-m3-outline-variant block"
298
+ aria-hidden="true"
299
+ style={{ overflow: "visible", transformOrigin: "left center" }}
300
+ initial={shouldAnimate ? { scaleX: 0, opacity: 0 } : undefined}
301
+ animate={shouldAnimate ? { scaleX: 1, opacity: 1 } : undefined}
302
+ transition={springTransition}
303
+ >
304
+ {containerWidth > 0 && (
305
+ <path
306
+ d={buildWavePath(
307
+ pathStartX,
308
+ pathEndX,
309
+ amplitude,
310
+ wavelength,
311
+ yCenter,
312
+ )}
313
+ fill="none"
314
+ stroke="currentColor"
315
+ strokeWidth={strokeWidth}
316
+ strokeLinecap="round"
317
+ />
318
+ )}
319
+ </m.svg>
320
+ </LazyMotion>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ // ────────────────────────────────────────────────────────────────────────
326
+ // RENDER: Flat
327
+ // ────────────────────────────────────────────────────────────────────────
328
+ return (
329
+ <LazyMotion features={domMax} strict>
330
+ <m.div
331
+ ref={mergedRef}
332
+ className={cn(
333
+ "block shrink-0 bg-m3-outline-variant",
334
+ isHorizontal ? "h-px w-full" : "w-px h-full self-stretch",
335
+ insetClass,
336
+ className,
337
+ )}
338
+ style={{
339
+ ...(isHorizontal
340
+ ? { transformOrigin: "left" }
341
+ : { transformOrigin: "top" }),
342
+ ...style,
343
+ }}
344
+ initial={
345
+ shouldAnimate
346
+ ? isHorizontal
347
+ ? { scaleX: 0 }
348
+ : { scaleY: 0 }
349
+ : undefined
350
+ }
351
+ animate={
352
+ shouldAnimate
353
+ ? isHorizontal
354
+ ? { scaleX: 1 }
355
+ : { scaleY: 1 }
356
+ : undefined
357
+ }
358
+ transition={springTransition}
359
+ {...a11yProps}
360
+ // biome-ignore lint/suspicious/noExplicitAny: spread safe HTMLDiv attrs
361
+ {...(props as any)}
362
+ />
363
+ </LazyMotion>
364
+ );
365
+ },
366
+ );
367
+
368
+ DividerImpl.displayName = "Divider";
369
+
370
+ // ─────────────────────────────────────────────────────────────────────────────
371
+ // Public export
372
+ // ─────────────────────────────────────────────────────────────────────────────
373
+
374
+ /**
375
+ * MD3 Expressive Divider — a thin line that groups content in lists and layouts.
376
+ *
377
+ * Supports four layout variants (full-bleed, inset, middle-inset, subheader),
378
+ * two orientations (horizontal, vertical), and two shape modes (flat, wavy).
379
+ *
380
+ * The wavy shape renders a sinusoidal SVG wave and is only supported with
381
+ * `orientation="horizontal"`.
382
+ *
383
+ * @example
384
+ * ```tsx
385
+ * // Default flat full-bleed horizontal divider
386
+ * <Divider />
387
+ *
388
+ * // Inset after icon list items
389
+ * <Divider variant="inset" insetStart="icon" />
390
+ *
391
+ * // Vertical separator between panes
392
+ * <Divider orientation="vertical" />
393
+ *
394
+ * // Wavy expressive divider
395
+ * <Divider shape="wavy" />
396
+ *
397
+ * // Wavy with custom wave config
398
+ * <Divider shape="wavy" waveConfig={{ amplitude: 5, wavelength: 24 }} />
399
+ *
400
+ * // Decorative (no a11y semantics)
401
+ * <Divider decorative />
402
+ *
403
+ * // No entrance animation
404
+ * <Divider animate={false} />
405
+ *
406
+ * // Middle-inset wavy divider
407
+ * <Divider variant="middle-inset" shape="wavy" />
408
+ * ```
409
+ *
410
+ * @see https://m3.material.io/components/divider/overview
411
+ */
412
+ export const Divider = React.memo(DividerImpl);
@@ -0,0 +1,240 @@
1
+ import * as RadixDialog from "@radix-ui/react-dialog";
2
+ import { AnimatePresence, m } from "motion/react";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+ import { Icon } from "./icon";
6
+
7
+ // ─── MD3 Expressive Drawer Animation ─────────────────────────────────────────
8
+ // Slide từ dưới lên, spring physics giống Google Material's "Emphasized" easing
9
+ const MD3_DRAWER_SPRING = {
10
+ type: "spring" as const,
11
+ stiffness: 350,
12
+ damping: 35,
13
+ mass: 0.9,
14
+ };
15
+
16
+ const MD3_DRAWER_ANIM = {
17
+ initial: { y: "100%", opacity: 0.6 },
18
+ animate: {
19
+ y: 0,
20
+ opacity: 1,
21
+ transition: MD3_DRAWER_SPRING,
22
+ },
23
+ exit: {
24
+ y: "100%",
25
+ opacity: 0,
26
+ transition: { duration: 0.22, ease: "easeIn" as const },
27
+ },
28
+ };
29
+
30
+ const MD3_SCRIM_ANIM = {
31
+ initial: { opacity: 0 },
32
+ animate: {
33
+ opacity: 1,
34
+ transition: { duration: 0.2, ease: "easeOut" as const },
35
+ },
36
+ exit: { opacity: 0, transition: { duration: 0.18, ease: "easeIn" as const } },
37
+ };
38
+
39
+ // ─── Types ────────────────────────────────────────────────────────────────────
40
+ export interface DrawerProps {
41
+ open?: boolean;
42
+ onOpenChange?: (open: boolean) => void;
43
+ children: React.ReactNode;
44
+ }
45
+
46
+ export interface DrawerContentProps
47
+ extends Omit<
48
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Content>,
49
+ "asChild"
50
+ > {
51
+ /** Chiều cao tối đa (vh). Mặc định 90vh */
52
+ maxHeight?: string;
53
+ /** Ẩn drag handle */
54
+ hideHandle?: boolean;
55
+ /** Ẩn nút đóng */
56
+ hideCloseButton?: boolean;
57
+ className?: string;
58
+ }
59
+
60
+ // ─── Root ─────────────────────────────────────────────────────────────────────
61
+ const Drawer = ({ open, onOpenChange, children }: DrawerProps) => (
62
+ <RadixDialog.Root open={open} onOpenChange={onOpenChange}>
63
+ {children}
64
+ </RadixDialog.Root>
65
+ );
66
+ Drawer.displayName = "Drawer";
67
+
68
+ const DrawerTrigger = RadixDialog.Trigger;
69
+ DrawerTrigger.displayName = "DrawerTrigger";
70
+
71
+ const DrawerClose = RadixDialog.Close;
72
+
73
+ // ─── Portal wrapper với AnimatePresence ───────────────────────────────────────
74
+ const DrawerPortal = ({
75
+ open,
76
+ children,
77
+ }: {
78
+ open?: boolean;
79
+ children: React.ReactNode;
80
+ }) => (
81
+ <RadixDialog.Portal forceMount>
82
+ <AnimatePresence mode="wait">{open && children}</AnimatePresence>
83
+ </RadixDialog.Portal>
84
+ );
85
+
86
+ // ─── Scrim overlay ────────────────────────────────────────────────────────────
87
+ const DrawerOverlay = React.forwardRef<
88
+ React.ComponentRef<typeof RadixDialog.Overlay>,
89
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Overlay>
90
+ >(({ className, ...props }, ref) => (
91
+ <RadixDialog.Overlay ref={ref} asChild {...props}>
92
+ <m.div
93
+ aria-hidden="true"
94
+ className={cn("fixed inset-0 z-50 bg-black/40", className)}
95
+ {...MD3_SCRIM_ANIM}
96
+ />
97
+ </RadixDialog.Overlay>
98
+ ));
99
+ DrawerOverlay.displayName = "DrawerOverlay";
100
+
101
+ // ─── Content ─────────────────────────────────────────────────────────────────
102
+ const DrawerContent = React.forwardRef<
103
+ React.ComponentRef<typeof RadixDialog.Content>,
104
+ DrawerContentProps
105
+ >(
106
+ (
107
+ {
108
+ className,
109
+ children,
110
+ maxHeight = "90vh",
111
+ hideHandle = false,
112
+ hideCloseButton = false,
113
+ style,
114
+ ...props
115
+ },
116
+ ref,
117
+ ) => (
118
+ <RadixDialog.Content ref={ref} asChild {...props}>
119
+ <m.div
120
+ className={cn(
121
+ // MD3 Bottom Sheet shape: chỉ bo góc trên
122
+ "fixed bottom-0 left-0 right-0 z-50",
123
+ "rounded-t-[28px] bg-m3-surface-container-low",
124
+ "flex flex-col overflow-hidden",
125
+ "outline-none",
126
+ // focus-visible ring — a11y
127
+ "focus-visible:ring-2 focus-visible:ring-m3-primary focus-visible:ring-inset",
128
+ className,
129
+ )}
130
+ style={{ maxHeight, ...style }}
131
+ {...MD3_DRAWER_ANIM}
132
+ >
133
+ {/* Drag handle — decorative hint */}
134
+ {!hideHandle && (
135
+ <div
136
+ aria-hidden="true"
137
+ className="mx-auto mt-3 h-1 w-9 rounded-full bg-m3-on-surface-variant/40 shrink-0"
138
+ />
139
+ )}
140
+
141
+ {/* Close button */}
142
+ {!hideCloseButton && (
143
+ <RadixDialog.Close
144
+ className={cn(
145
+ "absolute right-4 top-3 rounded-full p-2",
146
+ "text-m3-on-surface-variant",
147
+ "hover:bg-m3-on-surface/8",
148
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-m3-primary",
149
+ "transition-colors duration-200",
150
+ )}
151
+ aria-label="Đóng bảng điều khiển"
152
+ >
153
+ <Icon name="close" size={20} aria-hidden="true" />
154
+ </RadixDialog.Close>
155
+ )}
156
+
157
+ {/* Scrollable content area */}
158
+ <div className="flex-1 overflow-y-auto overscroll-contain p-6">
159
+ {children}
160
+ </div>
161
+ </m.div>
162
+ </RadixDialog.Content>
163
+ ),
164
+ );
165
+ DrawerContent.displayName = "DrawerContent";
166
+
167
+ // ─── Sub-components ───────────────────────────────────────────────────────────
168
+ const DrawerHeader = ({
169
+ className,
170
+ ...props
171
+ }: React.HTMLAttributes<HTMLDivElement>) => (
172
+ <div className={cn("flex flex-col gap-1 mb-4", className)} {...props} />
173
+ );
174
+ DrawerHeader.displayName = "DrawerHeader";
175
+
176
+ const DrawerFooter = ({
177
+ className,
178
+ ...props
179
+ }: React.HTMLAttributes<HTMLDivElement>) => (
180
+ <div className={cn("flex flex-col gap-2 mt-4", className)} {...props} />
181
+ );
182
+ DrawerFooter.displayName = "DrawerFooter";
183
+
184
+ const DrawerTitle = React.forwardRef<
185
+ React.ComponentRef<typeof RadixDialog.Title>,
186
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Title>
187
+ >(({ className, ...props }, ref) => (
188
+ <RadixDialog.Title
189
+ ref={ref}
190
+ className={cn(
191
+ "text-[22px] leading-7 font-medium text-m3-on-surface",
192
+ className,
193
+ )}
194
+ {...props}
195
+ />
196
+ ));
197
+ DrawerTitle.displayName = "DrawerTitle";
198
+
199
+ const DrawerDescription = React.forwardRef<
200
+ React.ComponentRef<typeof RadixDialog.Description>,
201
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Description>
202
+ >(({ className, ...props }, ref) => (
203
+ <RadixDialog.Description
204
+ ref={ref}
205
+ className={cn("text-sm text-m3-on-surface-variant leading-5", className)}
206
+ {...props}
207
+ />
208
+ ));
209
+ DrawerDescription.displayName = "DrawerDescription";
210
+
211
+ export {
212
+ Drawer,
213
+ DrawerClose,
214
+ DrawerContent,
215
+ DrawerDescription,
216
+ DrawerFooter,
217
+ DrawerHeader,
218
+ DrawerOverlay,
219
+ DrawerPortal,
220
+ DrawerTitle,
221
+ DrawerTrigger,
222
+ };
223
+
224
+ // ─── Usage:
225
+ // <Drawer open={open} onOpenChange={setOpen}>
226
+ // <DrawerTrigger asChild><Button>Mở Drawer</Button></DrawerTrigger>
227
+ // <DrawerPortal open={open}>
228
+ // <DrawerOverlay />
229
+ // <DrawerContent maxHeight="80vh">
230
+ // <DrawerHeader>
231
+ // <DrawerTitle>Chi tiết đơn hàng</DrawerTitle>
232
+ // <DrawerDescription>Xem thông tin đơn hàng #1234</DrawerDescription>
233
+ // </DrawerHeader>
234
+ // <p>Nội dung drawer...</p>
235
+ // <DrawerFooter>
236
+ // <Button colorStyle="filled" className="w-full">Xác nhận</Button>
237
+ // </DrawerFooter>
238
+ // </DrawerContent>
239
+ // </DrawerPortal>
240
+ // </Drawer>