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