@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
package/src/ui/toc.tsx CHANGED
@@ -139,7 +139,7 @@ export function TableOfContents({
139
139
  aria-label="On this page"
140
140
  className={cn("pl-6 flex flex-col h-full", className)}
141
141
  >
142
- <h4 className="text-xs font-bold text-m3-on-surface-variant uppercase tracking-widest mb-4 sm:hidden lg:block">
142
+ <h4 className="text-xs font-bold text-m3-on-surface-variant uppercase tracking-widest mb-4 hidden lg:block">
143
143
  On this page
144
144
  </h4>
145
145
  <ScrollArea
@@ -0,0 +1,51 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`BottomDockedToolbar > snapshots: with leading and trailing content 1`] = `
4
+ <div
5
+ aria-label="Bottom Docked Toolbar"
6
+ class="fixed bottom-0 left-0 right-0 w-full z-40 flex items-center h-16 justify-between shadow-[0_-1px_3px_rgba(0,0,0,0.1)] pointer-events-auto"
7
+ role="toolbar"
8
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); background-color: var(--toolbar-bg); color: var(--toolbar-color); padding-left: 16px; padding-right: 16px; transform: none;"
9
+ >
10
+ <div
11
+ class="flex items-center justify-start flex-1 shrink-0"
12
+ >
13
+ <span>
14
+ Lead
15
+ </span>
16
+ </div>
17
+ <div
18
+ class="flex items-center justify-center flex-1 shrink-0"
19
+ >
20
+ Center
21
+ </div>
22
+ <div
23
+ class="flex items-center justify-end flex-1 shrink-0"
24
+ >
25
+ <span>
26
+ Trail
27
+ </span>
28
+ </div>
29
+ </div>
30
+ `;
31
+
32
+ exports[`BottomDockedToolbar > snapshots: without optional content 1`] = `
33
+ <div
34
+ aria-label="Bottom Docked Toolbar"
35
+ class="fixed bottom-0 left-0 right-0 w-full z-40 flex items-center h-16 justify-between shadow-[0_-1px_3px_rgba(0,0,0,0.1)] pointer-events-auto"
36
+ role="toolbar"
37
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); background-color: var(--toolbar-bg); color: var(--toolbar-color); padding-left: 16px; padding-right: 16px; transform: none;"
38
+ >
39
+ <div
40
+ class="flex items-center justify-start flex-1 shrink-0"
41
+ />
42
+ <div
43
+ class="flex items-center justify-center flex-1 shrink-0"
44
+ >
45
+ Center
46
+ </div>
47
+ <div
48
+ class="flex items-center justify-end flex-1 shrink-0"
49
+ />
50
+ </div>
51
+ `;
@@ -0,0 +1,113 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`FloatingToolbarWithFab > snapshots: horizontal with fab end 1`] = `
4
+ <div>
5
+ <div
6
+ class="flex items-center justify-center pointer-events-auto flex-row h-(--toolbar-size)"
7
+ style="--fab-size: 56px; --toolbar-size: 64px; gap: 8px;"
8
+ >
9
+ <div
10
+ class="flex items-center shrink-0 min-w-0 min-h-0 overflow-hidden relative z-0"
11
+ style="padding: 24px; margin: -24px; opacity: 1; width: auto; height: auto; transform: none;"
12
+ >
13
+ <div
14
+ style="transform: none;"
15
+ >
16
+ <div
17
+ aria-expanded="true"
18
+ aria-label="Floating Toolbar"
19
+ aria-orientation="horizontal"
20
+ class="flex pointer-events-auto relative max-w-[90vw] h-(--toolbar-size) rounded-full shadow-md"
21
+ role="toolbar"
22
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); --toolbar-size: 64px; background-color: var(--toolbar-bg); color: var(--toolbar-color);"
23
+ >
24
+ <div
25
+ class="flex-1 w-full h-full rounded-[inherit] scrollbar-hide overflow-x-auto overflow-y-visible"
26
+ >
27
+ <div
28
+ class="flex items-center gap-1 min-w-0 min-h-0 flex-row h-full px-4"
29
+ >
30
+ <div
31
+ class="flex flex-1 items-center min-w-0 min-h-0 h-full flex-row justify-center"
32
+ style="gap: 4px;"
33
+ >
34
+ <button
35
+ type="button"
36
+ >
37
+ Center
38
+ </button>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <div
46
+ class="flex shrink-0 items-center justify-center relative z-10 *:w-full *:h-full"
47
+ style="width: 56px; height: 56px;"
48
+ >
49
+ <button
50
+ type="button"
51
+ >
52
+ FAB
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ `;
58
+
59
+ exports[`FloatingToolbarWithFab > snapshots: vertical with fab bottom 1`] = `
60
+ <div>
61
+ <div
62
+ class="flex items-center justify-center pointer-events-auto flex-col w-(--toolbar-size) h-fit"
63
+ style="--fab-size: 56px; --toolbar-size: 64px; gap: 8px;"
64
+ >
65
+ <div
66
+ class="flex items-center shrink-0 min-w-0 min-h-0 overflow-hidden relative z-0"
67
+ style="padding: 24px; margin: -24px; opacity: 1; width: auto; height: auto; transform: none;"
68
+ >
69
+ <div
70
+ style="transform: none;"
71
+ >
72
+ <div
73
+ aria-expanded="true"
74
+ aria-label="Floating Toolbar"
75
+ aria-orientation="vertical"
76
+ class="flex pointer-events-auto relative max-h-[90vh] w-(--toolbar-size) h-fit rounded-full shadow-md"
77
+ role="toolbar"
78
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); --toolbar-size: 64px; background-color: var(--toolbar-bg); color: var(--toolbar-color);"
79
+ >
80
+ <div
81
+ class="flex-1 w-full h-full rounded-[inherit] scrollbar-hide overflow-y-auto overflow-x-visible"
82
+ >
83
+ <div
84
+ class="flex items-center gap-1 min-w-0 min-h-0 flex-col w-full py-6"
85
+ >
86
+ <div
87
+ class="flex flex-1 items-center min-w-0 min-h-0 h-full flex-col justify-center"
88
+ style="gap: 4px;"
89
+ >
90
+ <button
91
+ type="button"
92
+ >
93
+ Center
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div
102
+ class="flex shrink-0 items-center justify-center relative z-10 *:w-full *:h-full"
103
+ style="width: 56px; height: 56px;"
104
+ >
105
+ <button
106
+ type="button"
107
+ >
108
+ FAB
109
+ </button>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ `;
@@ -0,0 +1,169 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`HorizontalFloatingToolbar > snapshots: horizontal collapsed 1`] = `
4
+ <div
5
+ style="transform: none;"
6
+ >
7
+ <div
8
+ aria-expanded="false"
9
+ aria-label="Floating Toolbar"
10
+ aria-orientation="horizontal"
11
+ class="flex pointer-events-auto relative max-w-[90vw] h-(--toolbar-size) rounded-full shadow-sm"
12
+ role="toolbar"
13
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); --toolbar-size: 64px; background-color: var(--toolbar-bg); color: var(--toolbar-color);"
14
+ >
15
+ <div
16
+ class="flex-1 w-full h-full rounded-[inherit] scrollbar-hide overflow-x-auto overflow-y-visible"
17
+ >
18
+ <div
19
+ class="flex items-center gap-1 min-w-0 min-h-0 flex-row h-full px-4"
20
+ >
21
+ <div
22
+ class="flex flex-1 items-center min-w-0 min-h-0 h-full flex-row justify-center"
23
+ style="gap: 4px;"
24
+ >
25
+ <button
26
+ type="button"
27
+ >
28
+ Center
29
+ </button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ `;
36
+
37
+ exports[`HorizontalFloatingToolbar > snapshots: horizontal expanded 1`] = `
38
+ <div
39
+ style="transform: none;"
40
+ >
41
+ <div
42
+ aria-expanded="true"
43
+ aria-label="Floating Toolbar"
44
+ aria-orientation="horizontal"
45
+ class="flex pointer-events-auto relative max-w-[90vw] h-(--toolbar-size) rounded-full shadow-md"
46
+ role="toolbar"
47
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); --toolbar-size: 64px; background-color: var(--toolbar-bg); color: var(--toolbar-color);"
48
+ >
49
+ <div
50
+ class="flex-1 w-full h-full rounded-[inherit] scrollbar-hide overflow-x-auto overflow-y-visible"
51
+ >
52
+ <div
53
+ class="flex items-center gap-1 min-w-0 min-h-0 flex-row h-full px-4"
54
+ >
55
+ <div
56
+ class="flex shrink-0 overflow-hidden flex-row"
57
+ style="opacity: 1; width: auto; height: auto; transform: none;"
58
+ >
59
+ <span>
60
+ Lead
61
+ </span>
62
+ </div>
63
+ <div
64
+ class="flex flex-1 items-center min-w-0 min-h-0 h-full flex-row justify-center"
65
+ style="gap: 4px;"
66
+ >
67
+ <button
68
+ type="button"
69
+ >
70
+ Center
71
+ </button>
72
+ </div>
73
+ <div
74
+ class="flex shrink-0 overflow-hidden flex-row"
75
+ style="opacity: 1; width: auto; height: auto; transform: none;"
76
+ >
77
+ <span>
78
+ Trail
79
+ </span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ `;
86
+
87
+ exports[`VerticalFloatingToolbar > snapshots: vertical collapsed 1`] = `
88
+ <div
89
+ style="transform: none;"
90
+ >
91
+ <div
92
+ aria-expanded="false"
93
+ aria-label="Floating Toolbar"
94
+ aria-orientation="vertical"
95
+ class="flex pointer-events-auto relative max-h-[90vh] w-(--toolbar-size) h-fit rounded-full shadow-sm"
96
+ role="toolbar"
97
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); --toolbar-size: 64px; background-color: var(--toolbar-bg); color: var(--toolbar-color);"
98
+ >
99
+ <div
100
+ class="flex-1 w-full h-full rounded-[inherit] scrollbar-hide overflow-y-auto overflow-x-visible"
101
+ >
102
+ <div
103
+ class="flex items-center gap-1 min-w-0 min-h-0 flex-col w-full py-6"
104
+ >
105
+ <div
106
+ class="flex flex-1 items-center min-w-0 min-h-0 h-full flex-col justify-center"
107
+ style="gap: 4px;"
108
+ >
109
+ <button
110
+ type="button"
111
+ >
112
+ Center
113
+ </button>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ `;
120
+
121
+ exports[`VerticalFloatingToolbar > snapshots: vertical expanded 1`] = `
122
+ <div
123
+ style="transform: none;"
124
+ >
125
+ <div
126
+ aria-expanded="true"
127
+ aria-label="Floating Toolbar"
128
+ aria-orientation="vertical"
129
+ class="flex pointer-events-auto relative max-h-[90vh] w-(--toolbar-size) h-fit rounded-full shadow-md"
130
+ role="toolbar"
131
+ style="--toolbar-bg: var(--md-sys-color-surface-container); --toolbar-color: var(--md-sys-color-on-surface); --toolbar-size: 64px; background-color: var(--toolbar-bg); color: var(--toolbar-color);"
132
+ >
133
+ <div
134
+ class="flex-1 w-full h-full rounded-[inherit] scrollbar-hide overflow-y-auto overflow-x-visible"
135
+ >
136
+ <div
137
+ class="flex items-center gap-1 min-w-0 min-h-0 flex-col w-full py-6"
138
+ >
139
+ <div
140
+ class="flex shrink-0 overflow-hidden flex-col"
141
+ style="opacity: 1; width: auto; height: auto; transform: none;"
142
+ >
143
+ <span>
144
+ Lead
145
+ </span>
146
+ </div>
147
+ <div
148
+ class="flex flex-1 items-center min-w-0 min-h-0 h-full flex-col justify-center"
149
+ style="gap: 4px;"
150
+ >
151
+ <button
152
+ type="button"
153
+ >
154
+ Center
155
+ </button>
156
+ </div>
157
+ <div
158
+ class="flex shrink-0 overflow-hidden flex-col"
159
+ style="opacity: 1; width: auto; height: auto; transform: none;"
160
+ >
161
+ <span>
162
+ Trail
163
+ </span>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ `;
@@ -0,0 +1,114 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import * as React from "react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { BottomDockedToolbar } from "./docked-toolbar";
5
+
6
+ describe("BottomDockedToolbar", () => {
7
+ it("renders at bottom of screen (position fixed)", () => {
8
+ const { container } = render(
9
+ <BottomDockedToolbar>Test</BottomDockedToolbar>,
10
+ );
11
+ expect(container.firstChild).toHaveClass("fixed bottom-0");
12
+ });
13
+
14
+ it("renders full-width (w-full)", () => {
15
+ const { container } = render(
16
+ <BottomDockedToolbar>Test</BottomDockedToolbar>,
17
+ );
18
+ expect(container.firstChild).toHaveClass("w-full");
19
+ });
20
+
21
+ it("renders with correct height (h-16 = 64px)", () => {
22
+ const { container } = render(
23
+ <BottomDockedToolbar>Test</BottomDockedToolbar>,
24
+ );
25
+ expect(container.firstChild).toHaveClass("h-16");
26
+ });
27
+
28
+ it("renders startContent", () => {
29
+ render(
30
+ <BottomDockedToolbar startContent={<span data-testid="leading" />}>
31
+ Test
32
+ </BottomDockedToolbar>,
33
+ );
34
+ expect(screen.getByTestId("leading")).toBeInTheDocument();
35
+ });
36
+
37
+ it("renders endContent", () => {
38
+ render(
39
+ <BottomDockedToolbar endContent={<span data-testid="trailing" />}>
40
+ Test
41
+ </BottomDockedToolbar>,
42
+ );
43
+ expect(screen.getByTestId("trailing")).toBeInTheDocument();
44
+ });
45
+
46
+ it("renders children in center", () => {
47
+ render(
48
+ <BottomDockedToolbar>
49
+ <span data-testid="center" />
50
+ </BottomDockedToolbar>,
51
+ );
52
+ expect(screen.getByTestId("center")).toBeInTheDocument();
53
+ });
54
+
55
+ it("standard and vibrant color variants", () => {
56
+ const { container: c1 } = render(
57
+ <BottomDockedToolbar variant="standard">Test</BottomDockedToolbar>,
58
+ );
59
+ expect(
60
+ (c1.firstChild as HTMLElement).style.getPropertyValue("--toolbar-bg"),
61
+ ).toBe("var(--md-sys-color-surface-container)");
62
+
63
+ const { container: c2 } = render(
64
+ <BottomDockedToolbar variant="vibrant">Test</BottomDockedToolbar>,
65
+ );
66
+ expect(
67
+ (c2.firstChild as HTMLElement).style.getPropertyValue("--toolbar-bg"),
68
+ ).toBe("var(--md-sys-color-primary-container)");
69
+ });
70
+
71
+ it("no rounded corners by default", () => {
72
+ const { container } = render(
73
+ <BottomDockedToolbar>Test</BottomDockedToolbar>,
74
+ );
75
+ // Tailwind rounded classes aren't present
76
+ expect(container.firstChild).not.toHaveClass("rounded-full");
77
+ expect(container.firstChild).not.toHaveClass("rounded-2xl");
78
+ });
79
+
80
+ it("applies aria-label", () => {
81
+ render(
82
+ <BottomDockedToolbar aria-label="Test Docked">Test</BottomDockedToolbar>,
83
+ );
84
+ expect(screen.getByRole("toolbar")).toHaveAttribute(
85
+ "aria-label",
86
+ "Test Docked",
87
+ );
88
+ });
89
+
90
+ it("forwards ref", () => {
91
+ const ref = React.createRef<HTMLDivElement>();
92
+ render(<BottomDockedToolbar ref={ref}>Test</BottomDockedToolbar>);
93
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
94
+ });
95
+
96
+ it("snapshots: with leading and trailing content", () => {
97
+ const { container } = render(
98
+ <BottomDockedToolbar
99
+ startContent={<span>Lead</span>}
100
+ endContent={<span>Trail</span>}
101
+ >
102
+ Center
103
+ </BottomDockedToolbar>,
104
+ );
105
+ expect(container.firstChild).toMatchSnapshot();
106
+ });
107
+
108
+ it("snapshots: without optional content", () => {
109
+ const { container } = render(
110
+ <BottomDockedToolbar>Center</BottomDockedToolbar>,
111
+ );
112
+ expect(container.firstChild).toMatchSnapshot();
113
+ });
114
+ });
@@ -0,0 +1,186 @@
1
+ import { m, useMotionValueEvent, useScroll } from "motion/react";
2
+ import * as React from "react";
3
+ import { cn } from "../../lib/utils";
4
+ import { SPRING_TRANSITION } from "../shared/constants";
5
+ import {
6
+ type FloatingToolbarColors,
7
+ standardFloatingToolbarColors,
8
+ vibrantFloatingToolbarColors,
9
+ } from "./toolbar-colors";
10
+ import { ToolbarContext } from "./toolbar-context";
11
+
12
+ export interface BottomDockedToolbarProps {
13
+ /** Color variant: standard or vibrant */
14
+ variant?: "standard" | "vibrant";
15
+ /** Custom colors override */
16
+ colors?: FloatingToolbarColors;
17
+ /** Whether to hide on scroll */
18
+ hideOnScroll?: boolean;
19
+ /** Scroll container ref for scroll behavior (if empty, uses window scroll) */
20
+ scrollContainerRef?: React.RefObject<HTMLElement | null>;
21
+ /** Start content (left-aligned) */
22
+ startContent?: React.ReactNode;
23
+ /** End content (right-aligned) */
24
+ endContent?: React.ReactNode;
25
+ /** Center content */
26
+ children?: React.ReactNode;
27
+ /**
28
+ * Horizontal padding (px) applied to the toolbar container.
29
+ * MD3 spec requires a minimum of 16dp on leading and trailing edges.
30
+ * @default 16
31
+ */
32
+ paddingX?: number;
33
+ /**
34
+ * Layout distribution of toolbar content.
35
+ * - `"between"` – leading/center/trailing spread across full width (default, compact screens).
36
+ * - `"center"` – all content centered; ideal for medium+ screen widths.
37
+ * - `"end-weighted"` – key action is centered while others are pushed to the edges.
38
+ * @default "between"
39
+ */
40
+ justify?: "between" | "center" | "end-weighted";
41
+ /**
42
+ * Container shape.
43
+ * - `"none"` – straight corners (MD3 default for mobile docked toolbars).
44
+ * - `"large"` – large rounded corners (recommended for web / large screens).
45
+ * - `"full"` – fully pill-shaped.
46
+ *
47
+ * MD3 guideline: *"On web and large screens, the docked toolbar can be rounded."*
48
+ * @default "none"
49
+ */
50
+ shape?: "none" | "large" | "full";
51
+ className?: string;
52
+ "aria-label"?: string;
53
+ }
54
+
55
+ /**
56
+ * A full-width docked toolbar typically fixed at the bottom of the screen.
57
+ * Replaces the deprecated Bottom App Bar in MD3 Expressive.
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * <BottomDockedToolbar hideOnScroll startContent={<IconButton aria-label="Menu"><Icon name="menu" /></IconButton>}>
62
+ * <div className="flex gap-2">
63
+ * <IconButton aria-label="Search"><Icon name="search" /></IconButton>
64
+ * <IconButton aria-label="Edit"><Icon name="edit" /></IconButton>
65
+ * </div>
66
+ * </BottomDockedToolbar>
67
+ * ```
68
+ */
69
+ export const BottomDockedToolbar = React.forwardRef<
70
+ HTMLDivElement,
71
+ BottomDockedToolbarProps
72
+ >(
73
+ (
74
+ {
75
+ variant = "standard",
76
+ colors: customColors,
77
+ hideOnScroll = false,
78
+ scrollContainerRef,
79
+ startContent,
80
+ endContent,
81
+ children,
82
+ paddingX = 16,
83
+ justify = "between",
84
+ shape = "none",
85
+ className,
86
+ "aria-label": ariaLabel,
87
+ ...props
88
+ },
89
+ ref,
90
+ ) => {
91
+ const colors =
92
+ customColors ||
93
+ (variant === "vibrant"
94
+ ? vibrantFloatingToolbarColors
95
+ : standardFloatingToolbarColors);
96
+
97
+ const { scrollY } = useScroll(
98
+ scrollContainerRef ? { container: scrollContainerRef } : undefined,
99
+ );
100
+
101
+ const [isHidden, setIsHidden] = React.useState(false);
102
+ const lastScrollY = React.useRef(0);
103
+
104
+ useMotionValueEvent(scrollY, "change", (latest) => {
105
+ if (!hideOnScroll) return;
106
+
107
+ const diff = latest - lastScrollY.current;
108
+
109
+ if (latest <= 0) {
110
+ setIsHidden(false);
111
+ lastScrollY.current = 0;
112
+ return;
113
+ }
114
+
115
+ if (Math.abs(diff) > 10) {
116
+ setIsHidden(diff > 0);
117
+ lastScrollY.current = latest;
118
+ }
119
+ });
120
+
121
+ const cssVars = {
122
+ "--toolbar-bg": colors.toolbarContainerColor,
123
+ "--toolbar-color": colors.toolbarContentColor,
124
+ } as React.CSSProperties;
125
+
126
+ const SHAPE_CLASSES = {
127
+ none: "",
128
+ large: "rounded-2xl",
129
+ full: "rounded-full",
130
+ };
131
+
132
+ const justifyClass =
133
+ justify === "center" ? "justify-center gap-4" : "justify-between";
134
+
135
+ return (
136
+ <ToolbarContext.Provider value={{ orientation: "horizontal" }}>
137
+ <m.div
138
+ ref={ref}
139
+ role="toolbar"
140
+ aria-label={ariaLabel || "Bottom Docked Toolbar"}
141
+ initial={false}
142
+ animate={{
143
+ y: isHidden ? "100%" : "0%",
144
+ }}
145
+ transition={SPRING_TRANSITION}
146
+ style={{
147
+ ...cssVars,
148
+ backgroundColor: "var(--toolbar-bg)",
149
+ color: "var(--toolbar-color)",
150
+ // Apply safe area inset for mobile bottom indicators
151
+ paddingBottom: "env(safe-area-inset-bottom, 0px)",
152
+ paddingLeft: paddingX,
153
+ paddingRight: paddingX,
154
+ }}
155
+ className={cn(
156
+ "fixed bottom-0 left-0 right-0 w-full z-40",
157
+ "flex items-center h-16",
158
+ justifyClass,
159
+ SHAPE_CLASSES[shape],
160
+ "shadow-[0_-1px_3px_rgba(0,0,0,0.1)] pointer-events-auto",
161
+ className,
162
+ )}
163
+ {...props}
164
+ >
165
+ <div className="flex items-center justify-start flex-1 shrink-0">
166
+ {startContent}
167
+ </div>
168
+
169
+ <div
170
+ className={cn(
171
+ "flex items-center justify-center",
172
+ justify === "center" ? "gap-1" : "flex-1 shrink-0",
173
+ )}
174
+ >
175
+ {children}
176
+ </div>
177
+
178
+ <div className="flex items-center justify-end flex-1 shrink-0">
179
+ {endContent}
180
+ </div>
181
+ </m.div>
182
+ </ToolbarContext.Provider>
183
+ );
184
+ },
185
+ );
186
+ BottomDockedToolbar.displayName = "BottomDockedToolbar";