@bug-on/md3-react 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +42 -42
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.css +107 -0
  4. package/dist/index.d.mts +1491 -1053
  5. package/dist/index.d.ts +1491 -1053
  6. package/dist/index.js +4457 -3156
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4394 -3109
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +11 -6
  11. package/scripts/copy-assets.js +113 -8
  12. package/src/index.ts +66 -18
  13. package/src/test/button.test.tsx +1 -1
  14. package/src/ui/app-bar/app-bar.tokens.ts +5 -24
  15. package/src/ui/badge.tsx +2 -1
  16. package/src/ui/buttons/button/button-tokens.ts +118 -0
  17. package/src/ui/{button.test.tsx → buttons/button/button.test.tsx} +0 -21
  18. package/src/ui/buttons/button/button.tsx +381 -0
  19. package/src/ui/buttons/button/index.ts +3 -0
  20. package/src/ui/buttons/button/types.ts +90 -0
  21. package/src/ui/buttons/button-group/button-group-defaults.ts +95 -0
  22. package/src/ui/buttons/button-group/button-group-tokens.ts +20 -0
  23. package/src/ui/{button-group.test.tsx → buttons/button-group/button-group.test.tsx} +9 -10
  24. package/src/ui/buttons/button-group/button-group.tsx +699 -0
  25. package/src/ui/buttons/button-group/index.ts +8 -0
  26. package/src/ui/buttons/button-group/types.ts +77 -0
  27. package/src/ui/{fab.tsx → buttons/fabs/fab/fab.tsx} +6 -6
  28. package/src/ui/buttons/fabs/fab/index.ts +1 -0
  29. package/src/ui/{fab-menu.tsx → buttons/fabs/fab-menu/fab-menu.tsx} +7 -4
  30. package/src/ui/buttons/fabs/fab-menu/index.ts +1 -0
  31. package/src/ui/buttons/fabs/index.ts +2 -0
  32. package/src/ui/{icon-button.tsx → buttons/icon-button/icon-button.tsx} +6 -6
  33. package/src/ui/buttons/icon-button/index.ts +1 -0
  34. package/src/ui/buttons/index.ts +4 -0
  35. package/src/ui/code-block.tsx +1 -1
  36. package/src/ui/dialog.tsx +4 -7
  37. package/src/ui/drawer.tsx +4 -7
  38. package/src/ui/menu/menu-animations.ts +14 -20
  39. package/src/ui/menu/menu-tokens.ts +7 -5
  40. package/src/ui/menu/menu.test.tsx +9 -4
  41. package/src/ui/navigation-bar.test.tsx +111 -0
  42. package/src/ui/navigation-bar.tsx +464 -0
  43. package/src/ui/navigation-rail.test.tsx +5 -4
  44. package/src/ui/navigation-rail.tsx +32 -23
  45. package/src/ui/scroll-area.tsx +4 -0
  46. package/src/ui/search/search-view-fullscreen.tsx +1 -1
  47. package/src/ui/search/search.tokens.ts +9 -43
  48. package/src/ui/search/trailing-action.tsx +1 -1
  49. package/src/ui/shared/constants.ts +25 -27
  50. package/src/ui/shared/motion-tokens.ts +238 -0
  51. package/src/ui/snackbar/snackbar.tsx +4 -6
  52. package/src/ui/switch/switch.tsx +12 -18
  53. package/src/ui/text-field/text-field.tokens.ts +12 -12
  54. package/src/ui/text-field/text-field.tsx +31 -19
  55. package/src/ui/theme-provider/index.tsx +1 -5
  56. package/src/ui/toc.tsx +1 -1
  57. package/src/ui/toolbar/__snapshots__/bottom-docked-toolbar.test.tsx.snap +51 -0
  58. package/src/ui/toolbar/__snapshots__/floating-toolbar-with-fab.test.tsx.snap +113 -0
  59. package/src/ui/toolbar/__snapshots__/floating-toolbar.test.tsx.snap +169 -0
  60. package/src/ui/toolbar/bottom-docked-toolbar.test.tsx +114 -0
  61. package/src/ui/toolbar/docked-toolbar.tsx +186 -0
  62. package/src/ui/toolbar/floating-toolbar-with-fab.test.tsx +139 -0
  63. package/src/ui/toolbar/floating-toolbar-with-fab.tsx +199 -0
  64. package/src/ui/toolbar/floating-toolbar.test.tsx +230 -0
  65. package/src/ui/toolbar/floating-toolbar.tsx +344 -0
  66. package/src/ui/toolbar/index.ts +35 -0
  67. package/src/ui/toolbar/toolbar-colors.ts +37 -0
  68. package/src/ui/toolbar/toolbar-context.tsx +13 -0
  69. package/src/ui/toolbar/toolbar-divider.test.tsx +54 -0
  70. package/src/ui/toolbar/toolbar-divider.tsx +73 -0
  71. package/src/ui/toolbar/toolbar-icon-button.test.tsx +68 -0
  72. package/src/ui/toolbar/toolbar-icon-button.tsx +136 -0
  73. package/src/ui/toolbar/toolbar-scroll-behavior.ts +140 -0
  74. package/src/ui/toolbar/toolbar-tokens.ts +51 -0
  75. package/test-clip.html +31 -0
  76. package/test-shadow.html +5 -1
  77. package/test-width.html +34 -0
  78. package/src/ui/button-group.tsx +0 -350
  79. package/src/ui/button.tsx +0 -665
  80. package/test-render.tsx +0 -4
  81. package/test_output.txt +0 -164
  82. package/test_output_v2.txt +0 -5
  83. /package/src/ui/{fab.test.tsx → buttons/fabs/fab/fab.test.tsx} +0 -0
  84. /package/src/ui/{fab-menu.test.tsx → buttons/fabs/fab-menu/fab-menu.test.tsx} +0 -0
  85. /package/src/ui/{icon-button.test.tsx → buttons/icon-button/icon-button.test.tsx} +0 -0
  86. /package/src/ui/{Text.tsx → text.tsx} +0 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @file toolbar-icon-button.tsx
3
+ *
4
+ * A thin wrapper around the MD3 `IconButton` component, pre-configured
5
+ * for use inside Toolbar slots.
6
+ *
7
+ * Supports the three emphasis variants described in the MD3 Flexibility & slots
8
+ * spec, plus narrow/wide sizing for asymmetric visual hierarchy.
9
+ *
10
+ * @see https://m3.material.io/components/toolbars/guidelines (Anatomy → Flexibility & slots)
11
+ * @see https://m3.material.io/components/icon-buttons/overview
12
+ */
13
+
14
+ import * as React from "react";
15
+
16
+ import { cn } from "../../lib/utils";
17
+ import { type BaseIconButtonProps, IconButton } from "../buttons/icon-button";
18
+ import { ToolbarIconButtonTokens } from "./toolbar-tokens";
19
+
20
+ export interface ToolbarIconButtonProps
21
+ extends Omit<BaseIconButtonProps, "colorStyle" | "size" | "shape"> {
22
+ /**
23
+ * Visual emphasis variant.
24
+ *
25
+ * ⚠️ Avoid emphasising more than one action at a time.
26
+ * @default "standard"
27
+ */
28
+ emphasis?: ToolbarIconButtonVariant;
29
+ /**
30
+ * Button width sizing.
31
+ *
32
+ * Height is always `48px` to meet MD3 touch-target requirements.
33
+ * @default "default"
34
+ */
35
+ toolbarSize?: ToolbarIconButtonSize;
36
+ /** Icon content — typically a single SVG icon component. */
37
+ children: React.ReactNode;
38
+ /**
39
+ * Accessible label — **REQUIRED** because icon buttons have no visible text.
40
+ */
41
+ "aria-label": string;
42
+ }
43
+
44
+ /**
45
+ * Width sizing for a toolbar icon button.
46
+ *
47
+ * MD3 guideline: *"Use wide and narrow icon buttons"* to create visual hierarchy.
48
+ * Touch-target height is always 48dp regardless of width.
49
+ *
50
+ * - `narrow` → 40px wide (secondary, de-emphasised)
51
+ * - `default` → 48px wide (standard square touch target)
52
+ * - `wide` → 64px wide (primary emphasis, draws eye)
53
+ */
54
+ export type ToolbarIconButtonSize = "narrow" | "default" | "wide";
55
+
56
+ /**
57
+ * Visual emphasis variant for a toolbar icon button.
58
+ *
59
+ * MD3 guideline: *"Use different icon button color styles, such as filled,
60
+ * tonal, and standard."*
61
+ *
62
+ * ⚠️ **Avoid emphasising more than one action at a time.**
63
+ * Use `"filled"` or `"tonal"` for the single highest-priority action and
64
+ * keep all other buttons as `"standard"`.
65
+ */
66
+ export type ToolbarIconButtonVariant = "standard" | "tonal" | "filled";
67
+
68
+ const TOOLBAR_ICON_BUTTON_WIDTH: Record<ToolbarIconButtonSize, string> = {
69
+ narrow: `w-[${ToolbarIconButtonTokens.NarrowWidth}px]`,
70
+ default: `w-[${ToolbarIconButtonTokens.DefaultWidth}px]`,
71
+ wide: `w-[${ToolbarIconButtonTokens.WideWidth}px]`,
72
+ };
73
+
74
+ const VARIANT_TO_COLOR_STYLE: Record<
75
+ ToolbarIconButtonVariant,
76
+ "standard" | "filled" | "tonal"
77
+ > = {
78
+ standard: "standard",
79
+ tonal: "tonal",
80
+ filled: "filled",
81
+ };
82
+
83
+ /**
84
+ * An MD3 icon button optimised for use inside Toolbar slots.
85
+ *
86
+ * Wraps the project's `IconButton` and adds:
87
+ * - Three emphasis styles (`standard`, `tonal`, `filled`) via `emphasis` prop.
88
+ * - Three width sizes (`narrow`, `default`, `wide`) for visual hierarchy.
89
+ * - Always `rounded-full` shape (required for floating toolbars per MD3 spec).
90
+ * - Always 48dp height to meet MD3 accessibility touch-target requirements.
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * // Standard (default) — use for most actions
95
+ * <ToolbarIconButton aria-label="Share">
96
+ * <ShareIcon />
97
+ * </ToolbarIconButton>
98
+ *
99
+ * // Filled + wide — use for the single highest-priority action
100
+ * <ToolbarIconButton emphasis="filled" toolbarSize="wide" aria-label="Add">
101
+ * <PlusIcon />
102
+ * </ToolbarIconButton>
103
+ * ```
104
+ *
105
+ * @see https://m3.material.io/components/toolbars/guidelines
106
+ */
107
+ export const ToolbarIconButton = React.forwardRef<
108
+ HTMLButtonElement,
109
+ ToolbarIconButtonProps
110
+ >(
111
+ (
112
+ { emphasis = "standard", toolbarSize = "default", className, ...props },
113
+ ref,
114
+ ) => {
115
+ const widthClass = TOOLBAR_ICON_BUTTON_WIDTH[toolbarSize];
116
+ const colorStyle = VARIANT_TO_COLOR_STYLE[emphasis];
117
+
118
+ return (
119
+ <IconButton
120
+ ref={ref}
121
+ variant="default"
122
+ colorStyle={colorStyle}
123
+ size="md"
124
+ shape="round"
125
+ className={cn(
126
+ `h-[${ToolbarIconButtonTokens.Height}px]`,
127
+ widthClass,
128
+ className,
129
+ )}
130
+ {...props}
131
+ />
132
+ );
133
+ },
134
+ );
135
+
136
+ ToolbarIconButton.displayName = "ToolbarIconButton";
@@ -0,0 +1,140 @@
1
+ import { useMotionValueEvent, useScroll } from "motion/react";
2
+ import { type RefObject, useEffect, useRef, useState } from "react";
3
+
4
+ /**
5
+ * Configuration options for the `useFloatingToolbarScrollBehavior` hook.
6
+ */
7
+ export interface UseFloatingToolbarScrollBehaviorOptions {
8
+ /** Direction toolbar exits: 'top' | 'bottom' | 'start' | 'end'. Defaults to 'bottom'. */
9
+ exitDirection?: "top" | "bottom" | "start" | "end";
10
+ /** Scroll distance threshold to trigger collapse (in px). Defaults to 10. */
11
+ collapseThreshold?: number;
12
+ /** Scroll distance threshold to trigger expand (in px). Defaults to 10. */
13
+ expandThreshold?: number;
14
+ /** Optional ref to a scrollable container. If not provided, it listens to window scroll. */
15
+ scrollContainerRef?: RefObject<HTMLElement | null>;
16
+ }
17
+
18
+ /**
19
+ * The resulting behavior object returned by `useFloatingToolbarScrollBehavior`.
20
+ */
21
+ export interface FloatingToolbarScrollBehavior {
22
+ /** Current offset. 0 is fully visible, -1 is fully hidden. */
23
+ offset: number;
24
+ /** Whether the toolbar is currently expanded. */
25
+ isExpanded: boolean;
26
+ /** Optional scroll handler to bind to a React scrollable container (legacy fallback). */
27
+ onScroll?: (event: React.UIEvent<HTMLElement>) => void;
28
+ /** Manually update the expanded state of the toolbar. */
29
+ setExpanded: (expanded: boolean) => void;
30
+ /** The configured exit direction. */
31
+ exitDirection: "top" | "bottom" | "start" | "end";
32
+ }
33
+
34
+ /**
35
+ * A hook that provides scroll behavior for floating toolbars, allowing them to
36
+ * collapse (exit) when scrolling down and expand (enter) when scrolling up.
37
+ * Leverages Framer Motion's useScroll internally for optimized performance.
38
+ *
39
+ * @param options Configuration options for scroll behavior.
40
+ * @returns An object containing the current scroll state and handlers.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const scrollBehavior = useFloatingToolbarScrollBehavior({
45
+ * exitDirection: 'bottom',
46
+ * collapseThreshold: 20
47
+ * });
48
+ *
49
+ * <HorizontalFloatingToolbar scrollBehavior={scrollBehavior}>
50
+ * <IconButton icon="bold" />
51
+ * </HorizontalFloatingToolbar>
52
+ * ```
53
+ */
54
+ export function useFloatingToolbarScrollBehavior(
55
+ options: UseFloatingToolbarScrollBehaviorOptions = {},
56
+ ): FloatingToolbarScrollBehavior {
57
+ const {
58
+ exitDirection = "bottom",
59
+ collapseThreshold = 10,
60
+ expandThreshold = 10,
61
+ scrollContainerRef,
62
+ } = options;
63
+
64
+ const [isExpanded, setIsExpanded] = useState(true);
65
+ const [offset, setOffset] = useState(0); // 0 to -1
66
+
67
+ const { scrollY } = useScroll({
68
+ container: scrollContainerRef,
69
+ });
70
+
71
+ const lastScrollY = useRef(0);
72
+ const accumulatedScroll = useRef(0);
73
+
74
+ useMotionValueEvent(scrollY, "change", (currentScrollY) => {
75
+ const deltaY = currentScrollY - lastScrollY.current;
76
+
77
+ // Ignore bouncing (negative scroll or past max scroll on macOS)
78
+ if (currentScrollY < 0) return;
79
+
80
+ lastScrollY.current = currentScrollY;
81
+
82
+ // Only accumulate if we are scrolling in the same direction
83
+ if (Math.sign(deltaY) !== Math.sign(accumulatedScroll.current)) {
84
+ accumulatedScroll.current = deltaY;
85
+ } else {
86
+ accumulatedScroll.current += deltaY;
87
+ }
88
+
89
+ if (deltaY > 0 && isExpanded) {
90
+ // Scrolling down -> collapse
91
+ if (accumulatedScroll.current > collapseThreshold) {
92
+ setIsExpanded(false);
93
+ setOffset(-1);
94
+ }
95
+ } else if (deltaY < 0 && !isExpanded) {
96
+ // Scrolling up -> expand
97
+ if (Math.abs(accumulatedScroll.current) > expandThreshold) {
98
+ setIsExpanded(true);
99
+ setOffset(0);
100
+ }
101
+ }
102
+ });
103
+
104
+ // Accessibility: Always force expanded if forced-colors mode or accessibility preferences
105
+ useEffect(() => {
106
+ if (typeof window !== "undefined") {
107
+ const matchMedia = window.matchMedia("(forced-colors: active)");
108
+ if (matchMedia.matches) {
109
+ setIsExpanded(true);
110
+ setOffset(0);
111
+ }
112
+
113
+ const listener = (e: MediaQueryListEvent) => {
114
+ if (e.matches) {
115
+ setIsExpanded(true);
116
+ setOffset(0);
117
+ }
118
+ };
119
+
120
+ matchMedia.addEventListener("change", listener);
121
+ return () => matchMedia.removeEventListener("change", listener);
122
+ }
123
+ }, []);
124
+
125
+ const setExpanded = (expanded: boolean) => {
126
+ setIsExpanded(expanded);
127
+ setOffset(expanded ? 0 : -1);
128
+ };
129
+
130
+ // Dummy onScroll for legacy compatibility if users still attach it
131
+ const onScroll = () => {};
132
+
133
+ return {
134
+ offset,
135
+ isExpanded,
136
+ onScroll,
137
+ setExpanded,
138
+ exitDirection,
139
+ };
140
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Design tokens for the MD3 Expressive Toolbars components.
3
+ * Extracted from `FloatingToolbarTokens.kt` and `FabBaselineTokens.kt`.
4
+ */
5
+
6
+ export const FloatingToolbarTokens = {
7
+ ContainerBetweenSpace: 4,
8
+ ContainerExternalPadding: 16,
9
+ ContainerHeight: 64,
10
+ ContainerLeadingSpace: 8,
11
+ ContainerShape: "full",
12
+ ContainerTrailingSpace: 8,
13
+ VerticalContainerExternalPadding: 24,
14
+ } as const;
15
+
16
+ export const FabBaselineTokens = {
17
+ ContainerHeight: 56,
18
+ ContainerWidth: 56,
19
+ MediumContainerHeight: 80,
20
+ MediumContainerWidth: 80,
21
+ IconSize: 24,
22
+ MediumIconSize: 28,
23
+ } as const;
24
+
25
+ export const ToolbarToFabGap = 8;
26
+
27
+ /**
28
+ * Design tokens for the ToolbarDivider component.
29
+ * The divider is a decorative separator used to group toolbar actions.
30
+ */
31
+ export const ToolbarDividerTokens = {
32
+ /** Divider line thickness in px */
33
+ Thickness: 1,
34
+ /** Divider size as a fraction of the toolbar container dimension */
35
+ HeightRatio: 0.5,
36
+ } as const;
37
+
38
+ /**
39
+ * Design tokens for the ToolbarIconButton component.
40
+ * Sizes follow MD3 Expressive touch-target minimums (48dp height always).
41
+ * - narrow: de-emphasised secondary actions
42
+ * - default: standard icon button
43
+ * - wide: primary action emphasis inside the toolbar
44
+ */
45
+ export const ToolbarIconButtonTokens = {
46
+ NarrowWidth: 40,
47
+ DefaultWidth: 48,
48
+ WideWidth: 64,
49
+ /** Minimum touch-target height per MD3 accessibility guidelines */
50
+ Height: 48,
51
+ } as const;
package/test-clip.html ADDED
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Test Clip - MD3 Expressive</title>
5
+ <meta name="description" content="Test file for clipping behavior in MD3 Expressive components.">
6
+ <meta property="og:title" content="Test Clip - MD3 Expressive">
7
+ <meta property="og:description" content="Test file for clipping behavior in MD3 Expressive components.">
8
+ <style>
9
+ .wrapper {
10
+ display: flex;
11
+ align-items: center;
12
+ overflow: hidden;
13
+ width: fit-content;
14
+ height: 64px;
15
+ background: rgba(255,0,0,0.2);
16
+ }
17
+ .inner {
18
+ width: 200px;
19
+ height: 64px;
20
+ border-radius: 32px;
21
+ background: blue;
22
+ box-shadow: 0 4px 6px rgba(0,0,0,0.5);
23
+ }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <div class="wrapper">
28
+ <div class="inner"></div>
29
+ </div>
30
+ </body>
31
+ </html>
package/test-shadow.html CHANGED
@@ -1,7 +1,11 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <style>
4
+ <title>Test Shadow - MD3 Expressive</title>
5
+ <meta name="description" content="Test file for shadow behavior in MD3 Expressive components.">
6
+ <meta property="og:title" content="Test Shadow - MD3 Expressive">
7
+ <meta property="og:description" content="Test file for shadow behavior in MD3 Expressive components.">
8
+ <style>
5
9
  .outer {
6
10
  width: 200px;
7
11
  display: flex;
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Test Width - MD3 Expressive</title>
5
+ <meta name="description" content="Test file for width transitions in MD3 Expressive components.">
6
+ <meta property="og:title" content="Test Width - MD3 Expressive">
7
+ <meta property="og:description" content="Test file for width transitions in MD3 Expressive components.">
8
+ <style>
9
+ .wrapper {
10
+ box-sizing: border-box;
11
+ overflow: hidden;
12
+ padding: 20px;
13
+ margin: -20px;
14
+ width: 0px; /* Animate to 0 */
15
+ background: rgba(0,255,0,0.5);
16
+ border: 1px solid red;
17
+ }
18
+ .inner {
19
+ width: 100px;
20
+ height: 50px;
21
+ background: blue;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div style="display: flex;">
27
+ <div style="width: 50px; height: 50px; background: yellow;"></div>
28
+ <div class="wrapper">
29
+ <div class="inner"></div>
30
+ </div>
31
+ <div style="width: 50px; height: 50px; background: yellow;"></div>
32
+ </div>
33
+ </body>
34
+ </html>