@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
@@ -1,350 +0,0 @@
1
- import * as React from "react";
2
- import { cn } from "../lib/utils";
3
- import type { ButtonProps } from "./button";
4
- import { Icon } from "./icon";
5
-
6
- /**
7
- * Thuộc tính truyền vào cho thành phần nhóm nút (Button Group).
8
- */
9
- export interface ButtonGroupProps
10
- extends React.FieldsetHTMLAttributes<HTMLFieldSetElement> {
11
- /**
12
- * Cấu trúc hiển thị của nhóm nút:
13
- * - `standard`: Các nút cách xa và có khoảng cách độc lập với nhau (gap).
14
- * - `connected`: Các nút nối liền khung viền với nhau để tạo thành dạng Segmented Button.
15
- * @default "standard"
16
- */
17
- variant?: "standard" | "connected";
18
- /**
19
- * Hướng sắp xếp của các nút trong nhóm.
20
- * @default "horizontal"
21
- */
22
- orientation?: "horizontal" | "vertical";
23
- /**
24
- * Đặt thành `true` nếu bạn muốn nhóm hiển thị dạng `standard` giãn đều lấp đầy toàn bộ khu vực chứa (container).
25
- * @default false
26
- */
27
- fullWidth?: boolean;
28
- /**
29
- * Áp dụng thống nhất chung một kích thước (`size`) cho tất cả các con trong nhóm (ghi đè kích thước lẻ từng nút).
30
- */
31
- size?: "xs" | "sm" | "md" | "lg" | "xl";
32
- /**
33
- * Bật/tắt hiệu ứng thu phóng độ rộng / khoảng đệm (Morphing Width) khi nhấn vào các nút (áp dụng cho nhóm `standard`).
34
- * @default true
35
- */
36
- morphingWidth?: boolean;
37
- /**
38
- * Tự động hiển thị biểu tượng (icon) Check khi một nút trạng thái nằm trong nhóm được chỉ định là `selected={true}`.
39
- * @default false
40
- */
41
- showCheck?: boolean;
42
- }
43
-
44
- // Bảng ánh xạ padding mặc định tương ứng với từng size trong button.tsx
45
- const SIZE_PADDING_MAP: Record<string, string> = {
46
- xs: "0.75rem",
47
- sm: "1rem",
48
- md: "1.5rem",
49
- lg: "3rem",
50
- xl: "3rem",
51
- };
52
-
53
- // Bảng ánh xạ Pill Radius (h/2) để animation mượt mà
54
- const PILL_RADIUS_MAP: Record<string, number> = {
55
- xs: 16,
56
- sm: 20,
57
- md: 28,
58
- lg: 48,
59
- xl: 68,
60
- };
61
-
62
- // ĐỒNG BỘ SPECS MD3: Bảng ánh xạ góc bo bên trong (Inner Radius) của Connected Group
63
- const INNER_RADIUS_MAP: Record<string, number> = {
64
- xs: 4,
65
- sm: 8,
66
- md: 8,
67
- lg: 16,
68
- xl: 20,
69
- };
70
-
71
- // Bảng ánh xạ Pressed Radius để morphing shape từ pill sang square
72
- const PRESSED_RADIUS_MAP: Record<string, number> = {
73
- xs: 8,
74
- sm: 10,
75
- md: 16,
76
- lg: 28,
77
- xl: 40,
78
- };
79
-
80
- const ButtonGroupComponent = React.forwardRef<
81
- HTMLFieldSetElement,
82
- ButtonGroupProps
83
- >(
84
- (
85
- {
86
- className,
87
- variant = "standard",
88
- orientation = "horizontal",
89
- fullWidth = false,
90
- size,
91
- morphingWidth = true,
92
- showCheck = false,
93
- children,
94
- ...props
95
- },
96
- ref,
97
- ) => {
98
- const [pressedIndex, setPressedIndex] = React.useState<number | null>(null);
99
-
100
- const childrenArray = React.useMemo(
101
- () => React.Children.toArray(children).filter(React.isValidElement),
102
- [children],
103
- );
104
- const count = childrenArray.length;
105
-
106
- const handlePointerLeaveAndUp = React.useCallback(() => {
107
- setPressedIndex(null);
108
- }, []);
109
-
110
- return (
111
- <fieldset
112
- ref={ref}
113
- className={cn(
114
- "inline-flex p-0 m-0 border-none max-w-full overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
115
- orientation === "vertical" ? "flex-col items-stretch" : "flex-row",
116
- // SPECS: Standard = 8px (gap-2), Connected = chính xác 2px
117
- variant === "standard" ? "gap-2" : "gap-0.5",
118
- fullWidth && (orientation === "horizontal" ? "w-full" : "h-full"),
119
- className,
120
- )}
121
- onPointerLeave={handlePointerLeaveAndUp}
122
- onPointerUp={handlePointerLeaveAndUp}
123
- {...props}
124
- >
125
- {childrenArray.map((child, index) => {
126
- const isFirst = index === 0;
127
- const isLast = index === count - 1;
128
-
129
- const element = child as React.ReactElement<ButtonProps>;
130
-
131
- // Lấy kích thước hiện tại để tính toán Specs
132
- // Ưu tiên prop size của Group, nếu không có thì lấy của nút con, mặc định là "sm"
133
- const itemSize = size || element.props.size || "sm";
134
- const basePx = SIZE_PADDING_MAP[itemSize] || "1rem";
135
- const innerRadius = INNER_RADIUS_MAP[itemSize] || 8;
136
-
137
- // Inject style động
138
- const dynamicStyle: React.CSSProperties = {
139
- ...element.props.style,
140
- "--m3-inner-rad": `${innerRadius}px`,
141
- } as React.CSSProperties;
142
-
143
- let motionPropsToOverride: Partial<ButtonProps> = {};
144
- let explicitIcon = element.props.icon;
145
- const isSelected = element.props.selected === true;
146
-
147
- if (showCheck && isSelected && !explicitIcon) {
148
- explicitIcon = <Icon name="check" aria-hidden="true" />;
149
- }
150
-
151
- // 1. STANDARD GROUP: Xử lý hiệu ứng Morphing Width khi press
152
- if (
153
- variant === "standard" &&
154
- orientation === "horizontal" &&
155
- morphingWidth
156
- ) {
157
- const isPressed = pressedIndex === index;
158
- const isNeighbor =
159
- pressedIndex !== null && Math.abs(pressedIndex - index) === 1;
160
-
161
- if (fullWidth) {
162
- if (isPressed) {
163
- dynamicStyle.flex = "1.15";
164
- } else if (pressedIndex !== null) {
165
- dynamicStyle.flex = isNeighbor ? "0.925" : "1";
166
- } else {
167
- dynamicStyle.flex = "1";
168
- }
169
- dynamicStyle.transition = "flex 0.3s cubic-bezier(0.2, 0, 0, 1)";
170
- } else {
171
- if (isPressed) {
172
- dynamicStyle.paddingInline = `calc(${basePx} + 0.6rem)`;
173
- } else if (isNeighbor) {
174
- dynamicStyle.paddingInline = `calc(${basePx} - 0.3rem)`;
175
- } else {
176
- dynamicStyle.paddingInline = basePx;
177
- }
178
- dynamicStyle.transition =
179
- "padding 0.2s cubic-bezier(0.2, 0, 0, 1)";
180
- }
181
-
182
- const pressedRadius = PRESSED_RADIUS_MAP[itemSize] || 10;
183
-
184
- motionPropsToOverride = {
185
- whileTap: {
186
- scale: 0.98,
187
- borderRadius: pressedRadius,
188
- },
189
- transition: { type: "spring", stiffness: 400, damping: 25 },
190
- };
191
- }
192
-
193
- // 2. CONNECTED GROUP: Xử lý trạng thái Selected và Border Radius
194
- let connectedClasses = "";
195
- if (variant === "connected") {
196
- // Ưu tiên z-index khi selected/hover để đè viền lên nhau
197
- if (isSelected) {
198
- connectedClasses = "z-20";
199
- }
200
-
201
- const r = PILL_RADIUS_MAP[itemSize] || 20;
202
- const i = innerRadius;
203
- let tl = r,
204
- tr = r,
205
- br = r,
206
- bl = r;
207
-
208
- if (!isSelected) {
209
- if (orientation === "horizontal") {
210
- if (isFirst && !isLast) {
211
- tl = r;
212
- tr = i;
213
- br = i;
214
- bl = r;
215
- } else if (!isFirst && isLast) {
216
- tl = i;
217
- tr = r;
218
- br = r;
219
- bl = i;
220
- } else if (!isFirst && !isLast) {
221
- tl = i;
222
- tr = i;
223
- br = i;
224
- bl = i;
225
- }
226
- } else {
227
- // vertical
228
- if (isFirst && !isLast) {
229
- tl = r;
230
- tr = r;
231
- br = i;
232
- bl = i;
233
- } else if (!isFirst && isLast) {
234
- tl = i;
235
- tr = i;
236
- br = r;
237
- bl = r;
238
- } else if (!isFirst && !isLast) {
239
- tl = i;
240
- tr = i;
241
- br = i;
242
- bl = i;
243
- }
244
- }
245
- }
246
-
247
- // Set inline style (fallback/SSR)
248
- dynamicStyle.borderTopLeftRadius = `${tl}px`;
249
- dynamicStyle.borderTopRightRadius = `${tr}px`;
250
- dynamicStyle.borderBottomRightRadius = `${br}px`;
251
- dynamicStyle.borderBottomLeftRadius = `${bl}px`;
252
-
253
- // Transition CSS thuần
254
- dynamicStyle.transition =
255
- "border-top-left-radius 0.25s cubic-bezier(0.2, 0, 0, 1), border-top-right-radius 0.25s cubic-bezier(0.2, 0, 0, 1), border-bottom-right-radius 0.25s cubic-bezier(0.2, 0, 0, 1), border-bottom-left-radius 0.25s cubic-bezier(0.2, 0, 0, 1), padding 0.2s cubic-bezier(0.2, 0, 0, 1), flex 0.2s cubic-bezier(0.2, 0, 0, 1)";
256
-
257
- // VÔ HIỆU HÓA Framer Motion borderRadius trên children Button bằng cách override explicit corners
258
- const animateProps =
259
- typeof element.props.animate === "object" &&
260
- !Array.isArray(element.props.animate)
261
- ? element.props.animate
262
- : {};
263
- const whileTapProps =
264
- typeof element.props.whileTap === "object" &&
265
- !Array.isArray(element.props.whileTap)
266
- ? element.props.whileTap
267
- : {};
268
-
269
- // Tính toán corners khi pressed: tất cả corners đều thu về innerRadius hoặc pressedRadius
270
- // Theo MD3, khi nhấn Segmented Button, nó nên morph về dạng vuông hơn.
271
- const pr = INNER_RADIUS_MAP[itemSize] || 8;
272
-
273
- motionPropsToOverride.animate = {
274
- ...animateProps,
275
- borderRadius: undefined,
276
- borderTopLeftRadius: tl,
277
- borderTopRightRadius: tr,
278
- borderBottomRightRadius: br,
279
- borderBottomLeftRadius: bl,
280
- } as ButtonProps["animate"];
281
- motionPropsToOverride.whileTap = {
282
- ...whileTapProps,
283
- borderRadius: undefined,
284
- borderTopLeftRadius: pr,
285
- borderTopRightRadius: pr,
286
- borderBottomRightRadius: pr,
287
- borderBottomLeftRadius: pr,
288
- } as ButtonProps["whileTap"];
289
-
290
- motionPropsToOverride.transition = {
291
- type: "spring",
292
- bounce: 0,
293
- duration: 0.2,
294
- };
295
- }
296
-
297
- return React.cloneElement(element, {
298
- key: element.key ?? index,
299
- tabIndex: isFirst ? 0 : -1,
300
- size: size || element.props.size, // Push size group down to children
301
- icon: explicitIcon, // Override icon if showCheck injected it
302
- // Automatically apply unselected/selected color semantics for connected group
303
- ...(variant === "connected" && {
304
- colorStyle: element.props.colorStyle || "tonal",
305
- selectedColorStyle: element.props.selectedColorStyle || "filled",
306
- }),
307
- className: cn(
308
- element.props.className,
309
- connectedClasses,
310
- "focus-visible:z-10 hover:z-10 relative",
311
- ),
312
- style: dynamicStyle,
313
- onPointerDown: (e: React.PointerEvent<HTMLButtonElement>) => {
314
- setPressedIndex(index);
315
- if (element.props.onPointerDown) {
316
- element.props.onPointerDown(e);
317
- }
318
- },
319
- ...motionPropsToOverride,
320
- });
321
- })}
322
- </fieldset>
323
- );
324
- },
325
- );
326
-
327
- ButtonGroupComponent.displayName = "ButtonGroup";
328
-
329
- /**
330
- * Component Nhóm Nút (Button Group) được sử dụng để gom nhóm nhiều nút có công năng tương tự lại với nhau.
331
- * Hỗ trợ tạo các bộ nút độc lập (Standard) hoặc khối liên kết liền mạch (Connected/Segmented Buttons).
332
- * Kế thừa cơ chế chuyển động kết hợp liền mạch của MD3 Expressive.
333
- *
334
- * @example
335
- * ```tsx
336
- * // Nhóm nút tiêu chuẩn rời rạc (Standard)
337
- * <ButtonGroup variant="standard">
338
- * <Button>Lựa chọn 1</Button>
339
- * <Button>Lựa chọn 2</Button>
340
- * </ButtonGroup>
341
- *
342
- * // Nhóm nút liền khối (Connected Segmented Button)
343
- * <ButtonGroup variant="connected" showCheck fullWidth>
344
- * <Button variant="toggle" selected={view === "day"} onClick={() => setView("day")}>Ngày</Button>
345
- * <Button variant="toggle" selected={view === "week"} onClick={() => setView("week")}>Tuần</Button>
346
- * <Button variant="toggle" selected={view === "month"} onClick={() => setView("month")}>Tháng</Button>
347
- * </ButtonGroup>
348
- * ```
349
- */
350
- export const ButtonGroup = React.memo(ButtonGroupComponent);