@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,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;