@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
package/src/ui/fab.tsx
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file fab.tsx
|
|
3
|
+
*
|
|
4
|
+
* MD3 Expressive Floating Action Button (FAB).
|
|
5
|
+
*
|
|
6
|
+
* Supports four sizes, an extended variant with animated label reveal,
|
|
7
|
+
* shape morphing, a `lowered` elevation variant, and an optional
|
|
8
|
+
* `FABPosition` container for absolute positioning within a layout.
|
|
9
|
+
*
|
|
10
|
+
* @see https://m3.material.io/components/floating-action-button/overview
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { HTMLMotionProps } from "motion/react";
|
|
14
|
+
import { AnimatePresence, domMax, LazyMotion, m } from "motion/react";
|
|
15
|
+
import * as React from "react";
|
|
16
|
+
import { cn } from "../lib/utils";
|
|
17
|
+
import { LoadingIndicator } from "./loading-indicator";
|
|
18
|
+
import { ProgressIndicator } from "./progress-indicator";
|
|
19
|
+
import { Ripple, useRippleState } from "./ripple";
|
|
20
|
+
import {
|
|
21
|
+
ICON_SPAN_VARIANTS,
|
|
22
|
+
SPRING_TRANSITION,
|
|
23
|
+
SPRING_TRANSITION_FAST,
|
|
24
|
+
} from "./shared/constants";
|
|
25
|
+
import { TouchTarget } from "./shared/touch-target";
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// Design Tokens
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-size layout classes for the FAB container.
|
|
33
|
+
* MD3 sizes: SM=40dp, MD=56dp, LG=96dp, XL=136dp.
|
|
34
|
+
* Extended FABs use `w-full` + `px-*` via the caller.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
const SIZE_STYLES: Record<string, string> = {
|
|
38
|
+
sm: "h-10 w-10",
|
|
39
|
+
md: "h-14 w-14",
|
|
40
|
+
lg: "h-24 w-24",
|
|
41
|
+
xl: "h-[8.5rem] w-[8.5rem]",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Per-size icon dimensions (Tailwind class + pixel value).
|
|
46
|
+
* MD3 icon sizes: SM=24dp, MD=24dp, LG=32dp, XL=40dp.
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
const SIZE_ICON: Record<string, { cls: string; px: number }> = {
|
|
50
|
+
sm: { cls: "size-6", px: 24 },
|
|
51
|
+
md: { cls: "size-6", px: 24 },
|
|
52
|
+
lg: { cls: "size-8", px: 32 },
|
|
53
|
+
xl: { cls: "size-10", px: 40 },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Per-size label typography classes used in the extended variant.
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
const SIZE_TEXT_CLASS: Record<string, string> = {
|
|
61
|
+
sm: "text-sm font-medium",
|
|
62
|
+
md: "text-base font-medium",
|
|
63
|
+
lg: "text-xl font-semibold",
|
|
64
|
+
xl: "text-2xl font-semibold",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Shape Morphing — Border Radius Map
|
|
69
|
+
//
|
|
70
|
+
// IMPORTANT: Use exact height/2 values for "round" radii to avoid the dead-zone
|
|
71
|
+
// artefact: CSS clips any radius > height/2 identically, so animating from
|
|
72
|
+
// 9999 → small value produces a jump/snap at the threshold.
|
|
73
|
+
// Heights: SM=40dp, MD=56dp, LG=96dp, XL=136dp
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Per-size border-radius tokens for all interaction / selection states.
|
|
78
|
+
*
|
|
79
|
+
* - `default`: idle pill radius (height / 2).
|
|
80
|
+
* - `pressed`: compressed on `whileTap`.
|
|
81
|
+
* - `extended`: radius for the extended (label-visible) state.
|
|
82
|
+
* - `extended_pressed`: compressed extended state on `whileTap`.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
const MORPH_RADIUS: Record<
|
|
87
|
+
string,
|
|
88
|
+
{
|
|
89
|
+
default: number;
|
|
90
|
+
pressed: number;
|
|
91
|
+
extended: number;
|
|
92
|
+
extended_pressed: number;
|
|
93
|
+
}
|
|
94
|
+
> = {
|
|
95
|
+
sm: { default: 12, pressed: 8, extended: 12, extended_pressed: 8 },
|
|
96
|
+
md: { default: 16, pressed: 10, extended: 16, extended_pressed: 10 },
|
|
97
|
+
lg: { default: 28, pressed: 20, extended: 28, extended_pressed: 20 },
|
|
98
|
+
xl: { default: 40, pressed: 28, extended: 40, extended_pressed: 28 },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
// Color Roles
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Color-role Tailwind class map for each FAB color variant.
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
const COLOR_CLASSES: Record<
|
|
110
|
+
string,
|
|
111
|
+
{ bg: string; text: string; shadow: string }
|
|
112
|
+
> = {
|
|
113
|
+
primary: {
|
|
114
|
+
bg: "bg-m3-primary-container",
|
|
115
|
+
text: "text-m3-on-primary-container",
|
|
116
|
+
shadow: "shadow-md",
|
|
117
|
+
},
|
|
118
|
+
secondary: {
|
|
119
|
+
bg: "bg-m3-secondary-container",
|
|
120
|
+
text: "text-m3-on-secondary-container",
|
|
121
|
+
shadow: "shadow-md",
|
|
122
|
+
},
|
|
123
|
+
tertiary: {
|
|
124
|
+
bg: "bg-m3-tertiary-container",
|
|
125
|
+
text: "text-m3-on-tertiary-container",
|
|
126
|
+
shadow: "shadow-md",
|
|
127
|
+
},
|
|
128
|
+
surface: {
|
|
129
|
+
bg: "bg-m3-surface-container-high",
|
|
130
|
+
text: "text-m3-primary",
|
|
131
|
+
shadow: "shadow-md",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
// Types
|
|
137
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
type MotionButtonProps = Omit<HTMLMotionProps<"button">, "children" | "color">;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Tham số Props dùng cho component thao tác nổi `FAB`.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* - Đảm bảo rằng đối với loại FAB chỉ show ra mỗi con Icon mà không có nhãn hiển thị (icon-only), bắt buộc phải có thuộc tính `aria-label` nhằm phục vụ (accessibility).
|
|
146
|
+
* - Ở hình thể mở rộng (khi `extended={true}`), phần nội dung truyền vào `children` chính là chuỗi Text được thể hiện cùng nút, và nó sẽ khiến nút button có label mặc định nên bạn có thể chém bớt tham số `aria-label`.
|
|
147
|
+
* - Thuộc tính cờ `lowered` (chìm) giúp giáng cấp hiệu ứng tạo bóng Shadow của thẻ, rải mảng cái shadow theo phong thái MD3 "lowered" FAB;
|
|
148
|
+
* sử dụng khi nút FAB này vốn bị bọc bên trong bề mặt chìm đè lên component gì khác mà vốn tụi nó đã nhún ở mực sâu (Ví dụ Bottom App Bar) để thiết lập Hierarchy hài hoà.
|
|
149
|
+
*
|
|
150
|
+
* @see https://m3.material.io/components/floating-action-button/overview
|
|
151
|
+
*/
|
|
152
|
+
export interface FABProps extends MotionButtonProps {
|
|
153
|
+
/**
|
|
154
|
+
* Icon đại diện render — thông thường là truyền thẻ component Icon.
|
|
155
|
+
* Sẽ được tráo đổi thành Spinner tự động quay khi giá trị `loading={true}`.
|
|
156
|
+
*/
|
|
157
|
+
icon: React.ReactNode;
|
|
158
|
+
/**
|
|
159
|
+
* Kích thước hiển thị FAB. Tuân chuẩn.
|
|
160
|
+
* - `sm`: Small (40dp) — Được khuyên dùng cho các không gian kín/trong lòng Content.
|
|
161
|
+
* - `md`: Regular (56dp) — Action thứ yếu hoặc tiêu điểm màn hình. (Phần đông người dùng xài).
|
|
162
|
+
* - `lg`: Large (96dp) — Trọng tâm thao tác quan trọng lớn nhát.
|
|
163
|
+
* - `xl`: Extra-large (136dp) — Gây tiếng vang, dạng Spotlight cực bùng nổ của app.
|
|
164
|
+
* @default "md"
|
|
165
|
+
*/
|
|
166
|
+
size?: "sm" | "md" | "lg" | "xl";
|
|
167
|
+
/**
|
|
168
|
+
* Container vai trò hệ thống tông màu MD3 dùng phết nền.
|
|
169
|
+
* @default "primary"
|
|
170
|
+
*/
|
|
171
|
+
colorStyle?: "primary" | "secondary" | "tertiary" | "surface";
|
|
172
|
+
/**
|
|
173
|
+
* Kích hoạt khi giá trị được đổi là `true`, sẽ diễn tả Animation bung chữ kèm theo độ dãn hình dài cho cái FAB.
|
|
174
|
+
* Chiều rộng tự cơi nới để thích ứng chuỗi `children`.
|
|
175
|
+
* @default false
|
|
176
|
+
*/
|
|
177
|
+
extended?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Nơi đón lấy chữ được render cùng khi `extended={true}` bật lên.
|
|
180
|
+
* Khuyến nghị là Text string thuần.
|
|
181
|
+
*/
|
|
182
|
+
children?: React.ReactNode;
|
|
183
|
+
/**
|
|
184
|
+
* Nhấn `true`, thì rút lại shadow đi một cấp xuống độ nổi nông cạn.
|
|
185
|
+
* Mảng bám ở Bottom bar hay Top bar Surface để ránh rườm rà.
|
|
186
|
+
* @default false
|
|
187
|
+
*/
|
|
188
|
+
lowered?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Nhấp chuột sang `true`, đổi Icon thành cối xay Spinner chờ kết quả. Đồng loạt chặn click tương tác.
|
|
191
|
+
* @default false
|
|
192
|
+
*/
|
|
193
|
+
loading?: boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Có 2 chuẩn hình của Loading chờ.
|
|
196
|
+
* @default "loading-indicator"
|
|
197
|
+
*/
|
|
198
|
+
loadingVariant?: "loading-indicator" | "circular";
|
|
199
|
+
/**
|
|
200
|
+
* Hiện thẻ FAB lên layout không (Kiểm soát bằng motion scale Entrance/Exit).
|
|
201
|
+
* @default true
|
|
202
|
+
*/
|
|
203
|
+
visible?: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
// FABPosition — Layout Wrapper
|
|
208
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Interface cho component bọc thẻ `FABPosition` — Gắn lớp absolute position nhét cục FAB vào một góc cố định của góc nào đó tại trình duyệt/bề mặt render.
|
|
212
|
+
*
|
|
213
|
+
* @see {@link FABPosition}
|
|
214
|
+
*/
|
|
215
|
+
export interface FABPositionProps {
|
|
216
|
+
/**
|
|
217
|
+
* Góc để niêm chặt nút FAB.
|
|
218
|
+
* @default "bottom-right"
|
|
219
|
+
*/
|
|
220
|
+
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
221
|
+
/** Kẹp một nùi element. Mong chờ thả Node `<FAB>` vào đây.*/
|
|
222
|
+
children: React.ReactNode;
|
|
223
|
+
/** CSS Class hỗ trợ chỉnh override */
|
|
224
|
+
className?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const POSITION_CLASS: Record<string, string> = {
|
|
228
|
+
"bottom-right": "bottom-4 right-4 sm:bottom-6 sm:right-6",
|
|
229
|
+
"bottom-left": "bottom-4 left-4 sm:bottom-6 sm:left-6",
|
|
230
|
+
"top-right": "top-4 right-4 sm:top-6 sm:right-6",
|
|
231
|
+
"top-left": "top-4 left-4 sm:top-6 sm:left-6",
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Element bao bọc thẻ định vị Absolute cho component `<FAB>`.
|
|
236
|
+
*
|
|
237
|
+
* Component dùng để cắm phao Neo cái FAB vào sát ở góc của screen kèm theo một cái offset space theo responsive an toàn mà lại trơn chu nhạy nhẽo.
|
|
238
|
+
* Nhưng có quy tắc gốc đó là phần tử bao bọc cha mẹ của nó PHẢI có thẻ tag css `position: relative` (hoặc ở cấp tổ tiêm của trang nào đó phải đẻ gốc rễ ra posisition).
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```tsx
|
|
242
|
+
* <div className="relative min-h-screen">
|
|
243
|
+
* // Cái nút sẽ xà xuống dưới cùng bên lề Trái
|
|
244
|
+
* <FABPosition position="bottom-left">
|
|
245
|
+
* <FAB icon={<Icon name="edit" />} aria-label="Compose New Mail" />
|
|
246
|
+
* </FABPosition>
|
|
247
|
+
* </div>
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* @see {@link FAB}
|
|
251
|
+
* @see https://m3.material.io/components/floating-action-button/guidelines
|
|
252
|
+
*/
|
|
253
|
+
export function FABPosition({
|
|
254
|
+
position = "bottom-right",
|
|
255
|
+
children,
|
|
256
|
+
className,
|
|
257
|
+
}: FABPositionProps) {
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
className={cn(
|
|
261
|
+
"absolute z-10",
|
|
262
|
+
POSITION_CLASS[position] ?? POSITION_CLASS["bottom-right"],
|
|
263
|
+
className,
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
266
|
+
{children}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
272
|
+
// FAB Component
|
|
273
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
const FABComponent = React.forwardRef<HTMLButtonElement, FABProps>(
|
|
276
|
+
(
|
|
277
|
+
{
|
|
278
|
+
className,
|
|
279
|
+
style,
|
|
280
|
+
icon,
|
|
281
|
+
size = "md",
|
|
282
|
+
colorStyle = "primary",
|
|
283
|
+
extended = false,
|
|
284
|
+
children,
|
|
285
|
+
lowered = false,
|
|
286
|
+
loading = false,
|
|
287
|
+
loadingVariant = "loading-indicator",
|
|
288
|
+
visible = true,
|
|
289
|
+
onClick,
|
|
290
|
+
onKeyDown,
|
|
291
|
+
"aria-label": ariaLabel,
|
|
292
|
+
...restProps
|
|
293
|
+
},
|
|
294
|
+
ref,
|
|
295
|
+
) => {
|
|
296
|
+
const colors = COLOR_CLASSES[colorStyle] ?? COLOR_CLASSES.primary;
|
|
297
|
+
const radiusConfig = MORPH_RADIUS[size] ?? MORPH_RADIUS.md;
|
|
298
|
+
|
|
299
|
+
const animateRadius = extended
|
|
300
|
+
? radiusConfig.extended
|
|
301
|
+
: radiusConfig.default;
|
|
302
|
+
const pressedRadius = extended
|
|
303
|
+
? radiusConfig.extended_pressed
|
|
304
|
+
: radiusConfig.pressed;
|
|
305
|
+
|
|
306
|
+
const sizeIcon = SIZE_ICON[size] ?? SIZE_ICON.md;
|
|
307
|
+
const iconClass = sizeIcon.cls;
|
|
308
|
+
const iconPx = sizeIcon.px;
|
|
309
|
+
|
|
310
|
+
// xs/sm share the SM token; only MD FAB (40dp) needs the touch target
|
|
311
|
+
const needsTouchTarget = size === "sm";
|
|
312
|
+
|
|
313
|
+
// ── Ripple ───────────────────────────────────────────────────────
|
|
314
|
+
const { ripples, onPointerDown, removeRipple } = useRippleState({
|
|
315
|
+
disabled: loading,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const handleClick = React.useCallback(
|
|
319
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
320
|
+
if (loading) {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
onClick?.(e);
|
|
325
|
+
},
|
|
326
|
+
[loading, onClick],
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const handleKeyDown = React.useCallback(
|
|
330
|
+
(e: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
331
|
+
if (loading) return;
|
|
332
|
+
if ((e.key === "Enter" || e.key === " ") && onClick) {
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
(e.currentTarget as HTMLButtonElement).click();
|
|
335
|
+
}
|
|
336
|
+
onKeyDown?.(e);
|
|
337
|
+
},
|
|
338
|
+
[loading, onClick, onKeyDown],
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<LazyMotion features={domMax} strict>
|
|
343
|
+
<AnimatePresence>
|
|
344
|
+
{visible && (
|
|
345
|
+
<m.button
|
|
346
|
+
ref={ref}
|
|
347
|
+
type="button"
|
|
348
|
+
aria-label={
|
|
349
|
+
ariaLabel ||
|
|
350
|
+
(typeof children === "string" ? children : undefined)
|
|
351
|
+
}
|
|
352
|
+
aria-busy={loading || undefined}
|
|
353
|
+
aria-disabled={loading || restProps.disabled}
|
|
354
|
+
onClick={handleClick}
|
|
355
|
+
onPointerDown={onPointerDown}
|
|
356
|
+
onKeyDown={handleKeyDown}
|
|
357
|
+
style={style}
|
|
358
|
+
// ── Entrance / Exit (FAB visibility) ────────────────
|
|
359
|
+
initial={{ scale: 0.5, opacity: 0, borderRadius: animateRadius }}
|
|
360
|
+
animate={{ scale: 1, opacity: 1, borderRadius: animateRadius }}
|
|
361
|
+
exit={{ scale: 0.5, opacity: 0 }}
|
|
362
|
+
// ── Shape Morphing (extended toggle) ────────────────
|
|
363
|
+
whileTap={{ borderRadius: pressedRadius }}
|
|
364
|
+
transition={{
|
|
365
|
+
borderRadius: SPRING_TRANSITION_FAST,
|
|
366
|
+
scale: SPRING_TRANSITION,
|
|
367
|
+
opacity: { duration: 0.25, ease: "easeOut" },
|
|
368
|
+
}}
|
|
369
|
+
className={cn(
|
|
370
|
+
"relative shrink-0 inline-flex items-center justify-center",
|
|
371
|
+
"select-none cursor-pointer overflow-hidden",
|
|
372
|
+
"transition-[box-shadow,opacity,filter] duration-200",
|
|
373
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-m3-primary focus-visible:ring-offset-2",
|
|
374
|
+
"disabled:pointer-events-none disabled:opacity-[0.38] disabled:shadow-none",
|
|
375
|
+
colors.bg,
|
|
376
|
+
colors.text,
|
|
377
|
+
lowered ? "shadow-sm" : colors.shadow,
|
|
378
|
+
SIZE_STYLES[size] ?? "h-14 w-14",
|
|
379
|
+
extended && "w-auto px-6",
|
|
380
|
+
SIZE_TEXT_CLASS[size],
|
|
381
|
+
loading && "pointer-events-none opacity-75 cursor-not-allowed",
|
|
382
|
+
className,
|
|
383
|
+
)}
|
|
384
|
+
{...restProps}
|
|
385
|
+
>
|
|
386
|
+
{needsTouchTarget && <TouchTarget />}
|
|
387
|
+
|
|
388
|
+
<Ripple ripples={ripples} onRippleDone={removeRipple} />
|
|
389
|
+
|
|
390
|
+
{/* Icon / Loading swap */}
|
|
391
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
392
|
+
{loading ? (
|
|
393
|
+
<m.span
|
|
394
|
+
key="loading"
|
|
395
|
+
{...ICON_SPAN_VARIANTS}
|
|
396
|
+
transition={SPRING_TRANSITION}
|
|
397
|
+
className={cn(
|
|
398
|
+
"flex items-center justify-center shrink-0",
|
|
399
|
+
iconClass,
|
|
400
|
+
)}
|
|
401
|
+
>
|
|
402
|
+
{loadingVariant === "loading-indicator" ? (
|
|
403
|
+
<LoadingIndicator
|
|
404
|
+
size={iconPx}
|
|
405
|
+
color="currentColor"
|
|
406
|
+
aria-label="Loading"
|
|
407
|
+
/>
|
|
408
|
+
) : (
|
|
409
|
+
<ProgressIndicator
|
|
410
|
+
variant="circular"
|
|
411
|
+
size={iconPx}
|
|
412
|
+
color="currentColor"
|
|
413
|
+
trackColor="transparent"
|
|
414
|
+
aria-label="Loading"
|
|
415
|
+
/>
|
|
416
|
+
)}
|
|
417
|
+
</m.span>
|
|
418
|
+
) : (
|
|
419
|
+
<m.span
|
|
420
|
+
key="icon"
|
|
421
|
+
{...ICON_SPAN_VARIANTS}
|
|
422
|
+
transition={SPRING_TRANSITION}
|
|
423
|
+
aria-hidden="true"
|
|
424
|
+
className={cn(
|
|
425
|
+
"flex items-center justify-center shrink-0 [&>svg]:w-full [&>svg]:h-full",
|
|
426
|
+
iconClass,
|
|
427
|
+
)}
|
|
428
|
+
>
|
|
429
|
+
{icon}
|
|
430
|
+
</m.span>
|
|
431
|
+
)}
|
|
432
|
+
</AnimatePresence>
|
|
433
|
+
|
|
434
|
+
{/* Extended label — animates in/out with the `extended` prop */}
|
|
435
|
+
<AnimatePresence initial={false}>
|
|
436
|
+
{extended && children && (
|
|
437
|
+
<m.span
|
|
438
|
+
key="label"
|
|
439
|
+
initial={{ width: 0, opacity: 0 }}
|
|
440
|
+
animate={{ width: "auto", opacity: 1 }}
|
|
441
|
+
exit={{ width: 0, opacity: 0 }}
|
|
442
|
+
transition={SPRING_TRANSITION}
|
|
443
|
+
className="overflow-hidden whitespace-nowrap ml-3"
|
|
444
|
+
>
|
|
445
|
+
{children}
|
|
446
|
+
</m.span>
|
|
447
|
+
)}
|
|
448
|
+
</AnimatePresence>
|
|
449
|
+
</m.button>
|
|
450
|
+
)}
|
|
451
|
+
</AnimatePresence>
|
|
452
|
+
</LazyMotion>
|
|
453
|
+
);
|
|
454
|
+
},
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
FABComponent.displayName = "FAB";
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Action nổi chính theo phong cách MD3 Expressive Floating Action Button (FAB).
|
|
461
|
+
*
|
|
462
|
+
* Phơi nhiễm các action tạo nhịp điệu kích hoạt cho người sử dụng với đủ bộ trang hoàn trọn kích thước Size (SM->XL),
|
|
463
|
+
* mang nhiều màu sắc Color role khác biệt, cung cấp một sức nén cho Label để kéo toẹt cái ống dài ra (gọi là Dạng Mở Rộng - Extended) tạo nên hành động sinh động,
|
|
464
|
+
* Trạng thái load/nhấp hiện xuất cùng animation thu scale thoát cảnh bắt mắt đầy nghệ thuật.
|
|
465
|
+
*
|
|
466
|
+
* @remarks
|
|
467
|
+
* - Chỉ định bắt buộc `aria-label` cho những mẫu icon bị đơn côi trơ trọi (icon-only FABs).
|
|
468
|
+
* - Trường hợp xài mode Mở Rộng qua việc truyền hàm `extended={true}`, nút FAB này tự ngộ nhận thân thế, lấy `children` dùng làm Aria label luôn hễ như `children` đó đang chứa text string.
|
|
469
|
+
* Khi ấy bạn tha hồ cắt bỏ thuộc tính `aria-label` ra.
|
|
470
|
+
* - Lúc cho biến mất (`visible={false}`), bộ nút tung chiêu lùi về sau làm quả rút bóng thoát Scale-out qua effect spring uyển chuyển.
|
|
471
|
+
* - Sài kèm `FABPosition` bao đùm nó lại nếu bạn muốn xích nó cố định ngấm chân sâu góc màn hình hiển thị.
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```tsx
|
|
475
|
+
* // FAB cơ bản, nhỏ xinh, chỉ hiện icon.
|
|
476
|
+
* <FAB icon={<Icon name="search" />} aria-label="Nhấn tìm kiếm" size="sm" />
|
|
477
|
+
*
|
|
478
|
+
* // Dịch sang dòng Extended có dòng caption chữ dài thòn
|
|
479
|
+
* const [isOpen, setOpen] = React.useState(false);
|
|
480
|
+
* <FAB
|
|
481
|
+
* icon={<Icon name="edit" />}
|
|
482
|
+
* extended={isOpen}
|
|
483
|
+
* onClick={() => setOpen(!isOpen)}
|
|
484
|
+
* >
|
|
485
|
+
* Viết tâm thư
|
|
486
|
+
* </FAB>
|
|
487
|
+
*
|
|
488
|
+
* // FAB to lớn nhất dùng trạng thái chờ load Submit lên Server
|
|
489
|
+
* <FAB
|
|
490
|
+
* icon={<Icon name="upload" />}
|
|
491
|
+
* size="lg"
|
|
492
|
+
* loading={isUploading}
|
|
493
|
+
* colorStyle="secondary"
|
|
494
|
+
* aria-label="Upload Files lên mây xanh"
|
|
495
|
+
* />
|
|
496
|
+
*
|
|
497
|
+
* // Cố định dưới chân tay phải
|
|
498
|
+
* <FABPosition position="bottom-right">
|
|
499
|
+
* <FAB icon={<Icon name="add" />} aria-label="Dấu Cộng sinh nảy" />
|
|
500
|
+
* </FABPosition>
|
|
501
|
+
* ```
|
|
502
|
+
*
|
|
503
|
+
* @see https://m3.material.io/components/floating-action-button/overview
|
|
504
|
+
*/
|
|
505
|
+
export const FAB = React.memo(FABComponent);
|