@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.
- package/.turbo/turbo-build.log +42 -0
- package/CHANGELOG.md +69 -0
- package/dist/index.css +178 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6135 -0
- package/dist/index.d.ts +6135 -71
- package/dist/index.js +1688 -631
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1600 -564
- 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/plugin.d.mts +1 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +13 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +3 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +28 -19
- package/scripts/copy-assets.js +115 -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 +195 -0
- package/src/lib/utils.ts +6 -0
- package/src/plugin.ts +12 -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 +306 -0
- package/src/ui/button.tsx +665 -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 +607 -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 +135 -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 +215 -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,382 @@
|
|
|
1
|
+
// ─── MD3 Expressive Menu — Vertical Menu (always-visible static variant) ──────
|
|
2
|
+
// Spec: SegmentedMenuTokens + MenuDefaults — two sub-variants:
|
|
3
|
+
// • Gap → gap-0.5 (2dp) between groups, no divider line
|
|
4
|
+
// • Divider → outline-variant <hr> between groups
|
|
5
|
+
//
|
|
6
|
+
// Key difference from Menu (baseline):
|
|
7
|
+
// • No portal, no popup animation, no trigger
|
|
8
|
+
// • Always rendered (static)
|
|
9
|
+
// • Shape morphing on hover works identically (reuses MenuGroup)
|
|
10
|
+
// • Still provides MenuContext with isStatic=true so MenuItems know to use Slot
|
|
11
|
+
//
|
|
12
|
+
// Vertical Menu Gap architecture:
|
|
13
|
+
// • Outer container: NO overflow-hidden (would clip group shape morphing!)
|
|
14
|
+
// Instead, elevation shadow is applied here, bg is transparent.
|
|
15
|
+
// • Each MenuGroup manages its own background + border-radius via Framer Motion.
|
|
16
|
+
// • The 2dp gap between groups is transparent — page background shows through.
|
|
17
|
+
//
|
|
18
|
+
// Vertical Menu Divider architecture:
|
|
19
|
+
// • Outer container applies containerBg + overflow-hidden + rounded-2xl
|
|
20
|
+
// (groups inside all stay flush, no shape morph needed for divider variant).
|
|
21
|
+
import * as React from "react";
|
|
22
|
+
import { cn } from "../../lib/utils";
|
|
23
|
+
import { MenuProvider, useMenuContext } from "./menu-context";
|
|
24
|
+
import { MenuGroup } from "./menu-group";
|
|
25
|
+
import {
|
|
26
|
+
MENU_GROUP_GAP,
|
|
27
|
+
MENU_MAX_WIDTH,
|
|
28
|
+
MENU_MIN_WIDTH,
|
|
29
|
+
STANDARD_COLORS,
|
|
30
|
+
VIBRANT_COLORS,
|
|
31
|
+
} from "./menu-tokens";
|
|
32
|
+
import type {
|
|
33
|
+
MenuGroupProps,
|
|
34
|
+
VerticalMenuContentProps,
|
|
35
|
+
VerticalMenuDividerProps,
|
|
36
|
+
VerticalMenuProps,
|
|
37
|
+
VerticalMenuSeparatorStyle,
|
|
38
|
+
} from "./menu-types";
|
|
39
|
+
|
|
40
|
+
// ─── VerticalMenuDivider ──────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A plain horizontal divider for use between groups in a VerticalMenuContent
|
|
44
|
+
* with `separatorStyle="divider"`.
|
|
45
|
+
*
|
|
46
|
+
* Uses the same visual spec as MenuDivider (outline-variant color, 12dp
|
|
47
|
+
* horizontal padding, 2dp vertical padding) but is a plain `<hr>` — no Radix
|
|
48
|
+
* dependency, safe inside static (non-popup) contexts.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* <VerticalMenuContent separatorStyle="divider">
|
|
52
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
53
|
+
* <VerticalMenuDivider />
|
|
54
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
55
|
+
* </VerticalMenuContent>
|
|
56
|
+
*/
|
|
57
|
+
export const VerticalMenuDivider = React.forwardRef<
|
|
58
|
+
HTMLHRElement,
|
|
59
|
+
VerticalMenuDividerProps & React.HTMLAttributes<HTMLHRElement>
|
|
60
|
+
>(({ className, index, count, isGapVariant, ...props }, ref) => (
|
|
61
|
+
<hr
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cn(
|
|
64
|
+
// HorizontalDividerPadding: horizontal=12dp, vertical=2dp
|
|
65
|
+
"mx-3 my-0.5",
|
|
66
|
+
// 1px height, no default border
|
|
67
|
+
"h-px border-0",
|
|
68
|
+
// Source: MenuDefaults.HorizontalDividerPadding / outline-variant
|
|
69
|
+
"bg-m3-outline-variant",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
));
|
|
75
|
+
VerticalMenuDivider.displayName = "VerticalMenuDivider";
|
|
76
|
+
|
|
77
|
+
// ─── VerticalMenuGroup ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A group within a VerticalMenu.
|
|
81
|
+
*
|
|
82
|
+
* Functionally identical to `MenuGroup` — shape morphing on hover, auto-injected
|
|
83
|
+
* `itemPosition` into MenuItem children, `colorVariant` from context.
|
|
84
|
+
*
|
|
85
|
+
* This component is a named re-export of `MenuGroup` so consumers of Vertical
|
|
86
|
+
* Menu have a semantically clear API without any code duplication.
|
|
87
|
+
*/
|
|
88
|
+
export const VerticalMenuGroup = MenuGroup;
|
|
89
|
+
|
|
90
|
+
// ─── VerticalMenuContent ──────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Renders a vertical list of VerticalMenuGroup children.
|
|
94
|
+
*
|
|
95
|
+
* Handles two separation styles:
|
|
96
|
+
* - `gap` → 2dp (`gap-0.5`) visual gap between groups (default)
|
|
97
|
+
* - `divider` → auto-inserts a `VerticalMenuDivider` between each pair of groups
|
|
98
|
+
*
|
|
99
|
+
* Auto-injects `index` and `count` props into VerticalMenuGroup children so
|
|
100
|
+
* that position-based shape morphing (leading/middle/trailing) works correctly.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Gap variant (default)
|
|
104
|
+
* <VerticalMenuContent separatorStyle="gap">
|
|
105
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
106
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
107
|
+
* </VerticalMenuContent>
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* // Divider variant
|
|
111
|
+
* <VerticalMenuContent separatorStyle="divider">
|
|
112
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
113
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
114
|
+
* </VerticalMenuContent>
|
|
115
|
+
*/
|
|
116
|
+
export const VerticalMenuContent = React.forwardRef<
|
|
117
|
+
HTMLDivElement,
|
|
118
|
+
VerticalMenuContentProps & React.HTMLAttributes<HTMLDivElement>
|
|
119
|
+
>(
|
|
120
|
+
(
|
|
121
|
+
{
|
|
122
|
+
children,
|
|
123
|
+
separatorStyle = "gap",
|
|
124
|
+
colorVariant: propColorVariant,
|
|
125
|
+
className,
|
|
126
|
+
...props
|
|
127
|
+
},
|
|
128
|
+
ref,
|
|
129
|
+
) => {
|
|
130
|
+
const { colorVariant: contextColorVariant } = useMenuContext();
|
|
131
|
+
const colorVariant = propColorVariant ?? contextColorVariant;
|
|
132
|
+
const colors =
|
|
133
|
+
colorVariant === "vibrant" ? VIBRANT_COLORS : STANDARD_COLORS;
|
|
134
|
+
|
|
135
|
+
// Helper to recursively flatten fragments and collect valid elements.
|
|
136
|
+
// This is necessary because cloneElement cannot be used on React.Fragment.
|
|
137
|
+
const flattenChildren = (
|
|
138
|
+
children: React.ReactNode,
|
|
139
|
+
): React.ReactElement[] => {
|
|
140
|
+
return React.Children.toArray(children).reduce(
|
|
141
|
+
(acc: React.ReactElement[], child) => {
|
|
142
|
+
if (React.isValidElement(child)) {
|
|
143
|
+
if (child.type === React.Fragment) {
|
|
144
|
+
return acc.concat(
|
|
145
|
+
flattenChildren(
|
|
146
|
+
(child as React.ReactElement<{ children?: React.ReactNode }>)
|
|
147
|
+
.props.children,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
acc.push(child as React.ReactElement);
|
|
152
|
+
}
|
|
153
|
+
return acc;
|
|
154
|
+
},
|
|
155
|
+
[],
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Collect only valid VerticalMenuGroup-like elements for index/count injection
|
|
160
|
+
const validChildren = flattenChildren(children);
|
|
161
|
+
const groupCount = validChildren.length;
|
|
162
|
+
|
|
163
|
+
// Auto-inject `index` and `count` into each child (enables leading/middle/trailing shape)
|
|
164
|
+
const enhancedChildren = validChildren.map((child, i) =>
|
|
165
|
+
React.cloneElement(child as React.ReactElement<MenuGroupProps>, {
|
|
166
|
+
index: i,
|
|
167
|
+
count: groupCount,
|
|
168
|
+
isGapVariant: separatorStyle === "gap",
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// For divider style: interleave VerticalMenuDivider between each group
|
|
173
|
+
const renderedChildren =
|
|
174
|
+
separatorStyle === "divider"
|
|
175
|
+
? enhancedChildren.reduce<React.ReactNode[]>((acc, child, i) => {
|
|
176
|
+
if (i > 0) {
|
|
177
|
+
acc.push(
|
|
178
|
+
<VerticalMenuDivider
|
|
179
|
+
key={`divider-${(child as React.ReactElement).key || i}`}
|
|
180
|
+
/>,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
acc.push(child);
|
|
184
|
+
return acc;
|
|
185
|
+
}, [])
|
|
186
|
+
: enhancedChildren;
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
ref={ref}
|
|
191
|
+
className={cn(
|
|
192
|
+
"flex flex-col w-full",
|
|
193
|
+
// Gap variant: transparent background + 2dp gap — page bg shows through gaps.
|
|
194
|
+
// Divider variant: solid container background behind all groups.
|
|
195
|
+
separatorStyle === "gap"
|
|
196
|
+
? cn("bg-transparent", MENU_GROUP_GAP)
|
|
197
|
+
: colors.containerBg,
|
|
198
|
+
className,
|
|
199
|
+
)}
|
|
200
|
+
{...props}
|
|
201
|
+
>
|
|
202
|
+
{renderedChildren}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
VerticalMenuContent.displayName = "VerticalMenuContent";
|
|
208
|
+
|
|
209
|
+
// ─── VerticalMenu (Root) ──────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Root of an always-visible vertical menu.
|
|
213
|
+
*
|
|
214
|
+
* Wraps children in `MenuProvider` (always `open={true}`) so MenuItem and
|
|
215
|
+
* MenuGroup components receive the correct `colorVariant` from context.
|
|
216
|
+
*
|
|
217
|
+
* Unlike the popup `Menu` component, there is no Radix DropdownMenu, no portal,
|
|
218
|
+
* and no enter/exit animation — the list is statically rendered at all times.
|
|
219
|
+
*
|
|
220
|
+
* ### Shape morphing
|
|
221
|
+
* For the **gap variant**, the outer container has NO `overflow-hidden` — this is
|
|
222
|
+
* intentional! Each `VerticalMenuGroup` manages its own shape via Framer Motion's
|
|
223
|
+
* `animate.borderRadius`. `overflow-hidden` on the parent would clip these
|
|
224
|
+
* morphing corners. The 2dp transparent gap lets page background show through.
|
|
225
|
+
*
|
|
226
|
+
* For the **divider variant**, the outer container applies `overflow-hidden` +
|
|
227
|
+
* `rounded-2xl` + background — groups sit flush inside without morphing.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // Vertical Menu with Gap (floating segments)
|
|
231
|
+
* <VerticalMenu colorVariant="standard">
|
|
232
|
+
* <VerticalMenuContent separatorStyle="gap">
|
|
233
|
+
* <VerticalMenuGroup>
|
|
234
|
+
* <MenuItem leadingIcon={<Icon name="visibility" size={20} />}>Item 1</MenuItem>
|
|
235
|
+
* <MenuItem leadingIcon={<Icon name="content_copy" size={20} />} trailingText="⌘C">Item 2</MenuItem>
|
|
236
|
+
* <MenuItem selected leadingIcon={<Icon name="edit" size={20} />}>Item 3</MenuItem>
|
|
237
|
+
* </VerticalMenuGroup>
|
|
238
|
+
* <VerticalMenuGroup>
|
|
239
|
+
* <MenuItem trailingIcon={<Icon name="chevron_right" size={20} />}>Item 4</MenuItem>
|
|
240
|
+
* </VerticalMenuGroup>
|
|
241
|
+
* </VerticalMenuContent>
|
|
242
|
+
* </VerticalMenu>
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* // Vertical Menu with Divider
|
|
246
|
+
* <VerticalMenu colorVariant="standard">
|
|
247
|
+
* <VerticalMenuContent separatorStyle="divider">
|
|
248
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
249
|
+
* <VerticalMenuGroup>...</VerticalMenuGroup>
|
|
250
|
+
* </VerticalMenuContent>
|
|
251
|
+
* </VerticalMenu>
|
|
252
|
+
*/
|
|
253
|
+
export const VerticalMenu = React.forwardRef<
|
|
254
|
+
HTMLDivElement,
|
|
255
|
+
VerticalMenuProps & React.HTMLAttributes<HTMLDivElement>
|
|
256
|
+
>(({ children, colorVariant = "standard", className, ...props }, ref) => {
|
|
257
|
+
// Vertical menus are always visible — `open` is a permanent true.
|
|
258
|
+
// onOpenChange is a no-op (no controlled/uncontrolled toggle needed).
|
|
259
|
+
const noop = React.useCallback(() => {}, []);
|
|
260
|
+
|
|
261
|
+
const colors = colorVariant === "vibrant" ? VIBRANT_COLORS : STANDARD_COLORS;
|
|
262
|
+
|
|
263
|
+
// Internal ref for keyboard navigation — merged with forwarded ref.
|
|
264
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
265
|
+
const mergedRef = React.useCallback(
|
|
266
|
+
(node: HTMLDivElement | null) => {
|
|
267
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current =
|
|
268
|
+
node;
|
|
269
|
+
if (typeof ref === "function") ref(node);
|
|
270
|
+
else if (ref)
|
|
271
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
272
|
+
},
|
|
273
|
+
[ref],
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Arrow key navigation: WAI-ARIA composite widget pattern.
|
|
277
|
+
// Moves focus among menuitem elements, skipping disabled ones.
|
|
278
|
+
const handleKeyDown = React.useCallback(
|
|
279
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
280
|
+
if (!containerRef.current) return;
|
|
281
|
+
const items = Array.from(
|
|
282
|
+
containerRef.current.querySelectorAll<HTMLElement>(
|
|
283
|
+
'[role="menuitem"]:not([aria-disabled="true"]):not([tabindex="-1"]),' +
|
|
284
|
+
'[role="menuitemcheckbox"]:not([aria-disabled="true"]):not([tabindex="-1"]),' +
|
|
285
|
+
'[role="menuitemradio"]:not([aria-disabled="true"]):not([tabindex="-1"])',
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
if (!items.length) return;
|
|
289
|
+
const idx = items.indexOf(document.activeElement as HTMLElement);
|
|
290
|
+
let next: number | null = null;
|
|
291
|
+
switch (e.key) {
|
|
292
|
+
case "ArrowDown":
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
next = idx < items.length - 1 ? idx + 1 : 0;
|
|
295
|
+
break;
|
|
296
|
+
case "ArrowUp":
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
next = idx > 0 ? idx - 1 : items.length - 1;
|
|
299
|
+
break;
|
|
300
|
+
case "Home":
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
next = 0;
|
|
303
|
+
break;
|
|
304
|
+
case "End":
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
next = items.length - 1;
|
|
307
|
+
break;
|
|
308
|
+
default:
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (next !== null) items[next]?.focus();
|
|
312
|
+
},
|
|
313
|
+
[],
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Detect separator style from VerticalMenuContent child to decide container styling.
|
|
317
|
+
const separatorStyle = detectSeparatorStyle(children);
|
|
318
|
+
const isGapVariant = separatorStyle !== "divider";
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<MenuProvider
|
|
322
|
+
variant="expressive"
|
|
323
|
+
colorVariant={colorVariant}
|
|
324
|
+
menuPrimitive="static"
|
|
325
|
+
open={true}
|
|
326
|
+
onOpenChange={noop}
|
|
327
|
+
>
|
|
328
|
+
<div
|
|
329
|
+
ref={mergedRef}
|
|
330
|
+
role="menu"
|
|
331
|
+
aria-orientation="vertical"
|
|
332
|
+
onKeyDown={handleKeyDown}
|
|
333
|
+
className={cn(
|
|
334
|
+
// Width constraints: 112dp min, 280dp max (MenuTokens)
|
|
335
|
+
MENU_MIN_WIDTH,
|
|
336
|
+
MENU_MAX_WIDTH,
|
|
337
|
+
"flex flex-col",
|
|
338
|
+
isGapVariant
|
|
339
|
+
? [
|
|
340
|
+
// GAP VARIANT: NO overflow-hidden — groups must morph freely.
|
|
341
|
+
"outline-none",
|
|
342
|
+
// NO background — transparent between segments.
|
|
343
|
+
// NO rounded corners — each group manages its own shape.
|
|
344
|
+
// Elevation is managed by each individual group.
|
|
345
|
+
]
|
|
346
|
+
: [
|
|
347
|
+
// DIVIDER VARIANT: Container clips the content.
|
|
348
|
+
"rounded-2xl",
|
|
349
|
+
"overflow-hidden",
|
|
350
|
+
colors.containerBg,
|
|
351
|
+
"elevation-2",
|
|
352
|
+
"outline-none",
|
|
353
|
+
],
|
|
354
|
+
className,
|
|
355
|
+
)}
|
|
356
|
+
{...props}
|
|
357
|
+
>
|
|
358
|
+
{children}
|
|
359
|
+
</div>
|
|
360
|
+
</MenuProvider>
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
VerticalMenu.displayName = "VerticalMenu";
|
|
364
|
+
|
|
365
|
+
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Inspects the `separatorStyle` prop from the first VerticalMenuContent child.
|
|
369
|
+
* Used by VerticalMenu root to decide container styling.
|
|
370
|
+
*/
|
|
371
|
+
function detectSeparatorStyle(
|
|
372
|
+
children: React.ReactNode,
|
|
373
|
+
): VerticalMenuSeparatorStyle {
|
|
374
|
+
const child = React.Children.toArray(children).find(React.isValidElement);
|
|
375
|
+
if (child) {
|
|
376
|
+
const style = (
|
|
377
|
+
child.props as { separatorStyle?: VerticalMenuSeparatorStyle }
|
|
378
|
+
).separatorStyle;
|
|
379
|
+
if (style) return style;
|
|
380
|
+
}
|
|
381
|
+
return "gap";
|
|
382
|
+
}
|