@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,102 @@
|
|
|
1
|
+
// ─── MD3 Expressive Menu — Framer Motion Animation Variants ──────────────────
|
|
2
|
+
// FastSpatial: mirrors MotionSchemeKeyTokens.FastSpatial
|
|
3
|
+
// Android: spring(stiffness=380, dampingRatio=0.7)
|
|
4
|
+
// Framer: damping = 2 × 0.7 × √(380 × 1) ≈ 27.3 → use 28
|
|
5
|
+
// FastEffects: mirrors MotionSchemeKeyTokens.FastEffects
|
|
6
|
+
// Android: duration=150ms, FastOutLinearIn
|
|
7
|
+
// Framer: duration=0.15, ease=[0.4, 0, 1, 1]
|
|
8
|
+
|
|
9
|
+
import type { Transition, Variants } from "motion/react";
|
|
10
|
+
|
|
11
|
+
// ─── Shared spring/easing specs ───────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** FastSpatial spring — used for shape morphing and spatial enter animations */
|
|
14
|
+
export const FAST_SPATIAL_SPRING: Transition = {
|
|
15
|
+
type: "spring",
|
|
16
|
+
stiffness: 380,
|
|
17
|
+
damping: 28,
|
|
18
|
+
mass: 1,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** FastEffects transition — used for opacity and exit animations */
|
|
22
|
+
export const FAST_EFFECTS_TRANSITION: Transition = {
|
|
23
|
+
duration: 0.15,
|
|
24
|
+
ease: [0.4, 0, 1, 1], // FastOutLinearIn
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ─── Menu popup container ─────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enter/exit animation for the menu popup container.
|
|
31
|
+
* Scale from 0.8→1.0 with FastSpatial spring.
|
|
32
|
+
* Transform-origin is driven by the Radix CSS variable
|
|
33
|
+
* `--radix-dropdown-menu-content-transform-origin`.
|
|
34
|
+
*/
|
|
35
|
+
export const MENU_CONTAINER_VARIANTS: Variants = {
|
|
36
|
+
hidden: {
|
|
37
|
+
opacity: 0,
|
|
38
|
+
scale: 0.8,
|
|
39
|
+
},
|
|
40
|
+
visible: {
|
|
41
|
+
opacity: 1,
|
|
42
|
+
scale: 1,
|
|
43
|
+
transition: FAST_SPATIAL_SPRING,
|
|
44
|
+
},
|
|
45
|
+
exit: {
|
|
46
|
+
opacity: 0,
|
|
47
|
+
scale: 0.8,
|
|
48
|
+
transition: FAST_EFFECTS_TRANSITION,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ─── Selected check icon ──────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** Size of the check icon in px (20dp per SegmentedMenuTokens.ItemLeadingIconSize) */
|
|
55
|
+
export const MENU_CHECK_ICON_SIZE = 20;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Expand/collapse animation for the check icon that appears when a MenuItem is
|
|
59
|
+
* selected. Uses horizontal expansion (width + marginInlineEnd) with FastSpatial.
|
|
60
|
+
*
|
|
61
|
+
* ONLY used for the animated check ↔ selectedIcon swap in selectable items.
|
|
62
|
+
* Regular (static) leading icons should NOT use these variants.
|
|
63
|
+
*/
|
|
64
|
+
export const CHECK_ICON_VARIANTS: Variants = {
|
|
65
|
+
hidden: {
|
|
66
|
+
opacity: 0,
|
|
67
|
+
width: 0,
|
|
68
|
+
},
|
|
69
|
+
visible: {
|
|
70
|
+
opacity: 1,
|
|
71
|
+
width: MENU_CHECK_ICON_SIZE,
|
|
72
|
+
transition: FAST_SPATIAL_SPRING,
|
|
73
|
+
},
|
|
74
|
+
exit: {
|
|
75
|
+
opacity: 0,
|
|
76
|
+
width: 0,
|
|
77
|
+
transition: FAST_EFFECTS_TRANSITION,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ─── SubMenu content ──────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/** SubMenu popup uses the same FastSpatial/FastEffects pattern as root menu */
|
|
84
|
+
export const SUBMENU_CONTAINER_VARIANTS: Variants = {
|
|
85
|
+
hidden: {
|
|
86
|
+
opacity: 0,
|
|
87
|
+
scale: 0.9,
|
|
88
|
+
x: -4,
|
|
89
|
+
},
|
|
90
|
+
visible: {
|
|
91
|
+
opacity: 1,
|
|
92
|
+
scale: 1,
|
|
93
|
+
x: 0,
|
|
94
|
+
transition: FAST_SPATIAL_SPRING,
|
|
95
|
+
},
|
|
96
|
+
exit: {
|
|
97
|
+
opacity: 0,
|
|
98
|
+
scale: 0.9,
|
|
99
|
+
x: -4,
|
|
100
|
+
transition: FAST_EFFECTS_TRANSITION,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ─── MD3 Expressive Menu — React Context ──────────────────────────────────────
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import type {
|
|
4
|
+
MenuColorVariant,
|
|
5
|
+
MenuPrimitive,
|
|
6
|
+
MenuVariant,
|
|
7
|
+
} from "./menu-types";
|
|
8
|
+
|
|
9
|
+
// ─── Context shape ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
interface MenuContextValue {
|
|
12
|
+
/** Visual variant: baseline (M3 standard) or expressive (shape-morphing) */
|
|
13
|
+
variant: MenuVariant;
|
|
14
|
+
/** Color variant inherited by all children unless overridden */
|
|
15
|
+
colorVariant: MenuColorVariant;
|
|
16
|
+
/**
|
|
17
|
+
* Which Radix primitive family drives this menu:
|
|
18
|
+
* - "dropdown" → @radix-ui/react-dropdown-menu (button/field trigger)
|
|
19
|
+
* - "context" → @radix-ui/react-context-menu (right-click trigger)
|
|
20
|
+
* - "static" → plain HTML via Slot (VerticalMenu, always-visible)
|
|
21
|
+
*/
|
|
22
|
+
menuPrimitive: MenuPrimitive;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the menu popup is currently open.
|
|
25
|
+
* Used by MenuContent to drive AnimatePresence for exit animations.
|
|
26
|
+
*/
|
|
27
|
+
open: boolean;
|
|
28
|
+
/** Setter forwarded from Menu root — kept in sync with Radix Root open state */
|
|
29
|
+
onOpenChange: (open: boolean) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Backward-compat derived getter ───────────────────────────────────────────
|
|
33
|
+
// Components that still reference `isStatic` (MenuGroup, etc.) use this helper
|
|
34
|
+
// during the incremental migration period.
|
|
35
|
+
export function isStaticPrimitive(primitive: MenuPrimitive): boolean {
|
|
36
|
+
return primitive === "static";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const MenuContext = React.createContext<MenuContextValue>({
|
|
42
|
+
variant: "baseline",
|
|
43
|
+
colorVariant: "standard",
|
|
44
|
+
menuPrimitive: "dropdown",
|
|
45
|
+
open: false,
|
|
46
|
+
onOpenChange: () => {},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ─── Provider ─────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export interface MenuProviderProps {
|
|
52
|
+
variant?: MenuVariant;
|
|
53
|
+
colorVariant?: MenuColorVariant;
|
|
54
|
+
menuPrimitive?: MenuPrimitive;
|
|
55
|
+
open: boolean;
|
|
56
|
+
onOpenChange: (open: boolean) => void;
|
|
57
|
+
children: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function MenuProvider({
|
|
61
|
+
variant = "baseline",
|
|
62
|
+
colorVariant = "standard",
|
|
63
|
+
menuPrimitive = "dropdown",
|
|
64
|
+
open,
|
|
65
|
+
onOpenChange,
|
|
66
|
+
children,
|
|
67
|
+
}: MenuProviderProps) {
|
|
68
|
+
const value = React.useMemo<MenuContextValue>(
|
|
69
|
+
() => ({ variant, colorVariant, menuPrimitive, open, onOpenChange }),
|
|
70
|
+
[variant, colorVariant, menuPrimitive, open, onOpenChange],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Hook ─────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns the nearest MenuContext value.
|
|
80
|
+
* Safe to use outside a MenuProvider (returns default: baseline, dropdown, closed).
|
|
81
|
+
*
|
|
82
|
+
* Includes backward-compat shims:
|
|
83
|
+
* - `menuVariant` → alias for `variant` (deprecated, will be removed)
|
|
84
|
+
* - `isStatic` → `menuPrimitive === "static"`
|
|
85
|
+
*/
|
|
86
|
+
export function useMenuContext(): MenuContextValue & {
|
|
87
|
+
isStatic: boolean;
|
|
88
|
+
menuVariant: MenuVariant;
|
|
89
|
+
} {
|
|
90
|
+
const ctx = React.useContext(MenuContext);
|
|
91
|
+
return React.useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
...ctx,
|
|
94
|
+
isStatic: ctx.menuPrimitive === "static",
|
|
95
|
+
menuVariant: ctx.variant,
|
|
96
|
+
}),
|
|
97
|
+
[ctx],
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ─── MD3 Expressive Menu — MenuDivider ───────────────────────────────────────
|
|
2
|
+
// Spec: HorizontalDividerPadding = PaddingValues(horizontal = 12.dp, vertical = 2.dp)
|
|
3
|
+
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { useMenuContext } from "./menu-context";
|
|
7
|
+
import type { MenuDividerProps } from "./menu-types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A horizontal divider for use between MenuItems or MenuGroups.
|
|
11
|
+
*
|
|
12
|
+
* Uses traditional line-based separation (as opposed to the gap-based
|
|
13
|
+
* separation in MenuGroup, which is the Expressive default).
|
|
14
|
+
*
|
|
15
|
+
* Spec: horizontal=12dp padding, vertical=2dp padding, `outline-variant` color.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <MenuContent>
|
|
19
|
+
* <MenuItem>Cut</MenuItem>
|
|
20
|
+
* <MenuDivider />
|
|
21
|
+
* <MenuItem>Paste</MenuItem>
|
|
22
|
+
* </MenuContent>
|
|
23
|
+
*/
|
|
24
|
+
export const MenuDivider = React.forwardRef<HTMLHRElement, MenuDividerProps>(
|
|
25
|
+
({ className, ...props }, ref) => {
|
|
26
|
+
const { menuVariant } = useMenuContext();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<DropdownMenu.Separator asChild>
|
|
30
|
+
<hr
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn(
|
|
33
|
+
// Baseline: 8dp vertical margin, 0 horizontal. Expressive: 12dp horizontal, 2dp vertical
|
|
34
|
+
menuVariant === "baseline" ? "my-2 mx-0" : "mx-3 my-0.5",
|
|
35
|
+
// 1px height line
|
|
36
|
+
"h-px border-0",
|
|
37
|
+
// outline-variant color
|
|
38
|
+
"bg-m3-outline-variant",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
</DropdownMenu.Separator>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
MenuDivider.displayName = "MenuDivider";
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// ─── MD3 Expressive Menu — MenuGroup ────────────────────────────────────────
|
|
2
|
+
// Gap-based grouping with shape morphing on hover (core Expressive feature)
|
|
3
|
+
import { m } from "motion/react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { FAST_SPATIAL_SPRING } from "./menu-animations";
|
|
7
|
+
|
|
8
|
+
import { useMenuContext } from "./menu-context";
|
|
9
|
+
import {
|
|
10
|
+
BASELINE_COLORS,
|
|
11
|
+
GROUP_SHAPES,
|
|
12
|
+
MENU_GROUP_PADDING_Y,
|
|
13
|
+
STANDARD_COLORS,
|
|
14
|
+
VIBRANT_COLORS,
|
|
15
|
+
} from "./menu-tokens";
|
|
16
|
+
import type {
|
|
17
|
+
MenuGroupPosition,
|
|
18
|
+
MenuGroupProps,
|
|
19
|
+
MenuItemPosition,
|
|
20
|
+
MenuItemProps,
|
|
21
|
+
} from "./menu-types";
|
|
22
|
+
|
|
23
|
+
// Extend MenuGroupProps with data-* attributes for testing and aria- attributes
|
|
24
|
+
type MenuGroupDivProps = MenuGroupProps & {
|
|
25
|
+
[key: `data-${string}`]: string | undefined;
|
|
26
|
+
id?: string;
|
|
27
|
+
"aria-label"?: string;
|
|
28
|
+
"aria-labelledby"?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─── Position helper ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function getGroupPosition(index: number, count: number): MenuGroupPosition {
|
|
34
|
+
if (count === 1) return "standalone";
|
|
35
|
+
if (index === 0) return "leading";
|
|
36
|
+
if (index === count - 1) return "trailing";
|
|
37
|
+
return "middle";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getGroupActiveShape(position: MenuGroupPosition): string {
|
|
41
|
+
return GROUP_SHAPES[
|
|
42
|
+
`${position}Active` as keyof typeof GROUP_SHAPES
|
|
43
|
+
] as string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── MenuGroup ────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A container that groups MenuItem elements with gap-based visual separation —
|
|
50
|
+
* the defining feature of MD3 Expressive menus.
|
|
51
|
+
*
|
|
52
|
+
* Shape morphing: on hover, the container's border-radius transitions from the
|
|
53
|
+
* "inactive" small shape to the "active" large shape via a FastSpatial spring.
|
|
54
|
+
* The shape depends on the group's position (leading/middle/trailing/standalone).
|
|
55
|
+
*
|
|
56
|
+
* MenuItem children automatically receive `itemPosition` props based on their
|
|
57
|
+
* index within the group.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* <MenuContent>
|
|
61
|
+
* <MenuGroup>
|
|
62
|
+
* <MenuItem>Cut</MenuItem>
|
|
63
|
+
* <MenuItem>Copy</MenuItem>
|
|
64
|
+
* <MenuItem>Paste</MenuItem>
|
|
65
|
+
* </MenuGroup>
|
|
66
|
+
* <MenuGroup>
|
|
67
|
+
* <MenuItem>Select All</MenuItem>
|
|
68
|
+
* </MenuGroup>
|
|
69
|
+
* </MenuContent>
|
|
70
|
+
*/
|
|
71
|
+
export const MenuGroup = React.forwardRef<HTMLDivElement, MenuGroupDivProps>(
|
|
72
|
+
(
|
|
73
|
+
{
|
|
74
|
+
children,
|
|
75
|
+
label,
|
|
76
|
+
index = 0,
|
|
77
|
+
count = 1,
|
|
78
|
+
colorVariant: propColorVariant,
|
|
79
|
+
isGapVariant,
|
|
80
|
+
itemPosition,
|
|
81
|
+
className,
|
|
82
|
+
...rest
|
|
83
|
+
},
|
|
84
|
+
ref,
|
|
85
|
+
) => {
|
|
86
|
+
const {
|
|
87
|
+
menuVariant,
|
|
88
|
+
colorVariant: contextColorVariant,
|
|
89
|
+
isStatic,
|
|
90
|
+
} = useMenuContext();
|
|
91
|
+
const colorVariant = propColorVariant ?? contextColorVariant;
|
|
92
|
+
const colors =
|
|
93
|
+
menuVariant === "baseline"
|
|
94
|
+
? BASELINE_COLORS
|
|
95
|
+
: colorVariant === "vibrant"
|
|
96
|
+
? VIBRANT_COLORS
|
|
97
|
+
: STANDARD_COLORS;
|
|
98
|
+
|
|
99
|
+
const position = getGroupPosition(index, count);
|
|
100
|
+
const activeShape = getGroupActiveShape(position);
|
|
101
|
+
|
|
102
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
103
|
+
const currentShape =
|
|
104
|
+
isStatic || isHovered ? activeShape : GROUP_SHAPES.inactive;
|
|
105
|
+
|
|
106
|
+
const handlePointerEnter = React.useCallback(() => setIsHovered(true), []);
|
|
107
|
+
const handlePointerLeave = React.useCallback(() => setIsHovered(false), []);
|
|
108
|
+
|
|
109
|
+
// Helper to recursively flatten fragments and collect valid elements.
|
|
110
|
+
// This is necessary because cloneElement cannot be used on React.Fragment.
|
|
111
|
+
const flattenChildren = (
|
|
112
|
+
children: React.ReactNode,
|
|
113
|
+
): React.ReactElement[] => {
|
|
114
|
+
return React.Children.toArray(children).reduce(
|
|
115
|
+
(acc: React.ReactElement[], child) => {
|
|
116
|
+
if (React.isValidElement(child)) {
|
|
117
|
+
if (child.type === React.Fragment) {
|
|
118
|
+
return acc.concat(
|
|
119
|
+
flattenChildren(
|
|
120
|
+
(child as React.ReactElement<{ children?: React.ReactNode }>)
|
|
121
|
+
.props.children,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
acc.push(child as React.ReactElement);
|
|
126
|
+
}
|
|
127
|
+
return acc;
|
|
128
|
+
},
|
|
129
|
+
[],
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const validChildren = flattenChildren(children);
|
|
134
|
+
const itemCount = validChildren.length;
|
|
135
|
+
|
|
136
|
+
const enhancedChildren = validChildren.map((child, i) => {
|
|
137
|
+
const itemPosition: MenuItemPosition =
|
|
138
|
+
itemCount === 1
|
|
139
|
+
? "standalone"
|
|
140
|
+
: i === 0
|
|
141
|
+
? "leading"
|
|
142
|
+
: i === itemCount - 1
|
|
143
|
+
? "trailing"
|
|
144
|
+
: "middle";
|
|
145
|
+
|
|
146
|
+
return React.cloneElement(child as React.ReactElement<MenuItemProps>, {
|
|
147
|
+
itemPosition,
|
|
148
|
+
colorVariant,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<m.div
|
|
154
|
+
ref={ref}
|
|
155
|
+
role="group"
|
|
156
|
+
aria-label={label}
|
|
157
|
+
className={cn(
|
|
158
|
+
"relative",
|
|
159
|
+
// In baseline variant, MenuGroup is transparent so it shouldn't clip.
|
|
160
|
+
// In expressive variant, it needs overflow-hidden to clip hover states to its morphing shape.
|
|
161
|
+
menuVariant === "baseline" ? "" : "overflow-hidden",
|
|
162
|
+
// Vertical padding: 2dp for gap variant (to match Figma), 4dp for baseline
|
|
163
|
+
isGapVariant ? "py-0.5" : MENU_GROUP_PADDING_Y,
|
|
164
|
+
// Horizontal padding: 4dp for expressive menus (both static and popup), 0 for baseline
|
|
165
|
+
menuVariant === "expressive" ? "px-1" : "",
|
|
166
|
+
// Gap variant has floating segments, so each group manages its own shadow
|
|
167
|
+
isGapVariant ? "elevation-2" : "",
|
|
168
|
+
// Background based on color variant (transparent for baseline to avoid double-layering)
|
|
169
|
+
menuVariant === "baseline" ? "bg-transparent" : colors.containerBg,
|
|
170
|
+
className,
|
|
171
|
+
)}
|
|
172
|
+
animate={{ borderRadius: currentShape }}
|
|
173
|
+
transition={FAST_SPATIAL_SPRING}
|
|
174
|
+
onPointerEnter={handlePointerEnter}
|
|
175
|
+
onPointerLeave={handlePointerLeave}
|
|
176
|
+
{...rest}
|
|
177
|
+
>
|
|
178
|
+
{/* Optional group label: labelSmall typography, 12dp horizontal padding */}
|
|
179
|
+
{label && (
|
|
180
|
+
<span
|
|
181
|
+
className={cn(
|
|
182
|
+
// Padding: 12dp top, 12dp horizontal, 8dp bottom (MD3 spec)
|
|
183
|
+
"block pt-3 px-3 pb-2",
|
|
184
|
+
"text-label-small",
|
|
185
|
+
menuVariant === "baseline"
|
|
186
|
+
? "text-m3-on-surface-variant"
|
|
187
|
+
: colorVariant === "vibrant"
|
|
188
|
+
? "text-m3-on-tertiary-container"
|
|
189
|
+
: "text-m3-on-surface-variant",
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{label}
|
|
193
|
+
</span>
|
|
194
|
+
)}
|
|
195
|
+
{enhancedChildren}
|
|
196
|
+
</m.div>
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
MenuGroup.displayName = "MenuGroup";
|