@imksh/create 1.0.28 → 1.0.31
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 +1 -1
- package/src/utils/askTemplate.js +2 -1
- package/templates/layers/electron/base/electron/main.js +262 -0
- package/templates/layers/electron/base/electron/preload.js +37 -0
- package/templates/layers/electron/base/electron-builder.json5 +68 -0
- package/templates/layers/electron/javascript/_gitignore +38 -0
- package/templates/layers/electron/javascript/package.json +49 -0
- package/templates/layers/electron/javascript/src/components/layout/SidebarLayout.jsx +43 -0
- package/templates/layers/electron/javascript/src/components/ui/TitleBar.jsx +130 -0
- package/templates/layers/electron/javascript/src/config/appConfig.js +51 -0
- package/templates/layers/electron/javascript/src/hooks/useElectron.js +68 -0
- package/templates/layers/electron/javascript/src/main.jsx +17 -0
- package/templates/layers/electron/javascript/src/store/useUiStore.js +59 -0
- package/templates/layers/electron/javascript/vite.config.js +29 -0
- package/templates/layers/electron/typescript/_gitignore +38 -0
- package/templates/layers/electron/typescript/electron/main.ts +213 -0
- package/templates/layers/electron/typescript/electron/preload.ts +52 -0
- package/templates/layers/electron/typescript/package.json +52 -0
- package/templates/layers/electron/typescript/src/components/layout/SidebarLayout.tsx +42 -0
- package/templates/layers/electron/typescript/src/components/ui/TitleBar.tsx +120 -0
- package/templates/layers/electron/typescript/src/config/appConfig.ts +52 -0
- package/templates/layers/electron/typescript/src/hooks/useElectron.ts +68 -0
- package/templates/layers/electron/typescript/src/main.tsx +17 -0
- package/templates/layers/electron/typescript/src/store/useUiStore.ts +97 -0
- package/templates/layers/electron/typescript/src/vite-env.d.ts +13 -0
- package/templates/layers/electron/typescript/tsconfig.json +25 -0
- package/templates/layers/electron/typescript/tsconfig.node.json +11 -0
- package/templates/layers/electron/typescript/vite.config.ts +29 -0
- package/templates/layers/next/javascript/src/components/common/Breadcrumbs.jsx +44 -6
- package/templates/layers/next/javascript/src/components/common/Loading.jsx +1 -0
- package/templates/layers/next/javascript/src/components/common/LottieIcon.jsx +1 -0
- package/templates/layers/next/javascript/src/components/common/Pagination.jsx +170 -0
- package/templates/layers/next/javascript/src/components/common/SidebarModal.jsx +180 -0
- package/templates/layers/next/javascript/src/components/common/StatsCard.jsx +168 -0
- package/templates/layers/next/javascript/src/components/common/Table.jsx +191 -41
- package/templates/layers/next/javascript/src/components/common/Tabs.jsx +187 -0
- package/templates/layers/next/javascript/src/components/common/ToggleSwitch.jsx +4 -3
- package/templates/layers/next/javascript/src/components/layout/ProtectedRoute.jsx +2 -1
- package/templates/layers/next/javascript/src/components/layout/PublicLayout.jsx +3 -6
- package/templates/layers/next/javascript/src/components/layout/SidebarLayout.jsx +4 -17
- package/templates/layers/next/javascript/src/components/modals/ConfirmModal.jsx +4 -12
- package/templates/layers/next/javascript/src/components/modals/Modal.jsx +50 -19
- package/templates/layers/next/javascript/src/components/ui/Input.jsx +50 -0
- package/templates/layers/next/{typescript/src/components/ui/sidebar.tsx → javascript/src/components/ui/Sidebar.jsx} +65 -53
- package/templates/layers/next/javascript/src/components/ui/SidebarHeader.jsx +11 -6
- package/templates/layers/next/javascript/src/config/appConfig.js +4 -4
- package/templates/layers/next/javascript/src/hooks/useWindowSize.js +2 -3
- package/templates/layers/next/javascript/src/store/useUiStore.js +9 -4
- package/templates/layers/next/typescript/src/components/common/Breadcrumbs.tsx +57 -8
- package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -0
- package/templates/layers/next/typescript/src/components/common/Pagination.tsx +186 -0
- package/templates/layers/next/typescript/src/components/common/SidebarModal.tsx +200 -0
- package/templates/layers/next/typescript/src/components/common/StatsCard.tsx +232 -0
- package/templates/layers/next/typescript/src/components/common/Table.tsx +201 -35
- package/templates/layers/next/typescript/src/components/common/Tabs.tsx +190 -0
- package/templates/layers/next/typescript/src/components/common/ToggleSwitch.tsx +12 -8
- package/templates/layers/next/typescript/src/components/layout/PublicLayout.tsx +8 -6
- package/templates/layers/next/typescript/src/components/layout/SidebarLayout.tsx +8 -17
- package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +1 -0
- package/templates/layers/next/typescript/src/components/modals/Modal.tsx +67 -21
- package/templates/layers/next/typescript/src/components/ui/Button.tsx +1 -0
- package/templates/layers/next/typescript/src/components/ui/Input.tsx +40 -60
- package/templates/layers/next/{javascript/src/components/ui/sidebar.jsx → typescript/src/components/ui/Sidebar.tsx} +77 -44
- package/templates/layers/next/typescript/src/components/ui/SidebarHeader.tsx +16 -10
- package/templates/layers/next/typescript/src/config/appConfig.ts +4 -4
- package/templates/layers/next/typescript/src/hooks/useWindowSize.ts +2 -3
- package/templates/layers/next/typescript/src/store/useUiStore.ts +14 -4
- package/templates/layers/node/javascript/package.json +2 -1
- package/templates/layers/node/javascript/src/controllers/push.controller.js +35 -14
- package/templates/layers/node/javascript/src/models/pushSubscription.model.js +5 -0
- package/templates/layers/node/javascript/src/utils/pushHelper.js +59 -12
- package/templates/layers/node/typescript/package-lock.json +3795 -0
- package/templates/layers/node/typescript/package.json +3 -2
- package/templates/layers/node/typescript/src/controllers/push.controller.ts +35 -14
- package/templates/layers/node/typescript/src/models/pushSubscription.model.ts +6 -0
- package/templates/layers/node/typescript/src/services/auth.service.ts +3 -3
- package/templates/layers/node/typescript/src/utils/pushHelper.ts +66 -12
- package/templates/layers/node-prisma/javascript/package.json +6 -3
- package/templates/layers/node-prisma/javascript/prisma/schema.prisma +20 -9
- package/templates/layers/node-prisma/javascript/prisma.config.js +12 -0
- package/templates/layers/node-prisma/javascript/src/config/{db.js → prisma.js} +9 -1
- package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +58 -19
- package/templates/layers/node-prisma/javascript/src/utils/pushHelper.js +62 -14
- package/templates/layers/node-prisma/typescript/package-lock.json +4808 -0
- package/templates/layers/node-prisma/typescript/package.json +8 -4
- package/templates/layers/node-prisma/typescript/prisma/schema.prisma +20 -9
- package/templates/layers/node-prisma/typescript/prisma.config.ts +12 -0
- package/templates/layers/node-prisma/typescript/src/config/{db.ts → prisma.ts} +9 -1
- package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -22
- package/templates/layers/node-prisma/typescript/src/index.ts +1 -1
- package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +15 -5
- package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +134 -128
- package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +69 -14
- package/templates/layers/prisma/typescript/prisma/schema.prisma +35 -10
- package/templates/layers/react/javascript/src/components/common/Breadcrumbs.jsx +62 -58
- package/templates/layers/react/javascript/src/components/common/Pagination.jsx +164 -76
- package/templates/layers/react/javascript/src/components/common/SidebarModal.jsx +179 -0
- package/templates/layers/react/javascript/src/components/common/StatsCard.jsx +167 -0
- package/templates/layers/react/javascript/src/components/common/Table.jsx +248 -132
- package/templates/layers/react/javascript/src/components/common/Tabs.jsx +164 -112
- package/templates/layers/react/javascript/src/components/common/ToggleSwitch.jsx +3 -3
- package/templates/layers/react/javascript/src/components/layout/SidebarLayout.jsx +4 -12
- package/templates/layers/react/javascript/src/components/modals/Modal.jsx +53 -18
- package/templates/layers/react/javascript/src/components/ui/Input.jsx +49 -0
- package/templates/layers/react/javascript/src/components/ui/Sidebar.jsx +215 -0
- package/templates/layers/react/javascript/src/components/ui/SidebarHeader.jsx +25 -30
- package/templates/layers/react/javascript/src/config/appConfig.js +3 -3
- package/templates/layers/react/javascript/src/hooks/useWindowSize.js +2 -2
- package/templates/layers/react/typescript/src/components/common/Breadcrumbs.tsx +32 -6
- package/templates/layers/react/typescript/src/components/common/Pagination.tsx +151 -57
- package/templates/layers/react/typescript/src/components/common/SidebarModal.tsx +199 -0
- package/templates/layers/react/typescript/src/components/common/StatsCard.tsx +231 -0
- package/templates/layers/react/typescript/src/components/common/Table.tsx +197 -34
- package/templates/layers/react/typescript/src/components/common/Tabs.tsx +56 -12
- package/templates/layers/react/typescript/src/components/common/ToggleSwitch.tsx +15 -3
- package/templates/layers/react/typescript/src/components/layout/SidebarLayout.tsx +3 -6
- package/templates/layers/react/typescript/src/components/modals/Modal.tsx +151 -0
- package/templates/layers/react/typescript/src/components/ui/Input.tsx +58 -0
- package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +116 -53
- package/templates/layers/react/typescript/src/components/ui/SidebarHeader.tsx +5 -4
- package/templates/layers/react/typescript/src/config/appConfig.ts +3 -3
- package/templates/layers/react/typescript/src/hooks/useWindowSize.ts +2 -3
- package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +2 -1
- package/templates/layers/react-native/base/src/config/appConfig.ts +44 -0
- package/templates/presets/electron/preset.json +9 -0
- package/templates/presets/electron-ts/preset.json +9 -0
- package/templates/layers/react/javascript/src/components/ui/sidebar.jsx +0 -169
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface PaginationProps {
|
|
6
|
+
currentPage: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
onPageChange: (page: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Pagination: React.FC<PaginationProps> = ({
|
|
12
|
+
currentPage,
|
|
13
|
+
totalPages,
|
|
14
|
+
onPageChange,
|
|
15
|
+
}) => {
|
|
16
|
+
if (totalPages <= 1) return null;
|
|
17
|
+
|
|
18
|
+
// Desktop pagination: 5 pages with outer bounds & ellipsis
|
|
19
|
+
const maxDesktopPages = 5;
|
|
20
|
+
let startDesktopPage = Math.max(
|
|
21
|
+
1,
|
|
22
|
+
currentPage - Math.floor(maxDesktopPages / 2),
|
|
23
|
+
);
|
|
24
|
+
let endDesktopPage = Math.min(
|
|
25
|
+
totalPages,
|
|
26
|
+
startDesktopPage + maxDesktopPages - 1,
|
|
27
|
+
);
|
|
28
|
+
if (endDesktopPage - startDesktopPage + 1 < maxDesktopPages) {
|
|
29
|
+
startDesktopPage = Math.max(1, endDesktopPage - maxDesktopPages + 1);
|
|
30
|
+
}
|
|
31
|
+
const desktopPages: number[] = [];
|
|
32
|
+
for (let i = startDesktopPage; i <= endDesktopPage; i++) {
|
|
33
|
+
desktopPages.push(i);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Small screen pagination: exactly 3 page circles
|
|
37
|
+
const maxMobilePages = 3;
|
|
38
|
+
let startMobilePage = Math.max(
|
|
39
|
+
1,
|
|
40
|
+
currentPage - Math.floor(maxMobilePages / 2),
|
|
41
|
+
);
|
|
42
|
+
let endMobilePage = Math.min(
|
|
43
|
+
totalPages,
|
|
44
|
+
startMobilePage + maxMobilePages - 1,
|
|
45
|
+
);
|
|
46
|
+
if (endMobilePage - startMobilePage + 1 < maxMobilePages) {
|
|
47
|
+
startMobilePage = Math.max(1, endMobilePage - maxMobilePages + 1);
|
|
48
|
+
}
|
|
49
|
+
const mobilePages: number[] = [];
|
|
50
|
+
for (let i = startMobilePage; i <= endMobilePage; i++) {
|
|
51
|
+
mobilePages.push(i);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const baseBtnClass =
|
|
55
|
+
"relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1.5 sm:px-2 text-xs sm:text-sm font-medium transition-all duration-200 rounded-full bg-base-100 hover:bg-base-200 text-base-content/70 hover:text-base-content focus:z-10 focus:outline-none focus:ring-2 focus:ring-primary/50 shadow-sm";
|
|
56
|
+
const activeBtnClass =
|
|
57
|
+
"relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1.5 sm:px-2 text-xs sm:text-sm font-bold transition-all duration-200 rounded-full border border-primary bg-primary text-primary-content z-10 focus:outline-none focus:ring-2 focus:ring-primary/50 shadow-sm shadow-primary/20";
|
|
58
|
+
const disabledBtnClass =
|
|
59
|
+
"relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1.5 sm:px-2 text-xs sm:text-sm font-medium transition-all duration-200 rounded-full bg-base-100 text-base-content/30 cursor-not-allowed shadow-sm";
|
|
60
|
+
const ellipsisClass =
|
|
61
|
+
"relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1 sm:px-2 text-xs sm:text-sm font-medium text-base-content/40 cursor-default";
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
{/* Mobile / Small Screen: 3 page circles + ellipses + boundary pages */}
|
|
66
|
+
<div className="flex sm:hidden items-center justify-center gap-1">
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => onPageChange(currentPage - 1)}
|
|
69
|
+
disabled={currentPage === 1}
|
|
70
|
+
className={currentPage === 1 ? disabledBtnClass : baseBtnClass}
|
|
71
|
+
aria-label="Previous Page"
|
|
72
|
+
>
|
|
73
|
+
<ChevronLeft className="w-4 h-4" />
|
|
74
|
+
</button>
|
|
75
|
+
|
|
76
|
+
{startMobilePage > 1 && (
|
|
77
|
+
<>
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => onPageChange(1)}
|
|
80
|
+
className={baseBtnClass}
|
|
81
|
+
>
|
|
82
|
+
1
|
|
83
|
+
</button>
|
|
84
|
+
{startMobilePage > 2 && (
|
|
85
|
+
<span className={ellipsisClass}>...</span>
|
|
86
|
+
)}
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{mobilePages.map((page) => (
|
|
91
|
+
<button
|
|
92
|
+
key={`mobile-${page}`}
|
|
93
|
+
onClick={() => onPageChange(page)}
|
|
94
|
+
className={currentPage === page ? activeBtnClass : baseBtnClass}
|
|
95
|
+
>
|
|
96
|
+
{page}
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
|
|
100
|
+
{endMobilePage < totalPages && (
|
|
101
|
+
<>
|
|
102
|
+
{endMobilePage < totalPages - 1 && (
|
|
103
|
+
<span className={ellipsisClass}>...</span>
|
|
104
|
+
)}
|
|
105
|
+
<button
|
|
106
|
+
onClick={() => onPageChange(totalPages)}
|
|
107
|
+
className={baseBtnClass}
|
|
108
|
+
>
|
|
109
|
+
{totalPages}
|
|
110
|
+
</button>
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => onPageChange(currentPage + 1)}
|
|
116
|
+
disabled={currentPage === totalPages}
|
|
117
|
+
className={currentPage === totalPages ? disabledBtnClass : baseBtnClass}
|
|
118
|
+
aria-label="Next Page"
|
|
119
|
+
>
|
|
120
|
+
<ChevronRight className="w-4 h-4" />
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Desktop Screen: 5 page circles + ellipses + boundary pages */}
|
|
125
|
+
<div className="hidden sm:flex items-center justify-center gap-1.5">
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => onPageChange(currentPage - 1)}
|
|
128
|
+
disabled={currentPage === 1}
|
|
129
|
+
className={currentPage === 1 ? disabledBtnClass : baseBtnClass}
|
|
130
|
+
aria-label="Previous Page"
|
|
131
|
+
>
|
|
132
|
+
<ChevronLeft className="w-4 h-4" />
|
|
133
|
+
</button>
|
|
134
|
+
|
|
135
|
+
{startDesktopPage > 1 && (
|
|
136
|
+
<>
|
|
137
|
+
<button
|
|
138
|
+
onClick={() => onPageChange(1)}
|
|
139
|
+
className={baseBtnClass}
|
|
140
|
+
>
|
|
141
|
+
1
|
|
142
|
+
</button>
|
|
143
|
+
{startDesktopPage > 2 && (
|
|
144
|
+
<span className={ellipsisClass}>...</span>
|
|
145
|
+
)}
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{desktopPages.map((page) => (
|
|
150
|
+
<button
|
|
151
|
+
key={`desktop-${page}`}
|
|
152
|
+
onClick={() => onPageChange(page)}
|
|
153
|
+
className={currentPage === page ? activeBtnClass : baseBtnClass}
|
|
154
|
+
>
|
|
155
|
+
{page}
|
|
156
|
+
</button>
|
|
157
|
+
))}
|
|
158
|
+
|
|
159
|
+
{endDesktopPage < totalPages && (
|
|
160
|
+
<>
|
|
161
|
+
{endDesktopPage < totalPages - 1 && (
|
|
162
|
+
<span className={ellipsisClass}>...</span>
|
|
163
|
+
)}
|
|
164
|
+
<button
|
|
165
|
+
onClick={() => onPageChange(totalPages)}
|
|
166
|
+
className={baseBtnClass}
|
|
167
|
+
>
|
|
168
|
+
{totalPages}
|
|
169
|
+
</button>
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => onPageChange(currentPage + 1)}
|
|
175
|
+
disabled={currentPage === totalPages}
|
|
176
|
+
className={currentPage === totalPages ? disabledBtnClass : baseBtnClass}
|
|
177
|
+
aria-label="Next Page"
|
|
178
|
+
>
|
|
179
|
+
<ChevronRight className="w-4 h-4" />
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export default Pagination;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useEffect, ReactNode } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import { IoClose } from "react-icons/io5";
|
|
6
|
+
import { useUiStore } from "../../store/useUiStore";
|
|
7
|
+
|
|
8
|
+
export type SidebarModalSide = "left" | "right";
|
|
9
|
+
|
|
10
|
+
export interface SidebarModalProps {
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
side?: SidebarModalSide;
|
|
14
|
+
title?: ReactNode;
|
|
15
|
+
description?: ReactNode;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
width?: string;
|
|
18
|
+
showCloseButton?: boolean;
|
|
19
|
+
closeOnBackdrop?: boolean;
|
|
20
|
+
closeOnEsc?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
header?: ReactNode;
|
|
23
|
+
footer?: ReactNode;
|
|
24
|
+
bodyClassName?: string;
|
|
25
|
+
hasBackdrop?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SidebarModal: React.FC<SidebarModalProps> = ({
|
|
29
|
+
isOpen,
|
|
30
|
+
onClose,
|
|
31
|
+
side = "right",
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
children,
|
|
35
|
+
width = "w-full sm:w-[420px]",
|
|
36
|
+
showCloseButton = true,
|
|
37
|
+
closeOnBackdrop = true,
|
|
38
|
+
closeOnEsc = true,
|
|
39
|
+
className = "",
|
|
40
|
+
header,
|
|
41
|
+
footer,
|
|
42
|
+
bodyClassName = "p-4 space-y-4",
|
|
43
|
+
hasBackdrop = true,
|
|
44
|
+
}) => {
|
|
45
|
+
const setModalOpen = useUiStore((state) => state.setModalOpen);
|
|
46
|
+
|
|
47
|
+
// Sync with global UI store modal state for scroll locking
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (isOpen) {
|
|
50
|
+
setModalOpen(true);
|
|
51
|
+
return () => {
|
|
52
|
+
setModalOpen(false);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}, [isOpen, setModalOpen]);
|
|
56
|
+
|
|
57
|
+
// Handle ESC key
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!isOpen || !closeOnEsc) return;
|
|
60
|
+
|
|
61
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
62
|
+
if (e.key === "Escape") {
|
|
63
|
+
onClose();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
68
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
69
|
+
}, [isOpen, closeOnEsc, onClose]);
|
|
70
|
+
|
|
71
|
+
const slideVariants = {
|
|
72
|
+
hidden: {
|
|
73
|
+
x: side === "right" ? "100%" : "-100%",
|
|
74
|
+
opacity: 0.7,
|
|
75
|
+
},
|
|
76
|
+
visible: {
|
|
77
|
+
x: 0,
|
|
78
|
+
opacity: 1,
|
|
79
|
+
transition: {
|
|
80
|
+
type: "spring",
|
|
81
|
+
damping: 30,
|
|
82
|
+
stiffness: 300,
|
|
83
|
+
mass: 0.8,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
exit: {
|
|
87
|
+
x: side === "right" ? "100%" : "-100%",
|
|
88
|
+
opacity: 0.5,
|
|
89
|
+
transition: {
|
|
90
|
+
type: "tween",
|
|
91
|
+
duration: 0.22,
|
|
92
|
+
ease: "easeInOut",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const backdropVariants = {
|
|
98
|
+
hidden: { opacity: 0 },
|
|
99
|
+
visible: { opacity: 1, transition: { duration: 0.2 } },
|
|
100
|
+
exit: { opacity: 0, transition: { duration: 0.18 } },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const content = (
|
|
104
|
+
<AnimatePresence>
|
|
105
|
+
{isOpen && (
|
|
106
|
+
<div className="fixed inset-0 z-50 overflow-hidden">
|
|
107
|
+
{/* Backdrop */}
|
|
108
|
+
{hasBackdrop && (
|
|
109
|
+
<motion.div
|
|
110
|
+
variants={backdropVariants}
|
|
111
|
+
initial="hidden"
|
|
112
|
+
animate="visible"
|
|
113
|
+
exit="exit"
|
|
114
|
+
className="fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity"
|
|
115
|
+
onClick={() => {
|
|
116
|
+
if (closeOnBackdrop) onClose();
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Sliding Panel */}
|
|
122
|
+
<div
|
|
123
|
+
className={`fixed inset-y-0 ${
|
|
124
|
+
side === "right" ? "right-0" : "left-0"
|
|
125
|
+
} max-w-full flex`}
|
|
126
|
+
>
|
|
127
|
+
<motion.div
|
|
128
|
+
variants={slideVariants}
|
|
129
|
+
initial="hidden"
|
|
130
|
+
animate="visible"
|
|
131
|
+
exit="exit"
|
|
132
|
+
className={`
|
|
133
|
+
relative h-full flex flex-col bg-base-100 border-base-300 shadow-2xl overflow-hidden
|
|
134
|
+
${side === "right" ? "border-l" : "border-r"}
|
|
135
|
+
${width}
|
|
136
|
+
${className}
|
|
137
|
+
`}
|
|
138
|
+
onClick={(e) => e.stopPropagation()}
|
|
139
|
+
>
|
|
140
|
+
{/* Optional Custom Header or Default Header */}
|
|
141
|
+
{header ? (
|
|
142
|
+
<div className="shrink-0">{header}</div>
|
|
143
|
+
) : title ? (
|
|
144
|
+
<div className="flex items-center justify-between p-4 border-b border-base-300 bg-base-200/40 shrink-0">
|
|
145
|
+
<div className="flex-1 min-w-0 pr-2">
|
|
146
|
+
<h2 className="text-base font-bold text-base-content truncate">
|
|
147
|
+
{title}
|
|
148
|
+
</h2>
|
|
149
|
+
{description && (
|
|
150
|
+
<p className="text-xs text-base-content/60 truncate mt-0.5">
|
|
151
|
+
{description}
|
|
152
|
+
</p>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
{showCloseButton && (
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
className="btn btn-circle btn-sm btn-error btn-soft text-error/70 hover:text-error hover:bg-error/20 hover:shadow-sm hover:rotate-180 transition-transform duration-200"
|
|
159
|
+
onClick={onClose}
|
|
160
|
+
title="Close"
|
|
161
|
+
>
|
|
162
|
+
<IoClose size={18} />
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
) : showCloseButton ? (
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className="btn btn-circle btn-sm btn-error btn-soft text-error/70 hover:text-error hover:bg-error/20 hover:shadow-sm hover:rotate-180 transition-transform duration-200"
|
|
170
|
+
onClick={onClose}
|
|
171
|
+
title="Close"
|
|
172
|
+
>
|
|
173
|
+
<IoClose size={18} />
|
|
174
|
+
</button>
|
|
175
|
+
) : null}
|
|
176
|
+
|
|
177
|
+
{/* Scrollable Body */}
|
|
178
|
+
<div className={`flex-1 overflow-y-auto ${bodyClassName}`}>
|
|
179
|
+
{children}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Optional Footer */}
|
|
183
|
+
{footer && (
|
|
184
|
+
<div className="p-4 border-t border-base-300 bg-base-200/30 shrink-0">
|
|
185
|
+
{footer}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</motion.div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</AnimatePresence>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return typeof document !== "undefined"
|
|
196
|
+
? createPortal(content, document.body)
|
|
197
|
+
: null;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export default SidebarModal;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type StatsCardColor =
|
|
5
|
+
| "blue"
|
|
6
|
+
| "emerald"
|
|
7
|
+
| "amber"
|
|
8
|
+
| "purple"
|
|
9
|
+
| "primary"
|
|
10
|
+
| "secondary"
|
|
11
|
+
| "success"
|
|
12
|
+
| "warning"
|
|
13
|
+
| "info"
|
|
14
|
+
| "error";
|
|
15
|
+
|
|
16
|
+
export interface StatsCardTrend {
|
|
17
|
+
value: string | number;
|
|
18
|
+
isPositive?: boolean;
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StatsCardBadge {
|
|
23
|
+
label: string | number;
|
|
24
|
+
variant?:
|
|
25
|
+
| "info"
|
|
26
|
+
| "success"
|
|
27
|
+
| "warning"
|
|
28
|
+
| "error"
|
|
29
|
+
| "neutral"
|
|
30
|
+
| "ghost"
|
|
31
|
+
| "secondary"
|
|
32
|
+
| "primary";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StatsCardProps {
|
|
36
|
+
/** Main title or label of the stat card */
|
|
37
|
+
title: ReactNode;
|
|
38
|
+
/** Primary metric value (e.g. number, count, currency) */
|
|
39
|
+
value: ReactNode;
|
|
40
|
+
/** Secondary value displayed beside the main value (e.g. "75%", "Queue", "USD") */
|
|
41
|
+
subValue?: ReactNode;
|
|
42
|
+
/** Trend indicator with direction and value */
|
|
43
|
+
trend?: StatsCardTrend | ReactNode;
|
|
44
|
+
/** Badge displayed in the bottom footer row */
|
|
45
|
+
badge?: StatsCardBadge | ReactNode;
|
|
46
|
+
/** Descriptive subtext displayed in the footer row */
|
|
47
|
+
description?: ReactNode;
|
|
48
|
+
/** Icon displayed in the top-right corner */
|
|
49
|
+
icon?: ReactNode;
|
|
50
|
+
/** Color theme for icon container and active selection border */
|
|
51
|
+
color?: StatsCardColor;
|
|
52
|
+
/** Active selection state with highlighting ring */
|
|
53
|
+
active?: boolean;
|
|
54
|
+
/** Displays a loading spinner in place of value */
|
|
55
|
+
loading?: boolean;
|
|
56
|
+
/** Click handler for interactive selection / drill-down */
|
|
57
|
+
onClick?: () => void;
|
|
58
|
+
/** Custom additional container classes */
|
|
59
|
+
className?: string;
|
|
60
|
+
/** @deprecated use `subValue` instead */
|
|
61
|
+
percentage?: number | string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const COLOR_STYLES: Record<
|
|
65
|
+
StatsCardColor,
|
|
66
|
+
{
|
|
67
|
+
icon: string;
|
|
68
|
+
activeBorder: string;
|
|
69
|
+
}
|
|
70
|
+
> = {
|
|
71
|
+
blue: {
|
|
72
|
+
icon: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
|
|
73
|
+
activeBorder:
|
|
74
|
+
"border-blue-500/50 shadow-sm ring-1 ring-blue-500/20 bg-gradient-to-br from-base-100 to-blue-500/[0.03]",
|
|
75
|
+
},
|
|
76
|
+
emerald: {
|
|
77
|
+
icon: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
|
78
|
+
activeBorder:
|
|
79
|
+
"border-emerald-500/50 shadow-sm ring-1 ring-emerald-500/20 bg-gradient-to-br from-base-100 to-emerald-500/[0.03]",
|
|
80
|
+
},
|
|
81
|
+
amber: {
|
|
82
|
+
icon: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
|
|
83
|
+
activeBorder:
|
|
84
|
+
"border-amber-500/50 shadow-sm ring-1 ring-amber-500/20 bg-gradient-to-br from-base-100 to-amber-500/[0.03]",
|
|
85
|
+
},
|
|
86
|
+
purple: {
|
|
87
|
+
icon: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
|
|
88
|
+
activeBorder:
|
|
89
|
+
"border-purple-500/50 shadow-sm ring-1 ring-purple-500/20 bg-gradient-to-br from-base-100 to-purple-500/[0.03]",
|
|
90
|
+
},
|
|
91
|
+
primary: {
|
|
92
|
+
icon: "bg-primary/10 text-primary",
|
|
93
|
+
activeBorder:
|
|
94
|
+
"border-primary/50 shadow-sm ring-1 ring-primary/20 bg-gradient-to-br from-base-100 to-primary/[0.03]",
|
|
95
|
+
},
|
|
96
|
+
secondary: {
|
|
97
|
+
icon: "bg-secondary/10 text-secondary",
|
|
98
|
+
activeBorder:
|
|
99
|
+
"border-secondary/50 shadow-sm ring-1 ring-secondary/20 bg-gradient-to-br from-base-100 to-secondary/[0.03]",
|
|
100
|
+
},
|
|
101
|
+
success: {
|
|
102
|
+
icon: "bg-success/10 text-success",
|
|
103
|
+
activeBorder:
|
|
104
|
+
"border-success/50 shadow-sm ring-1 ring-success/20 bg-gradient-to-br from-base-100 to-success/[0.03]",
|
|
105
|
+
},
|
|
106
|
+
warning: {
|
|
107
|
+
icon: "bg-warning/10 text-warning",
|
|
108
|
+
activeBorder:
|
|
109
|
+
"border-warning/50 shadow-sm ring-1 ring-warning/20 bg-gradient-to-br from-base-100 to-warning/[0.03]",
|
|
110
|
+
},
|
|
111
|
+
info: {
|
|
112
|
+
icon: "bg-info/10 text-info",
|
|
113
|
+
activeBorder:
|
|
114
|
+
"border-info/50 shadow-sm ring-1 ring-info/20 bg-gradient-to-br from-base-100 to-info/[0.03]",
|
|
115
|
+
},
|
|
116
|
+
error: {
|
|
117
|
+
icon: "bg-error/10 text-error",
|
|
118
|
+
activeBorder:
|
|
119
|
+
"border-error/50 shadow-sm ring-1 ring-error/20 bg-gradient-to-br from-base-100 to-error/[0.03]",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const StatsCard: React.FC<StatsCardProps> = ({
|
|
124
|
+
title,
|
|
125
|
+
value,
|
|
126
|
+
subValue,
|
|
127
|
+
trend,
|
|
128
|
+
icon,
|
|
129
|
+
color = "primary",
|
|
130
|
+
percentage,
|
|
131
|
+
badge,
|
|
132
|
+
description,
|
|
133
|
+
active = false,
|
|
134
|
+
loading = false,
|
|
135
|
+
onClick,
|
|
136
|
+
className = "",
|
|
137
|
+
}) => {
|
|
138
|
+
const styles = COLOR_STYLES[color] || COLOR_STYLES.primary;
|
|
139
|
+
|
|
140
|
+
// Support both new `subValue` prop and deprecated `percentage` prop
|
|
141
|
+
const displaySubValue =
|
|
142
|
+
subValue !== undefined
|
|
143
|
+
? subValue
|
|
144
|
+
: percentage !== undefined && percentage !== null
|
|
145
|
+
? typeof percentage === "number"
|
|
146
|
+
? `${percentage}%`
|
|
147
|
+
: percentage
|
|
148
|
+
: null;
|
|
149
|
+
|
|
150
|
+
const renderBadge = () => {
|
|
151
|
+
if (!badge) return null;
|
|
152
|
+
if (React.isValidElement(badge)) return badge;
|
|
153
|
+
const b = badge as StatsCardBadge;
|
|
154
|
+
return (
|
|
155
|
+
<span className={`badge badge-xs font-medium ${b.variant ? `badge-${b.variant}` : "badge-ghost"}`}>
|
|
156
|
+
{b.label}
|
|
157
|
+
</span>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const renderTrend = () => {
|
|
162
|
+
if (!trend) return null;
|
|
163
|
+
if (React.isValidElement(trend)) return trend;
|
|
164
|
+
const t = trend as StatsCardTrend;
|
|
165
|
+
const isPos = t.isPositive ?? true;
|
|
166
|
+
return (
|
|
167
|
+
<span
|
|
168
|
+
className={`inline-flex items-center gap-0.5 text-xs font-semibold ${
|
|
169
|
+
isPos ? "text-success" : "text-error"
|
|
170
|
+
}`}
|
|
171
|
+
>
|
|
172
|
+
<span>{isPos ? "↑" : "↓"}</span>
|
|
173
|
+
<span>{t.value}</span>
|
|
174
|
+
{t.label && <span className="opacity-70 font-normal ml-0.5">{t.label}</span>}
|
|
175
|
+
</span>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
onClick={onClick}
|
|
182
|
+
className={`card bg-base-100 border transition-all duration-200 p-4 rounded-2xl ${
|
|
183
|
+
onClick ? "cursor-pointer hover:shadow-sm active:scale-[0.99]" : ""
|
|
184
|
+
} ${
|
|
185
|
+
active
|
|
186
|
+
? styles.activeBorder
|
|
187
|
+
: "border-base-200 hover:border-base-300"
|
|
188
|
+
} ${className}`}
|
|
189
|
+
>
|
|
190
|
+
{/* Top row: Title and Icon */}
|
|
191
|
+
<div className="flex items-center justify-between gap-2">
|
|
192
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-base-content/60 truncate">
|
|
193
|
+
{title}
|
|
194
|
+
</span>
|
|
195
|
+
{icon && (
|
|
196
|
+
<div
|
|
197
|
+
className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${styles.icon}`}
|
|
198
|
+
>
|
|
199
|
+
{icon}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Middle row: Value and optional SubValue/Trend */}
|
|
205
|
+
<div className="mt-2 flex items-baseline gap-2 flex-wrap">
|
|
206
|
+
<span className="text-2xl md:text-3xl font-bold tracking-tight text-base-content">
|
|
207
|
+
{loading ? (
|
|
208
|
+
<span className="loading loading-spinner loading-sm"></span>
|
|
209
|
+
) : (
|
|
210
|
+
value
|
|
211
|
+
)}
|
|
212
|
+
</span>
|
|
213
|
+
{displaySubValue !== null && !loading && (
|
|
214
|
+
<span className="text-xs font-semibold opacity-80">
|
|
215
|
+
{displaySubValue}
|
|
216
|
+
</span>
|
|
217
|
+
)}
|
|
218
|
+
{!loading && renderTrend()}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Bottom row: Badge and description */}
|
|
222
|
+
{(badge || description) && (
|
|
223
|
+
<div className="mt-2 flex items-center gap-1.5 text-xs text-base-content/60 min-w-0">
|
|
224
|
+
{renderBadge()}
|
|
225
|
+
{description && <span className="truncate">{description}</span>}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export default StatsCard;
|