@baodev/mini-app-component 1.0.2 → 1.0.4
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/package.json +8 -5
- package/src/components/{DatePicker.tsx → DataEntry/DatePicker.tsx} +1 -1
- package/src/components/{TimePicker.tsx → DataEntry/TimePicker.tsx} +1 -1
- package/src/components/Feedback/Sheet/Sheet.tsx +152 -0
- package/src/components/Feedback/Sheet/SheetBody.tsx +24 -0
- package/src/components/Feedback/Sheet/SheetFooter.tsx +20 -0
- package/src/components/Feedback/Sheet/SheetHeader.tsx +87 -0
- package/src/components/Feedback/Sheet/index.ts +4 -0
- package/src/index.ts +26 -14
- package/src/utils/cn.ts +6 -0
- /package/src/components/{Button.tsx → Base/Button.tsx} +0 -0
- /package/src/components/{Card.tsx → DataDisPlay/Card.tsx} +0 -0
- /package/src/components/{Collapse.tsx → DataDisPlay/Collapse.tsx} +0 -0
- /package/src/components/{Toast.tsx → DataDisPlay/Toast.tsx} +0 -0
- /package/src/components/{VirtualList.tsx → DataDisPlay/VirtualList.tsx} +0 -0
- /package/src/components/{OTPInput.tsx → DataEntry/OTPInput.tsx} +0 -0
- /package/src/components/{SearchBar.tsx → DataEntry/SearchBar.tsx} +0 -0
- /package/src/components/{BottomSheet.tsx → Feedback/BottomSheet.tsx} +0 -0
- /package/src/components/{Drawer.tsx → Feedback/Drawer.tsx} +0 -0
- /package/src/components/{Modal.tsx → Feedback/Modal.tsx} +0 -0
- /package/src/components/{Tabs.tsx → Layout/Tabs.tsx} +0 -0
- /package/src/components/{Header.tsx → Navigation/Header.tsx} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baodev/mini-app-component",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WEBVIEW COMPONENT",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -17,20 +17,23 @@
|
|
|
17
17
|
"@storybook/addon-docs": "^10.4.1",
|
|
18
18
|
"@storybook/addon-mcp": "^0.6.0",
|
|
19
19
|
"@storybook/addon-vitest": "^10.4.1",
|
|
20
|
+
"@storybook/react": "^10.4.1",
|
|
20
21
|
"@storybook/react-vite": "^10.4.1",
|
|
21
22
|
"@types/react": "latest",
|
|
22
23
|
"@vitest/browser-playwright": "^4.1.7",
|
|
23
24
|
"@vitest/coverage-v8": "^4.1.7",
|
|
25
|
+
"framer-motion": "^12.40.0",
|
|
24
26
|
"playwright": "^1.60.0",
|
|
25
27
|
"storybook": "^10.4.1",
|
|
26
28
|
"tailwindcss": "^3.4.19",
|
|
27
29
|
"tsup": "latest",
|
|
28
30
|
"typescript": "latest",
|
|
29
|
-
"vitest": "^4.1.7"
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
"vitest": "^4.1.7"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"clsx": "^2.1.1",
|
|
35
|
+
"tailwind-merge": "^3.6.0"
|
|
32
36
|
},
|
|
33
|
-
"dependencies": {},
|
|
34
37
|
"scripts": {
|
|
35
38
|
"build": "tsup",
|
|
36
39
|
"storybook": "storybook dev -p 6006",
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
ComponentPropsWithRef,
|
|
4
|
+
MouseEvent,
|
|
5
|
+
useEffect,
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { createPortal } from "react-dom";
|
|
10
|
+
import {
|
|
11
|
+
motion,
|
|
12
|
+
AnimatePresence,
|
|
13
|
+
useMotionValue,
|
|
14
|
+
useTransform,
|
|
15
|
+
} from "framer-motion";
|
|
16
|
+
|
|
17
|
+
// --- BƯỚC ĐỘT PHÁ: Tạo Context để dùng chung state ---
|
|
18
|
+
interface SheetContextType {
|
|
19
|
+
onClose?: () => void;
|
|
20
|
+
}
|
|
21
|
+
export const SheetContext = createContext<SheetContextType>({});
|
|
22
|
+
export const useSheetContext = () => useContext(SheetContext);
|
|
23
|
+
|
|
24
|
+
export type ISheetProps = ComponentPropsWithRef<"div"> & {
|
|
25
|
+
open?: boolean;
|
|
26
|
+
onBackdropClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
layout?: "docked" | "inset";
|
|
29
|
+
radius?: "none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Sheet = forwardRef<HTMLDivElement, ISheetProps>(
|
|
33
|
+
(
|
|
34
|
+
{
|
|
35
|
+
children,
|
|
36
|
+
className = "",
|
|
37
|
+
open = false,
|
|
38
|
+
onBackdropClick,
|
|
39
|
+
onClose,
|
|
40
|
+
layout = "docked",
|
|
41
|
+
radius = "3xl",
|
|
42
|
+
...rest
|
|
43
|
+
},
|
|
44
|
+
ref,
|
|
45
|
+
) => {
|
|
46
|
+
const y = useMotionValue(0);
|
|
47
|
+
|
|
48
|
+
// Hiệu ứng mờ dần lớp nền đen khi kéo Sheet xuống (Đỉnh cao UX)
|
|
49
|
+
const dragOverlayOpacity = useTransform(y, [0, 300], [1, 0]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (open) {
|
|
53
|
+
document.body.style.overflow = "hidden";
|
|
54
|
+
} else {
|
|
55
|
+
document.body.style.overflow = "";
|
|
56
|
+
}
|
|
57
|
+
return () => {
|
|
58
|
+
document.body.style.overflow = "";
|
|
59
|
+
};
|
|
60
|
+
}, [open]);
|
|
61
|
+
|
|
62
|
+
if (typeof document === "undefined") return null;
|
|
63
|
+
|
|
64
|
+
const radiusStyles: Record<string, string> = {
|
|
65
|
+
none: "0px",
|
|
66
|
+
sm: "8px",
|
|
67
|
+
md: "12px",
|
|
68
|
+
lg: "16px",
|
|
69
|
+
xl: "20px",
|
|
70
|
+
"2xl": "24px",
|
|
71
|
+
"3xl": "32px",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const borderRadius = radiusStyles[radius] || "32px";
|
|
75
|
+
|
|
76
|
+
return createPortal(
|
|
77
|
+
<SheetContext.Provider value={{ onClose }}>
|
|
78
|
+
<AnimatePresence>
|
|
79
|
+
{open && (
|
|
80
|
+
<div
|
|
81
|
+
ref={ref}
|
|
82
|
+
className={`fixed inset-0 z-[1000] flex items-end justify-center ${layout === "inset" ? "p-4 pb-safe-bottom" : "p-0"} ${className}`}
|
|
83
|
+
{...rest}
|
|
84
|
+
>
|
|
85
|
+
{/* Lớp nền đen tĩnh */}
|
|
86
|
+
<motion.div
|
|
87
|
+
key="backdrop-static"
|
|
88
|
+
initial={{ opacity: 0 }}
|
|
89
|
+
animate={{ opacity: 1 }}
|
|
90
|
+
exit={{ opacity: 0 }}
|
|
91
|
+
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
92
|
+
className="absolute inset-0 z-10 bg-black/40"
|
|
93
|
+
onClick={(e) => {
|
|
94
|
+
onBackdropClick?.(e);
|
|
95
|
+
onClose?.();
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{/* Lớp nền phản hồi theo lực kéo */}
|
|
100
|
+
<motion.div
|
|
101
|
+
key="backdrop-drag"
|
|
102
|
+
style={{ opacity: dragOverlayOpacity }}
|
|
103
|
+
className="absolute inset-0 z-10 bg-black/20 pointer-events-none"
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
{/* Lõi Sheet */}
|
|
107
|
+
<motion.div
|
|
108
|
+
key="drawer"
|
|
109
|
+
style={{
|
|
110
|
+
y,
|
|
111
|
+
willChange: "transform",
|
|
112
|
+
touchAction: "none",
|
|
113
|
+
borderTopLeftRadius: borderRadius,
|
|
114
|
+
borderTopRightRadius: borderRadius,
|
|
115
|
+
borderBottomLeftRadius:
|
|
116
|
+
layout === "inset" ? borderRadius : "0px",
|
|
117
|
+
borderBottomRightRadius:
|
|
118
|
+
layout === "inset" ? borderRadius : "0px",
|
|
119
|
+
}}
|
|
120
|
+
initial={{ y: "100%" }}
|
|
121
|
+
animate={{ y: 0 }}
|
|
122
|
+
exit={{ y: "100%" }}
|
|
123
|
+
transition={{
|
|
124
|
+
type: "tween",
|
|
125
|
+
duration: 0.35,
|
|
126
|
+
ease: [0.32, 0.94, 0.6, 1],
|
|
127
|
+
}}
|
|
128
|
+
drag="y"
|
|
129
|
+
dragConstraints={{ top: 0 }}
|
|
130
|
+
dragElastic={0.08}
|
|
131
|
+
onDragEnd={(_, info) => {
|
|
132
|
+
if (info.offset.y > 80 || info.velocity.y > 400) {
|
|
133
|
+
onClose?.();
|
|
134
|
+
}
|
|
135
|
+
}}
|
|
136
|
+
className="relative w-full max-h-[90vh] z-20 bg-white overflow-hidden flex flex-col shadow-2xl"
|
|
137
|
+
>
|
|
138
|
+
{/* Thanh cầm kéo (Drag Handle) */}
|
|
139
|
+
<div className="w-12 h-1.5 bg-gray-300 rounded-full mx-auto mt-3 mb-1 flex-shrink-0 cursor-grab active:cursor-grabbing" />
|
|
140
|
+
|
|
141
|
+
{children}
|
|
142
|
+
</motion.div>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</AnimatePresence>
|
|
146
|
+
</SheetContext.Provider>,
|
|
147
|
+
document.body,
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
Sheet.displayName = "Sheet";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, FC } from "react";
|
|
2
|
+
|
|
3
|
+
export type ISheetBodyProps = ComponentPropsWithRef<"div"> & {
|
|
4
|
+
horizontalSpace?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const SheetBody: FC<ISheetBodyProps> = ({
|
|
8
|
+
horizontalSpace = true,
|
|
9
|
+
className = "",
|
|
10
|
+
children,
|
|
11
|
+
...rest
|
|
12
|
+
}) => (
|
|
13
|
+
<div
|
|
14
|
+
className={`w-full flex-1 overflow-y-auto overscroll-contain pt-2 pb-4 ${horizontalSpace ? "px-4" : ""} ${className}`}
|
|
15
|
+
// Đảm bảo nội dung không bị che bởi thanh điều hướng vuốt của iPhone
|
|
16
|
+
style={{
|
|
17
|
+
paddingBottom:
|
|
18
|
+
"calc(var(--ejsc-safe-bottom, env(safe-area-inset-bottom, 0px)) + 16px)",
|
|
19
|
+
}}
|
|
20
|
+
{...rest}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, FC } from "react";
|
|
2
|
+
|
|
3
|
+
export type ISheetFooterProps = ComponentPropsWithRef<"div">;
|
|
4
|
+
|
|
5
|
+
export const SheetFooter: FC<ISheetFooterProps> = ({
|
|
6
|
+
className = "",
|
|
7
|
+
children,
|
|
8
|
+
...rest
|
|
9
|
+
}) => (
|
|
10
|
+
<div
|
|
11
|
+
className={`w-full flex flex-row items-center gap-3 p-4 border-t border-gray-100 bg-white flex-shrink-0 ${className}`}
|
|
12
|
+
style={{
|
|
13
|
+
paddingBottom:
|
|
14
|
+
"calc(var(--ejsc-safe-bottom, env(safe-area-inset-bottom, 0px)) + 16px)",
|
|
15
|
+
}}
|
|
16
|
+
{...rest}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, FC, ReactNode } from "react";
|
|
2
|
+
import { useSheetContext } from "./Sheet";
|
|
3
|
+
|
|
4
|
+
export type ISheetHeaderProps = ComponentPropsWithRef<"div"> & {
|
|
5
|
+
title?: ReactNode;
|
|
6
|
+
backIcon?: boolean | ReactNode;
|
|
7
|
+
onBackClick?: () => void;
|
|
8
|
+
closeIcon?: boolean | ReactNode;
|
|
9
|
+
onCloseClick?: () => void; // Vẫn cho phép truyền prop này để ghi đè nếu muốn
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const SheetHeader: FC<ISheetHeaderProps> = ({
|
|
13
|
+
title,
|
|
14
|
+
backIcon,
|
|
15
|
+
onBackClick,
|
|
16
|
+
closeIcon = true, // Mặc định hiện nút Close
|
|
17
|
+
onCloseClick,
|
|
18
|
+
className = "",
|
|
19
|
+
...rest
|
|
20
|
+
}) => {
|
|
21
|
+
// Tự động lấy hàm onClose từ Component Sheet bọc ngoài
|
|
22
|
+
const { onClose } = useSheetContext();
|
|
23
|
+
const handleClose = onCloseClick || onClose;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={`flex items-center justify-between px-4 py-3 border-b border-gray-100 w-full flex-shrink-0 ${className}`}
|
|
28
|
+
{...rest}
|
|
29
|
+
>
|
|
30
|
+
<div className="w-10 flex items-center justify-start">
|
|
31
|
+
{backIcon && (
|
|
32
|
+
<button onClick={onBackClick} className="text-gray-800 p-1">
|
|
33
|
+
{typeof backIcon === "boolean" ? (
|
|
34
|
+
<svg
|
|
35
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
36
|
+
width="24"
|
|
37
|
+
height="24"
|
|
38
|
+
viewBox="0 0 24 24"
|
|
39
|
+
fill="none"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
strokeWidth="2.5"
|
|
42
|
+
strokeLinecap="round"
|
|
43
|
+
strokeLinejoin="round"
|
|
44
|
+
>
|
|
45
|
+
<path d="m15 18-6-6 6-6" />
|
|
46
|
+
</svg>
|
|
47
|
+
) : (
|
|
48
|
+
backIcon
|
|
49
|
+
)}
|
|
50
|
+
</button>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="flex-1 text-center font-semibold text-base text-gray-900 truncate">
|
|
55
|
+
{title}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="w-10 flex items-center justify-end">
|
|
59
|
+
{(closeIcon || handleClose) && (
|
|
60
|
+
<button
|
|
61
|
+
onClick={handleClose}
|
|
62
|
+
className="text-gray-500 hover:bg-gray-100 rounded-full p-1 transition-colors"
|
|
63
|
+
>
|
|
64
|
+
{closeIcon && typeof closeIcon !== "boolean" ? (
|
|
65
|
+
closeIcon
|
|
66
|
+
) : (
|
|
67
|
+
<svg
|
|
68
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
69
|
+
width="22"
|
|
70
|
+
height="22"
|
|
71
|
+
viewBox="0 0 24 24"
|
|
72
|
+
fill="none"
|
|
73
|
+
stroke="currentColor"
|
|
74
|
+
strokeWidth="2.5"
|
|
75
|
+
strokeLinecap="round"
|
|
76
|
+
strokeLinejoin="round"
|
|
77
|
+
>
|
|
78
|
+
<path d="M18 6 6 18" />
|
|
79
|
+
<path d="m6 6 12 12" />
|
|
80
|
+
</svg>
|
|
81
|
+
)}
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
export
|
|
1
|
+
// Base
|
|
2
|
+
export { Button } from "./components/Base/Button";
|
|
3
|
+
|
|
4
|
+
// Data Display
|
|
5
|
+
export * from "./components/DataDisPlay/Toast";
|
|
6
|
+
export * from "./components/DataDisPlay/Card";
|
|
7
|
+
export * from "./components/DataDisPlay/VirtualList";
|
|
8
|
+
export * from "./components/DataDisPlay/Collapse";
|
|
9
|
+
|
|
10
|
+
// Data Entry
|
|
11
|
+
export * from "./components/DataEntry/DatePicker";
|
|
12
|
+
export * from "./components/DataEntry/TimePicker";
|
|
13
|
+
export * from "./components/DataEntry/SearchBar";
|
|
14
|
+
export * from "./components/DataEntry/OTPInput";
|
|
15
|
+
|
|
16
|
+
// Feedback
|
|
17
|
+
export * from "./components/Feedback/Modal";
|
|
18
|
+
export * from "./components/Feedback/Drawer";
|
|
19
|
+
export * from "./components/Feedback/Sheet";
|
|
20
|
+
export * from "./components/Feedback/BottomSheet";
|
|
21
|
+
|
|
22
|
+
// Layout
|
|
23
|
+
export * from "./components/Layout/Tabs";
|
|
24
|
+
|
|
25
|
+
// Navigation
|
|
26
|
+
export * from "./components/Navigation/Header";
|
package/src/utils/cn.ts
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|