@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.
- package/.turbo/turbo-build.log +33 -0
- package/CHANGELOG.md +55 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6127 -0
- package/dist/index.d.ts +6127 -71
- package/dist/index.js +1653 -614
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1566 -547
- package/dist/index.mjs.map +1 -1
- package/dist/material-symbols-cdn.css.d.ts +2 -0
- package/dist/material-symbols-self-hosted.css.d.ts +2 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +22 -19
- package/scripts/copy-assets.js +82 -0
- package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
- package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/loading-indicator.svg +19 -0
- package/src/assets/material-symbols-cdn.css +65 -0
- package/src/assets/material-symbols-self-hosted.css +90 -0
- package/src/css.d.ts +20 -0
- package/src/hooks/useClickOutside.ts +37 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useRipple.ts +88 -0
- package/src/index.css +23 -0
- package/src/index.ts +349 -0
- package/src/lib/material-symbols-preconnect.tsx +82 -0
- package/src/lib/theme-utils.ts +180 -0
- package/src/lib/utils.ts +6 -0
- package/src/test/button.test.tsx +59 -0
- package/src/test/icon.test.tsx +91 -0
- package/src/test/loading-indicator.test.tsx +128 -0
- package/src/test/progress-indicator.test.tsx +306 -0
- package/src/test/setup.ts +80 -0
- package/src/test/typography.test.tsx +206 -0
- package/src/types/index.ts +7 -0
- package/src/types/md3.ts +31 -0
- package/src/ui/Text.tsx +60 -0
- package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
- package/src/ui/app-bar/app-bar-column.tsx +99 -0
- package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
- package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
- package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
- package/src/ui/app-bar/app-bar-row.tsx +104 -0
- package/src/ui/app-bar/app-bar.test.tsx +87 -0
- package/src/ui/app-bar/app-bar.tokens.ts +223 -0
- package/src/ui/app-bar/app-bar.types.ts +441 -0
- package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
- package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
- package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
- package/src/ui/app-bar/docked-toolbar.tsx +54 -0
- package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
- package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
- package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
- package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
- package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
- package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
- package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
- package/src/ui/app-bar/search-app-bar.tsx +176 -0
- package/src/ui/app-bar/search-view.tsx +227 -0
- package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
- package/src/ui/app-bar/small-app-bar.tsx +203 -0
- package/src/ui/badge.test.tsx +345 -0
- package/src/ui/badge.tsx +282 -0
- package/src/ui/button-group.test.tsx +71 -0
- package/src/ui/button-group.tsx +350 -0
- package/src/ui/button.test.tsx +297 -0
- package/src/ui/button.tsx +669 -0
- package/src/ui/card.test.tsx +187 -0
- package/src/ui/card.tsx +259 -0
- package/src/ui/checkbox.test.tsx +423 -0
- package/src/ui/checkbox.tsx +525 -0
- package/src/ui/chip.test.tsx +292 -0
- package/src/ui/chip.tsx +548 -0
- package/src/ui/code-block.tsx +219 -0
- package/src/ui/dialog.test.tsx +300 -0
- package/src/ui/dialog.tsx +384 -0
- package/src/ui/divider.test.tsx +314 -0
- package/src/ui/divider.tsx +412 -0
- package/src/ui/drawer.tsx +240 -0
- package/src/ui/fab-menu.test.tsx +494 -0
- package/src/ui/fab-menu.tsx +739 -0
- package/src/ui/fab.test.tsx +232 -0
- package/src/ui/fab.tsx +505 -0
- package/src/ui/icon-button.test.tsx +515 -0
- package/src/ui/icon-button.tsx +525 -0
- package/src/ui/icon.test.tsx +197 -0
- package/src/ui/icon.tsx +179 -0
- package/src/ui/loading-indicator.test.tsx +73 -0
- package/src/ui/loading-indicator.tsx +312 -0
- package/src/ui/menu/context-menu.tsx +275 -0
- package/src/ui/menu/index.ts +77 -0
- package/src/ui/menu/menu-animations.ts +102 -0
- package/src/ui/menu/menu-context.tsx +99 -0
- package/src/ui/menu/menu-divider.tsx +47 -0
- package/src/ui/menu/menu-group.tsx +200 -0
- package/src/ui/menu/menu-item.tsx +294 -0
- package/src/ui/menu/menu-tokens.ts +208 -0
- package/src/ui/menu/menu-types.ts +313 -0
- package/src/ui/menu/menu.test.tsx +624 -0
- package/src/ui/menu/menu.tsx +289 -0
- package/src/ui/menu/sub-menu.tsx +223 -0
- package/src/ui/menu/vertical-menu.tsx +382 -0
- package/src/ui/navigation-rail.test.tsx +404 -0
- package/src/ui/navigation-rail.tsx +604 -0
- package/src/ui/progress-indicator/circular.tsx +248 -0
- package/src/ui/progress-indicator/hooks.ts +51 -0
- package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
- package/src/ui/progress-indicator/linear-flat.tsx +83 -0
- package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
- package/src/ui/progress-indicator/linear.tsx +143 -0
- package/src/ui/progress-indicator/types.ts +158 -0
- package/src/ui/progress-indicator/utils.ts +73 -0
- package/src/ui/radio-button.test.tsx +407 -0
- package/src/ui/radio-button.tsx +551 -0
- package/src/ui/ripple.test.tsx +72 -0
- package/src/ui/ripple.tsx +234 -0
- package/src/ui/scroll-area.test.tsx +58 -0
- package/src/ui/scroll-area.tsx +139 -0
- package/src/ui/search/animated-placeholder.tsx +145 -0
- package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
- package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
- package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
- package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
- package/src/ui/search/index.ts +44 -0
- package/src/ui/search/search-bar.tsx +220 -0
- package/src/ui/search/search-context.tsx +42 -0
- package/src/ui/search/search-view-docked.tsx +194 -0
- package/src/ui/search/search-view-fullscreen.tsx +247 -0
- package/src/ui/search/search.test.tsx +233 -0
- package/src/ui/search/search.tokens.ts +134 -0
- package/src/ui/search/search.tsx +131 -0
- package/src/ui/search/search.types.ts +154 -0
- package/src/ui/search/trailing-action.tsx +49 -0
- package/src/ui/shared/constants.ts +122 -0
- package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
- package/src/ui/slider/hooks/useSliderMath.ts +195 -0
- package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
- package/src/ui/slider/range-slider.tsx +561 -0
- package/src/ui/slider/slider-thumb.tsx +379 -0
- package/src/ui/slider/slider-track.tsx +912 -0
- package/src/ui/slider/slider.tokens.ts +189 -0
- package/src/ui/slider/slider.tsx +259 -0
- package/src/ui/slider/slider.types.ts +288 -0
- package/src/ui/snackbar/index.ts +20 -0
- package/src/ui/snackbar/snackbar.test.tsx +338 -0
- package/src/ui/snackbar/snackbar.tsx +476 -0
- package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
- package/src/ui/switch/switch.stories.tsx +309 -0
- package/src/ui/switch/switch.test.tsx +243 -0
- package/src/ui/switch/switch.tokens.ts +89 -0
- package/src/ui/switch/switch.tsx +504 -0
- package/src/ui/switch/switch.types.ts +62 -0
- package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
- package/src/ui/tabs/tab.tsx +407 -0
- package/src/ui/tabs/tabs-content.tsx +89 -0
- package/src/ui/tabs/tabs-list.tsx +146 -0
- package/src/ui/tabs/tabs.test.tsx +290 -0
- package/src/ui/tabs/tabs.tokens.ts +121 -0
- package/src/ui/tabs/tabs.tsx +229 -0
- package/src/ui/tabs/tabs.types.ts +185 -0
- package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
- package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
- package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
- package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
- package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
- package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
- package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
- package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
- package/src/ui/text-field/text-field.test.tsx +454 -0
- package/src/ui/text-field/text-field.tokens.ts +104 -0
- package/src/ui/text-field/text-field.tsx +548 -0
- package/src/ui/text-field/text-field.types.ts +180 -0
- package/src/ui/theme-provider/index.tsx +190 -0
- package/src/ui/toc.test.tsx +108 -0
- package/src/ui/toc.tsx +172 -0
- package/src/ui/tooltip/plain-tooltip.tsx +63 -0
- package/src/ui/tooltip/rich-tooltip.tsx +94 -0
- package/src/ui/tooltip/tooltip-box.tsx +266 -0
- package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
- package/src/ui/tooltip/tooltip.tokens.ts +26 -0
- package/src/ui/tooltip/tooltip.types.ts +70 -0
- package/src/ui/tooltip/use-tooltip-position.ts +208 -0
- package/src/ui/tooltip/use-tooltip-state.ts +41 -0
- package/src/ui/typography/__tests__/typography.test.tsx +170 -0
- package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
- package/src/ui/typography/type-scale-tokens.ts +205 -0
- package/src/ui/typography/typography-key-tokens.ts +43 -0
- package/src/ui/typography/typography-tokens.ts +360 -0
- package/src/ui/typography/typography.css +22 -0
- package/src/ui/typography/typography.tsx +559 -0
- package/test-render.tsx +4 -0
- package/test-shadow.html +26 -0
- package/test_output.txt +164 -0
- package/test_output_v2.txt +5 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +11 -0
- package/dist/hooks/useClickOutside.d.ts +0 -8
- package/dist/hooks/useMediaQuery.d.ts +0 -11
- package/dist/hooks/useRipple.d.ts +0 -26
- package/dist/lib/material-symbols-preconnect.d.ts +0 -42
- package/dist/lib/theme-utils.d.ts +0 -63
- package/dist/lib/utils.d.ts +0 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/md3.d.ts +0 -14
- package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
- package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
- package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
- package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
- package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
- package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
- package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
- package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
- package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
- package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
- package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
- package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
- package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
- package/dist/ui/app-bar/search-view.d.ts +0 -54
- package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
- package/dist/ui/badge.d.ts +0 -125
- package/dist/ui/button-group.d.ts +0 -59
- package/dist/ui/button.d.ts +0 -148
- package/dist/ui/card.d.ts +0 -62
- package/dist/ui/checkbox.d.ts +0 -82
- package/dist/ui/chip.d.ts +0 -110
- package/dist/ui/code-block.d.ts +0 -14
- package/dist/ui/dialog.d.ts +0 -111
- package/dist/ui/divider.d.ts +0 -164
- package/dist/ui/drawer.d.ts +0 -39
- package/dist/ui/dropdown.d.ts +0 -29
- package/dist/ui/fab-menu.d.ts +0 -204
- package/dist/ui/fab.d.ts +0 -162
- package/dist/ui/icon-button.d.ts +0 -131
- package/dist/ui/icon.d.ts +0 -88
- package/dist/ui/loading-indicator.d.ts +0 -42
- package/dist/ui/navigation-rail.d.ts +0 -29
- package/dist/ui/progress-indicator/circular.d.ts +0 -3
- package/dist/ui/progress-indicator/hooks.d.ts +0 -3
- package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
- package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
- package/dist/ui/progress-indicator/linear.d.ts +0 -3
- package/dist/ui/progress-indicator/types.d.ts +0 -151
- package/dist/ui/progress-indicator/utils.d.ts +0 -3
- package/dist/ui/radio-button.d.ts +0 -106
- package/dist/ui/ripple.d.ts +0 -126
- package/dist/ui/scroll-area.d.ts +0 -27
- package/dist/ui/search/animated-placeholder.d.ts +0 -54
- package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
- package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
- package/dist/ui/search/index.d.ts +0 -27
- package/dist/ui/search/search-bar.d.ts +0 -32
- package/dist/ui/search/search-context.d.ts +0 -24
- package/dist/ui/search/search-view-docked.d.ts +0 -25
- package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
- package/dist/ui/search/search.d.ts +0 -50
- package/dist/ui/search/search.tokens.d.ts +0 -112
- package/dist/ui/search/search.types.d.ts +0 -131
- package/dist/ui/search/trailing-action.d.ts +0 -9
- package/dist/ui/shared/constants.d.ts +0 -86
- package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
- package/dist/ui/slider/range-slider.d.ts +0 -47
- package/dist/ui/slider/slider-thumb.d.ts +0 -33
- package/dist/ui/slider/slider-track.d.ts +0 -25
- package/dist/ui/slider/slider.d.ts +0 -60
- package/dist/ui/slider/slider.tokens.d.ts +0 -151
- package/dist/ui/slider/slider.types.d.ts +0 -259
- package/dist/ui/snackbar/index.d.ts +0 -6
- package/dist/ui/snackbar/snackbar.d.ts +0 -197
- package/dist/ui/switch/switch.d.ts +0 -30
- package/dist/ui/switch/switch.stories.d.ts +0 -48
- package/dist/ui/switch/switch.tokens.d.ts +0 -67
- package/dist/ui/switch/switch.types.d.ts +0 -59
- package/dist/ui/tabs/tab.d.ts +0 -43
- package/dist/ui/tabs/tabs-content.d.ts +0 -36
- package/dist/ui/tabs/tabs-list.d.ts +0 -40
- package/dist/ui/tabs/tabs.d.ts +0 -60
- package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
- package/dist/ui/tabs/tabs.types.d.ts +0 -172
- package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
- package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
- package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
- package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
- package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
- package/dist/ui/text-field/text-field.d.ts +0 -49
- package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
- package/dist/ui/text-field/text-field.types.d.ts +0 -126
- package/dist/ui/theme-provider/index.d.ts +0 -48
- package/dist/ui/toc.d.ts +0 -80
- package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
- package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
- package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
- package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
- package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
- package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
- package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
- package/dist/ui/typography/typography-tokens.d.ts +0 -220
- package/dist/ui/typography/typography.d.ts +0 -265
- /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
- /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>
|