@baodev/mini-app-component 1.0.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baodev/mini-app-component",
3
- "version": "1.0.3",
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
- "@storybook/react": "^10.4.1",
31
- "framer-motion": "^12.40.0"
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",
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useEffect, useCallback } from "react";
2
- import { BottomSheet } from "./BottomSheet";
2
+ import { BottomSheet } from "../Feedback/BottomSheet";
3
3
 
4
4
  // ================================
5
5
  // Types
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useEffect, useCallback } from "react";
2
- import { BottomSheet } from "./BottomSheet";
2
+ import { BottomSheet } from "../Feedback/BottomSheet";
3
3
 
4
4
  // ================================
5
5
  // Types
@@ -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
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./Sheet";
2
+ export * from "./SheetHeader";
3
+ export * from "./SheetBody";
4
+ export * from "./SheetFooter";
package/src/index.ts CHANGED
@@ -1,14 +1,26 @@
1
- export { Button } from "./components/Button";
2
- export { Header } from "./components/Header";
3
- export { BottomSheet } from "./components/BottomSheet";
4
- export { ToastProvider, useToast } from "./components/Toast";
5
- export { DatePicker } from "./components/DatePicker";
6
- export { TimePicker } from "./components/TimePicker";
7
- export { OTPInput } from "./components/OTPInput";
8
- export { Card } from "./components/Card";
9
- export { VirtualList } from "./components/VirtualList";
10
- export { SearchBar } from "./components/SearchBar";
11
- export { Modal } from './components/Modal'
12
- export { Tabs } from './components/Tabs'
13
- export { Collapse } from './components/Collapse'
14
- export { Drawer } from './components/Drawer'
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";
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
File without changes
File without changes