warqadui 0.0.5 → 0.0.6
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/.vscode/settings.json +3 -0
- package/apps/dev-app/.env +1 -0
- package/apps/dev-app/errors.log +0 -0
- package/apps/dev-app/index.html +12 -0
- package/apps/dev-app/node_modules/.vite/deps/@tanstack_react-table.js +3254 -0
- package/apps/dev-app/node_modules/.vite/deps/@tanstack_react-table.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/_metadata.json +178 -0
- package/apps/dev-app/node_modules/.vite/deps/antd.js +108982 -0
- package/apps/dev-app/node_modules/.vite/deps/antd.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/axios.js +2751 -0
- package/apps/dev-app/node_modules/.vite/deps/axios.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-5OG7DCD7.js +41 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-5OG7DCD7.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-7YRZYZRE.js +7807 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-7YRZYZRE.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-DC5AMYBS.js +39 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-DKXRQMOD.js +135 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-DKXRQMOD.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-EL47BWQR.js +37 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-EL47BWQR.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-HHL3MHGV.js +288 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-HHL3MHGV.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-IGGUWUPT.js +60 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-IGGUWUPT.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-IGXZPJXT.js +928 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-IGXZPJXT.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-L2GCM37S.js +21628 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-L2GCM37S.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-LDRT62EN.js +14806 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-LDRT62EN.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-M7DZDBHW.js +14 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-M7DZDBHW.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-S54SBVCU.js +1906 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-S54SBVCU.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-WFNHCR67.js +21 -0
- package/apps/dev-app/node_modules/.vite/deps/chunk-WFNHCR67.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/clsx.js +10 -0
- package/apps/dev-app/node_modules/.vite/deps/clsx.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/dayjs.js +6 -0
- package/apps/dev-app/node_modules/.vite/deps/dayjs.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/dayjs_plugin_customParseFormat.js +6 -0
- package/apps/dev-app/node_modules/.vite/deps/dayjs_plugin_customParseFormat.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/framer-motion.js +12388 -0
- package/apps/dev-app/node_modules/.vite/deps/framer-motion.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/html2canvas-pro.js +9713 -0
- package/apps/dev-app/node_modules/.vite/deps/html2canvas-pro.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/html2canvas.esm-VL7GM4AH.js +8 -0
- package/apps/dev-app/node_modules/.vite/deps/html2canvas.esm-VL7GM4AH.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/index.es-3WTXOFZ2.js +10392 -0
- package/apps/dev-app/node_modules/.vite/deps/index.es-3WTXOFZ2.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/jspdf.js +41 -0
- package/apps/dev-app/node_modules/.vite/deps/jspdf.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/lucide-react.js +31586 -0
- package/apps/dev-app/node_modules/.vite/deps/lucide-react.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/package.json +3 -0
- package/apps/dev-app/node_modules/.vite/deps/purify.es-JNLDEIMX.js +1029 -0
- package/apps/dev-app/node_modules/.vite/deps/purify.es-JNLDEIMX.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react-dom.js +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react-dom.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react-dom_client.js +8 -0
- package/apps/dev-app/node_modules/.vite/deps/react-dom_client.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react-hook-form.js +2233 -0
- package/apps/dev-app/node_modules/.vite/deps/react-hook-form.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react-phone-number-input.js +9307 -0
- package/apps/dev-app/node_modules/.vite/deps/react-phone-number-input.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react-router-dom.js +14234 -0
- package/apps/dev-app/node_modules/.vite/deps/react-router-dom.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react-to-pdf.js +268 -0
- package/apps/dev-app/node_modules/.vite/deps/react-to-pdf.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react.js +6 -0
- package/apps/dev-app/node_modules/.vite/deps/react.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react_jsx-dev-runtime.js +913 -0
- package/apps/dev-app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react_jsx-runtime.js +7 -0
- package/apps/dev-app/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
- package/apps/dev-app/node_modules/.vite/deps/tailwind-merge.js +2534 -0
- package/apps/dev-app/node_modules/.vite/deps/tailwind-merge.js.map +7 -0
- package/apps/dev-app/node_modules/tailwindcss/LICENSE +21 -0
- package/apps/dev-app/node_modules/tailwindcss/README.md +36 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/chunk-L5IEUH3R.mjs +38 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/chunk-UWKE2Z6N.mjs +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/chunk-X4GG3EDV.mjs +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/colors-C__qRT83.d.ts +347 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/colors.d.mts +347 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/colors.d.ts +5 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/colors.js +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/colors.mjs +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.d.mts +1199 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.d.ts +1199 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.js +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.mjs +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.d.mts +6 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.d.ts +6 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.js +3 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.mjs +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/lib.d.mts +378 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/lib.d.ts +3 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/lib.js +38 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/lib.mjs +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/plugin.d.mts +11 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/plugin.d.ts +134 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/plugin.js +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/plugin.mjs +1 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/resolve-config-B4yBzhca.d.ts +29 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts +190 -0
- package/apps/dev-app/node_modules/tailwindcss/dist/types-CJYAW1ql.d.mts +128 -0
- package/apps/dev-app/node_modules/tailwindcss/index.css +944 -0
- package/apps/dev-app/node_modules/tailwindcss/package.json +89 -0
- package/apps/dev-app/node_modules/tailwindcss/preflight.css +393 -0
- package/apps/dev-app/node_modules/tailwindcss/theme.css +510 -0
- package/apps/dev-app/node_modules/tailwindcss/utilities.css +1 -0
- package/apps/dev-app/package.json +35 -0
- package/apps/dev-app/src/App.tsx +74 -0
- package/apps/dev-app/src/index.css +18 -0
- package/apps/dev-app/src/main.tsx +18 -0
- package/apps/dev-app/src/pages/Buttons.tsx +122 -0
- package/apps/dev-app/src/pages/DataTable.tsx +208 -0
- package/apps/dev-app/src/pages/Fields.tsx +342 -0
- package/apps/dev-app/src/pages/Modals.tsx +151 -0
- package/apps/dev-app/src/pages/Spins.tsx +161 -0
- package/apps/dev-app/ts_errors.txt +0 -0
- package/apps/dev-app/tsconfig.json +25 -0
- package/apps/dev-app/tsconfig.node.json +10 -0
- package/apps/dev-app/vite.config.ts +11 -0
- package/package.json +10 -49
- package/packages/ui/dist/index.d.mts +356 -0
- package/packages/ui/dist/index.d.ts +356 -0
- package/packages/ui/dist/index.js +2296 -0
- package/packages/ui/dist/index.mjs +2249 -0
- package/packages/ui/dist/styles.js +26 -0
- package/packages/ui/dist/styles.mjs +24 -0
- package/packages/ui/log.txt +0 -0
- package/packages/ui/package.json +68 -0
- package/packages/ui/postcss.config.js +6 -0
- package/packages/ui/src/components/Button.tsx +85 -0
- package/packages/ui/src/components/Card.tsx +97 -0
- package/packages/ui/src/components/CodeBlock.tsx +53 -0
- package/packages/ui/src/components/DashboardLayout.tsx +442 -0
- package/packages/ui/src/components/Fields/Input.tsx +191 -0
- package/packages/ui/src/components/Fields/PhoneInput.tsx +134 -0
- package/packages/ui/src/components/Fields/date.tsx +165 -0
- package/packages/ui/src/components/Fields/index.tsx +17 -0
- package/packages/ui/src/components/Fields/searchApi.tsx +479 -0
- package/packages/ui/src/components/Fields/select.tsx +131 -0
- package/packages/ui/src/components/Fields/textArea.tsx +121 -0
- package/packages/ui/src/components/LoadingBox.tsx +11 -0
- package/packages/ui/src/components/PageHeader.tsx +34 -0
- package/packages/ui/src/components/ThemeToggle.tsx +35 -0
- package/packages/ui/src/components/modal/Modal.tsx +81 -0
- package/packages/ui/src/components/spins/ClassicSpin.tsx +18 -0
- package/packages/ui/src/components/spins/LoadingSpin.tsx +45 -0
- package/packages/ui/src/components/spins/OverlaySpin.tsx +10 -0
- package/packages/ui/src/components/spins/index.tsx +13 -0
- package/packages/ui/src/components/tables/DataTable.tsx +261 -0
- package/packages/ui/src/components/tables/index.ts +1 -0
- package/packages/ui/src/hooks/Fetches/useApis.tsx +197 -0
- package/packages/ui/src/hooks/ThemeContext.tsx +56 -0
- package/packages/ui/src/hooks/useModal.tsx +38 -0
- package/packages/ui/src/hooks/useTheme.ts +34 -0
- package/packages/ui/src/index.ts +24 -0
- package/packages/ui/src/providers/WarqadProvider.tsx +69 -0
- package/packages/ui/src/styles.css +26 -0
- package/packages/ui/src/utils/cn.ts +6 -0
- package/packages/ui/src/utils/pdf.ts +171 -0
- package/packages/ui/tailwind.config.js +13 -0
- package/packages/ui/tsconfig.json +17 -0
- package/packages/ui/warqad-ui-0.0.1.tgz +0 -0
- package/packages/ui/warqadui-0.0.3.tgz +0 -0
- package/warqad-ui-0.0.1.tgz +0 -0
- package/dist/index.d.mts +0 -35
- package/dist/index.d.ts +0 -35
- package/dist/index.js +0 -470
- package/dist/index.mjs +0 -440
- package/dist/styles.js +0 -26
- package/dist/styles.mjs +0 -24
- /package/{dist → packages/ui/dist}/index.css +0 -0
- /package/{dist → packages/ui/dist}/styles.d.mts +0 -0
- /package/{dist → packages/ui/dist}/styles.d.ts +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
3
|
+
import { Menu, ChevronDown, ChevronRight } from "lucide-react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
|
|
7
|
+
// --- Types ---
|
|
8
|
+
export interface NavItem {
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
path?: string;
|
|
12
|
+
subItems?: (NavItem | false | null | undefined)[];
|
|
13
|
+
hidden?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type NavItems = (NavItem | false | null | undefined)[];
|
|
18
|
+
|
|
19
|
+
interface DashboardLayoutProps {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
navItems: NavItems;
|
|
22
|
+
title?: string;
|
|
23
|
+
logo?: React.ReactNode;
|
|
24
|
+
userProfile?: React.ReactNode;
|
|
25
|
+
fontFamily?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- Hooks ---
|
|
29
|
+
const useMediaQuery = (query: string) => {
|
|
30
|
+
const [matches, setMatches] = useState(() => {
|
|
31
|
+
if (typeof window !== "undefined") {
|
|
32
|
+
return window.matchMedia(query).matches;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const media = window.matchMedia(query);
|
|
39
|
+
if (media.matches !== matches) {
|
|
40
|
+
setMatches(media.matches);
|
|
41
|
+
}
|
|
42
|
+
const listener = () => setMatches(media.matches);
|
|
43
|
+
media.addEventListener("change", listener);
|
|
44
|
+
return () => media.removeEventListener("change", listener);
|
|
45
|
+
}, [matches, query]);
|
|
46
|
+
|
|
47
|
+
return matches;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isPathActive = (navPath?: string, currentPath?: string) => {
|
|
51
|
+
if (!navPath || !currentPath) return false;
|
|
52
|
+
return (
|
|
53
|
+
currentPath === navPath ||
|
|
54
|
+
(navPath !== "/" && currentPath.startsWith(`${navPath}/`))
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// --- Components ---
|
|
59
|
+
|
|
60
|
+
// 1. Sidebar Item
|
|
61
|
+
const SidebarItem = ({
|
|
62
|
+
item,
|
|
63
|
+
isActive,
|
|
64
|
+
isChildActive,
|
|
65
|
+
onClick,
|
|
66
|
+
isExpanded,
|
|
67
|
+
onToggleExpand,
|
|
68
|
+
isSidebarCollapsed,
|
|
69
|
+
}: {
|
|
70
|
+
item: NavItem;
|
|
71
|
+
isActive: boolean;
|
|
72
|
+
isChildActive?: boolean;
|
|
73
|
+
onClick: () => void;
|
|
74
|
+
isExpanded: boolean;
|
|
75
|
+
onToggleExpand: () => void;
|
|
76
|
+
isSidebarCollapsed: boolean;
|
|
77
|
+
}) => {
|
|
78
|
+
const hasSubItems = item.subItems && item.subItems.length > 0;
|
|
79
|
+
|
|
80
|
+
// Decide if the parent item itself should be highlighted
|
|
81
|
+
const shouldHighlight =
|
|
82
|
+
isActive || (hasSubItems && isExpanded) || isChildActive;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="mb-1 group relative ">
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={
|
|
89
|
+
item.disabled ? undefined : hasSubItems ? onToggleExpand : onClick
|
|
90
|
+
}
|
|
91
|
+
disabled={item.disabled}
|
|
92
|
+
title={isSidebarCollapsed ? item.label : undefined}
|
|
93
|
+
className={cn(
|
|
94
|
+
"w-full flex items-center py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
|
|
95
|
+
isSidebarCollapsed ? "justify-center px-2" : "justify-between px-3",
|
|
96
|
+
shouldHighlight && !item.disabled
|
|
97
|
+
? "bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
|
|
98
|
+
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200",
|
|
99
|
+
item.disabled && "opacity-50 cursor-not-allowed grayscale-[0.5]",
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
<div
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex items-center gap-3 overflow-hidden",
|
|
105
|
+
isSidebarCollapsed ? "justify-center w-full" : "",
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
{item.icon && (
|
|
109
|
+
<span
|
|
110
|
+
className={cn(
|
|
111
|
+
"flex-shrink-0 transition-colors [&_svg]:w-4 [&_svg]:h-4",
|
|
112
|
+
shouldHighlight && !item.disabled
|
|
113
|
+
? "text-blue-600 dark:text-blue-400"
|
|
114
|
+
: "text-gray-400 group-hover:text-blue-500 dark:text-gray-500 dark:group-hover:text-blue-400",
|
|
115
|
+
item.disabled && "group-hover:text-gray-400",
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{item.icon}
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
<AnimatePresence>
|
|
123
|
+
{!isSidebarCollapsed && (
|
|
124
|
+
<motion.span
|
|
125
|
+
initial={{ opacity: 0, width: 0 }}
|
|
126
|
+
animate={{ opacity: 1, width: "auto" }}
|
|
127
|
+
exit={{ opacity: 0, width: 0 }}
|
|
128
|
+
transition={{ duration: 0.2 }}
|
|
129
|
+
className="truncate"
|
|
130
|
+
>
|
|
131
|
+
{item.label}
|
|
132
|
+
</motion.span>
|
|
133
|
+
)}
|
|
134
|
+
</AnimatePresence>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{!isSidebarCollapsed && hasSubItems && (
|
|
138
|
+
<span
|
|
139
|
+
className={cn(
|
|
140
|
+
"flex-shrink-0 ml-2 transition-transform duration-200",
|
|
141
|
+
shouldHighlight ? "text-blue-500" : "text-gray-400",
|
|
142
|
+
)}
|
|
143
|
+
>
|
|
144
|
+
{isExpanded ? (
|
|
145
|
+
<ChevronDown size={16} />
|
|
146
|
+
) : (
|
|
147
|
+
<ChevronRight size={16} />
|
|
148
|
+
)}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</button>
|
|
152
|
+
|
|
153
|
+
{/* Submenu */}
|
|
154
|
+
<AnimatePresence initial={false}>
|
|
155
|
+
{!isSidebarCollapsed && hasSubItems && isExpanded && !item.disabled && (
|
|
156
|
+
<motion.div
|
|
157
|
+
initial={{ height: 0, opacity: 0 }}
|
|
158
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
159
|
+
exit={{ height: 0, opacity: 0 }}
|
|
160
|
+
transition={{ duration: 0.2 }}
|
|
161
|
+
className="overflow-hidden"
|
|
162
|
+
>
|
|
163
|
+
<div className="relative ml-6 mt-1 space-y-1">
|
|
164
|
+
{/* Vertical Line for tree structure */}
|
|
165
|
+
<div className="absolute left-0 top-0 bottom-0 w-px bg-gray-200 dark:bg-gray-800" />
|
|
166
|
+
|
|
167
|
+
{item
|
|
168
|
+
.subItems!.filter((sub): sub is NavItem => !!sub && !sub.hidden)
|
|
169
|
+
.map((subItem, index) => (
|
|
170
|
+
<SubItem
|
|
171
|
+
key={index}
|
|
172
|
+
item={subItem}
|
|
173
|
+
parentDisabled={item.disabled}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</motion.div>
|
|
178
|
+
)}
|
|
179
|
+
</AnimatePresence>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// 2. Sub Item Helper
|
|
185
|
+
const SubItem = ({
|
|
186
|
+
item,
|
|
187
|
+
parentDisabled,
|
|
188
|
+
}: {
|
|
189
|
+
item: NavItem;
|
|
190
|
+
parentDisabled?: boolean;
|
|
191
|
+
}) => {
|
|
192
|
+
const location = useLocation();
|
|
193
|
+
const navigate = useNavigate();
|
|
194
|
+
const isActive = isPathActive(item.path, location.pathname);
|
|
195
|
+
const isDisabled = item.disabled || parentDisabled;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<button
|
|
199
|
+
onClick={() => !isDisabled && item.path && navigate(item.path)}
|
|
200
|
+
disabled={isDisabled}
|
|
201
|
+
className={cn(
|
|
202
|
+
"relative w-full flex items-center px-3 py-2 pl-6 rounded-lg text-sm transition-colors duration-200",
|
|
203
|
+
isActive && !isDisabled
|
|
204
|
+
? "bg-blue-100/50 text-blue-700 font-medium dark:bg-blue-900/30 dark:text-blue-400"
|
|
205
|
+
: "text-gray-500 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200",
|
|
206
|
+
isDisabled && "opacity-50 cursor-not-allowed grayscale-[0.5]",
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
<span className="truncate">{item.label}</span>
|
|
210
|
+
{isActive && !isDisabled && (
|
|
211
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-4 bg-blue-500 rounded-r-full" />
|
|
212
|
+
)}
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// --- Main Layout Component ---
|
|
218
|
+
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
|
|
219
|
+
children,
|
|
220
|
+
navItems,
|
|
221
|
+
title = "Dashboard",
|
|
222
|
+
logo,
|
|
223
|
+
userProfile,
|
|
224
|
+
fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"', // Humanist System Stack
|
|
225
|
+
}) => {
|
|
226
|
+
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
|
227
|
+
const location = useLocation();
|
|
228
|
+
const navigate = useNavigate();
|
|
229
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
|
230
|
+
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>(
|
|
231
|
+
() => {
|
|
232
|
+
const initialState: Record<string, boolean> = {};
|
|
233
|
+
navItems.forEach((item) => {
|
|
234
|
+
if (
|
|
235
|
+
item &&
|
|
236
|
+
!item.hidden &&
|
|
237
|
+
!item.disabled &&
|
|
238
|
+
item.subItems?.some(
|
|
239
|
+
(sub) =>
|
|
240
|
+
sub && !sub.hidden && isPathActive(sub.path, location.pathname),
|
|
241
|
+
)
|
|
242
|
+
) {
|
|
243
|
+
initialState[item.label] = true;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return initialState;
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const prevPathRef = useRef(location.pathname);
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (prevPathRef.current !== location.pathname) {
|
|
254
|
+
const activeItem = navItems.find(
|
|
255
|
+
(item): item is NavItem =>
|
|
256
|
+
!!item &&
|
|
257
|
+
!item.hidden &&
|
|
258
|
+
!item.disabled &&
|
|
259
|
+
item.subItems?.some(
|
|
260
|
+
(sub) =>
|
|
261
|
+
sub && !sub.hidden && isPathActive(sub.path, location.pathname),
|
|
262
|
+
) === true,
|
|
263
|
+
);
|
|
264
|
+
if (activeItem) {
|
|
265
|
+
setExpandedMenus((prev) => ({ ...prev, [activeItem.label]: true }));
|
|
266
|
+
}
|
|
267
|
+
prevPathRef.current = location.pathname;
|
|
268
|
+
}
|
|
269
|
+
}, [location.pathname, navItems]);
|
|
270
|
+
|
|
271
|
+
const isCollapsed = isDesktop && !isSidebarOpen;
|
|
272
|
+
|
|
273
|
+
// Auto-close sidebar on mobile route change
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!isDesktop) {
|
|
276
|
+
setIsSidebarOpen(false);
|
|
277
|
+
} else {
|
|
278
|
+
setIsSidebarOpen(true);
|
|
279
|
+
}
|
|
280
|
+
}, [isDesktop]);
|
|
281
|
+
|
|
282
|
+
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
|
|
283
|
+
|
|
284
|
+
// Accordion behavior: Close others when opening a new one
|
|
285
|
+
const toggleMenu = (label: string) => {
|
|
286
|
+
if (isCollapsed) {
|
|
287
|
+
setIsSidebarOpen(true);
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
setExpandedMenus((prev) => {
|
|
290
|
+
const isCurrentlyOpen = !!prev[label];
|
|
291
|
+
return isCurrentlyOpen ? {} : { [label]: true };
|
|
292
|
+
});
|
|
293
|
+
}, 50);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
setExpandedMenus((prev) => {
|
|
297
|
+
const isCurrentlyOpen = !!prev[label];
|
|
298
|
+
return isCurrentlyOpen ? {} : { [label]: true };
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const sidebarVariants = {
|
|
303
|
+
desktopOpen: {
|
|
304
|
+
width: "16rem",
|
|
305
|
+
transition: { duration: 0.3, type: "spring", bounce: 0, damping: 20 },
|
|
306
|
+
},
|
|
307
|
+
desktopClosed: {
|
|
308
|
+
width: "5rem", // Mini Sidebar Width
|
|
309
|
+
transition: { duration: 0.3, type: "spring", bounce: 0, damping: 20 },
|
|
310
|
+
},
|
|
311
|
+
mobileOpen: {
|
|
312
|
+
x: 0,
|
|
313
|
+
transition: { duration: 0.3, type: "spring", bounce: 0, damping: 20 },
|
|
314
|
+
},
|
|
315
|
+
mobileClosed: {
|
|
316
|
+
x: "-100%",
|
|
317
|
+
transition: { duration: 0.3, type: "spring", bounce: 0, damping: 20 },
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div
|
|
323
|
+
className="min-h-screen bg-gray-50 dark:bg-[#0B0F1A] text-slate-900 dark:text-slate-100 flex transition-colors duration-500 font-sans overflow-hidden"
|
|
324
|
+
style={{ fontFamily }}
|
|
325
|
+
>
|
|
326
|
+
{/* Mobile Overlay */}
|
|
327
|
+
<AnimatePresence>
|
|
328
|
+
{!isDesktop && isSidebarOpen && (
|
|
329
|
+
<motion.div
|
|
330
|
+
initial={{ opacity: 0 }}
|
|
331
|
+
animate={{ opacity: 0.5 }}
|
|
332
|
+
exit={{ opacity: 0 }}
|
|
333
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
334
|
+
className="fixed inset-0 bg-black/50 z-40 lg:hidden backdrop-blur-sm"
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
</AnimatePresence>
|
|
338
|
+
{/* Sidebar */}
|
|
339
|
+
<motion.aside
|
|
340
|
+
className="fixed lg:relative inset-y-0 left-0 z-50 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col h-screen"
|
|
341
|
+
initial={isDesktop ? "desktopOpen" : "mobileClosed"}
|
|
342
|
+
animate={
|
|
343
|
+
isDesktop
|
|
344
|
+
? isSidebarOpen
|
|
345
|
+
? "desktopOpen"
|
|
346
|
+
: "desktopClosed"
|
|
347
|
+
: isSidebarOpen
|
|
348
|
+
? "mobileOpen"
|
|
349
|
+
: "mobileClosed"
|
|
350
|
+
}
|
|
351
|
+
variants={sidebarVariants as any}
|
|
352
|
+
>
|
|
353
|
+
<div
|
|
354
|
+
className={cn(
|
|
355
|
+
"h-16 flex items-center border-b border-gray-100 dark:border-gray-800 flex-shrink-0 overflow-hidden transition-all duration-300",
|
|
356
|
+
isCollapsed ? "justify-center px-0" : "px-6",
|
|
357
|
+
)}
|
|
358
|
+
>
|
|
359
|
+
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400">
|
|
360
|
+
{isCollapsed ? (
|
|
361
|
+
logo
|
|
362
|
+
) : (
|
|
363
|
+
<>
|
|
364
|
+
{logo}
|
|
365
|
+
<span className="font-bold text-lg tracking-tight text-gray-900 dark:text-white">
|
|
366
|
+
{title}
|
|
367
|
+
</span>
|
|
368
|
+
</>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div className="flex-1 p-4 overflow-y-auto overflow-x-hidden scrollbar-hide">
|
|
374
|
+
<nav className="space-y-1">
|
|
375
|
+
{navItems
|
|
376
|
+
.filter((item): item is NavItem => !!item && !item.hidden)
|
|
377
|
+
.map((item) => {
|
|
378
|
+
const isActive = isPathActive(item.path, location.pathname);
|
|
379
|
+
|
|
380
|
+
const isChildActive = item.subItems?.some(
|
|
381
|
+
(sub) =>
|
|
382
|
+
sub &&
|
|
383
|
+
!sub.hidden &&
|
|
384
|
+
isPathActive(sub.path, location.pathname),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const isExpanded = expandedMenus[item.label];
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<SidebarItem
|
|
391
|
+
key={item.label}
|
|
392
|
+
item={item}
|
|
393
|
+
isActive={isActive}
|
|
394
|
+
isChildActive={!!isChildActive}
|
|
395
|
+
isExpanded={!!isExpanded}
|
|
396
|
+
isSidebarCollapsed={!!isCollapsed}
|
|
397
|
+
onClick={() => {
|
|
398
|
+
if (item.path) navigate(item.path);
|
|
399
|
+
if (isCollapsed && !item.path) {
|
|
400
|
+
setIsSidebarOpen(true);
|
|
401
|
+
}
|
|
402
|
+
}}
|
|
403
|
+
onToggleExpand={() => toggleMenu(item.label)}
|
|
404
|
+
/>
|
|
405
|
+
);
|
|
406
|
+
})}
|
|
407
|
+
</nav>
|
|
408
|
+
</div>
|
|
409
|
+
</motion.aside>
|
|
410
|
+
|
|
411
|
+
{/* Main Content Wrapper */}
|
|
412
|
+
<div className="flex-1 flex flex-col min-w-0 h-screen overflow-hidden">
|
|
413
|
+
{/* Header */}
|
|
414
|
+
<header className="sticky top-0 z-30 h-16 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 px-4 sm:px-6 flex items-center justify-between flex-shrink-0">
|
|
415
|
+
<div className="flex items-center gap-4">
|
|
416
|
+
<button
|
|
417
|
+
onClick={toggleSidebar}
|
|
418
|
+
className="p-2 -ml-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 transition-colors"
|
|
419
|
+
>
|
|
420
|
+
<Menu size={20} />
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{/* Right Actions */}
|
|
425
|
+
<div className="flex items-center gap-2">
|
|
426
|
+
{userProfile && (
|
|
427
|
+
<>
|
|
428
|
+
<div className="w-px h-6 mx-2 bg-gray-200 dark:bg-gray-800" />
|
|
429
|
+
{userProfile}
|
|
430
|
+
</>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
</header>
|
|
434
|
+
|
|
435
|
+
{/* Body */}
|
|
436
|
+
<main className="flex-1 p-2 md:p-4 overflow-y-auto">
|
|
437
|
+
<div className="max-w-7xl mx-auto">{children}</div>
|
|
438
|
+
</main>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useState } from "react";
|
|
2
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
3
|
+
import { useWarqadConfig } from "../../providers/WarqadProvider";
|
|
4
|
+
import {
|
|
5
|
+
Controller,
|
|
6
|
+
type FieldValues,
|
|
7
|
+
type Path,
|
|
8
|
+
type UseFormReturn,
|
|
9
|
+
} from "react-hook-form";
|
|
10
|
+
|
|
11
|
+
export interface InputProps<T extends FieldValues> extends Omit<
|
|
12
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
13
|
+
"name" | "form"
|
|
14
|
+
> {
|
|
15
|
+
label: string;
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
error?: string;
|
|
18
|
+
containerClassName?: string;
|
|
19
|
+
name?: Path<T>;
|
|
20
|
+
form?: UseFormReturn<T>;
|
|
21
|
+
type?: "text" | "number" | "email" | "password";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Input = forwardRef(
|
|
25
|
+
<T extends FieldValues>(
|
|
26
|
+
{
|
|
27
|
+
label,
|
|
28
|
+
icon,
|
|
29
|
+
error,
|
|
30
|
+
containerClassName = "",
|
|
31
|
+
name,
|
|
32
|
+
form,
|
|
33
|
+
onFocus,
|
|
34
|
+
onBlur,
|
|
35
|
+
className = "",
|
|
36
|
+
type,
|
|
37
|
+
...props
|
|
38
|
+
}: InputProps<T>,
|
|
39
|
+
ref: React.ForwardedRef<HTMLInputElement>,
|
|
40
|
+
) => {
|
|
41
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
42
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
43
|
+
const { theme } = useWarqadConfig();
|
|
44
|
+
const primaryColor = theme?.primaryColor;
|
|
45
|
+
|
|
46
|
+
let message = error;
|
|
47
|
+
if (form && name) {
|
|
48
|
+
const {
|
|
49
|
+
formState: { errors },
|
|
50
|
+
} = form;
|
|
51
|
+
const errorObj = name
|
|
52
|
+
.split(".")
|
|
53
|
+
.reduce((acc: any, current: string) => acc?.[current], errors);
|
|
54
|
+
message = (errorObj as any)?.message;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (form && name) {
|
|
59
|
+
form.clearErrors("root");
|
|
60
|
+
}
|
|
61
|
+
}, [form?.watch(name as any)]);
|
|
62
|
+
|
|
63
|
+
const renderInput = (
|
|
64
|
+
inputProps: any,
|
|
65
|
+
localType: string = type || "text",
|
|
66
|
+
) => (
|
|
67
|
+
<div className={`space-y-2 group ${containerClassName}`}>
|
|
68
|
+
<label
|
|
69
|
+
htmlFor={props.id}
|
|
70
|
+
className={`block capitalize text-xs font-medium transition-colors duration-200`}
|
|
71
|
+
style={{ color: isFocused ? primaryColor : undefined }}
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
{props.required && <span className="text-red-500 ml-1">*</span>}
|
|
75
|
+
</label>
|
|
76
|
+
<div className="relative">
|
|
77
|
+
{icon && (
|
|
78
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
79
|
+
<div
|
|
80
|
+
className={`transition-colors duration-200`}
|
|
81
|
+
style={{ color: isFocused ? primaryColor : "#9ca3af" }}
|
|
82
|
+
>
|
|
83
|
+
{icon}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
<input
|
|
88
|
+
ref={ref}
|
|
89
|
+
onFocus={(e) => {
|
|
90
|
+
setIsFocused(true);
|
|
91
|
+
onFocus?.(e);
|
|
92
|
+
}}
|
|
93
|
+
onBlur={(e) => {
|
|
94
|
+
setIsFocused(false);
|
|
95
|
+
onBlur?.(e);
|
|
96
|
+
}}
|
|
97
|
+
className={`block w-full ${
|
|
98
|
+
icon ? "pl-10" : "pl-3"
|
|
99
|
+
} pr-3 h-8 py-0 rounded-lg border-gray-200 dark:border-zinc-800 bg-gray-50 dark:bg-zinc-900/50 text-gray-900 dark:text-white placeholder-gray-400 focus:bg-white dark:focus:bg-zinc-900 outline-none transition-all duration-200 border text-sm ${className}`}
|
|
100
|
+
style={{
|
|
101
|
+
borderColor: isFocused ? primaryColor : undefined,
|
|
102
|
+
boxShadow: isFocused
|
|
103
|
+
? `${primaryColor}33 0px 0px 0px 2px`
|
|
104
|
+
: undefined,
|
|
105
|
+
}}
|
|
106
|
+
{...props}
|
|
107
|
+
{...inputProps}
|
|
108
|
+
type={
|
|
109
|
+
localType === "password"
|
|
110
|
+
? showPassword
|
|
111
|
+
? "text"
|
|
112
|
+
: "password"
|
|
113
|
+
: localType
|
|
114
|
+
}
|
|
115
|
+
/>
|
|
116
|
+
{type === "password" && (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
120
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors focus:outline-none cursor-pointer"
|
|
121
|
+
tabIndex={-1}
|
|
122
|
+
>
|
|
123
|
+
{showPassword ? (
|
|
124
|
+
<EyeOff className="h-5 w-5" />
|
|
125
|
+
) : (
|
|
126
|
+
<Eye className="h-5 w-5" />
|
|
127
|
+
)}
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
{message && (
|
|
132
|
+
<p className="text-sm text-red-600 dark:text-red-400">{message}</p>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (form && name && type === "number") {
|
|
138
|
+
return (
|
|
139
|
+
<Controller
|
|
140
|
+
control={form.control}
|
|
141
|
+
name={name}
|
|
142
|
+
render={({ field: { onChange, value, ref: fieldRef, onBlur } }) =>
|
|
143
|
+
renderInput(
|
|
144
|
+
{
|
|
145
|
+
ref: fieldRef,
|
|
146
|
+
onBlur: () => {
|
|
147
|
+
onBlur();
|
|
148
|
+
},
|
|
149
|
+
value: value
|
|
150
|
+
? String(value)
|
|
151
|
+
.split(".")
|
|
152
|
+
.map((part, index) =>
|
|
153
|
+
index === 0
|
|
154
|
+
? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
|
155
|
+
: part,
|
|
156
|
+
)
|
|
157
|
+
.join(".")
|
|
158
|
+
: "",
|
|
159
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
160
|
+
const rawValue = e.target.value.replace(/,/g, "");
|
|
161
|
+
if (!/^\d*\.?\d*$/.test(rawValue)) return;
|
|
162
|
+
|
|
163
|
+
if (rawValue === "") {
|
|
164
|
+
onChange("");
|
|
165
|
+
} else {
|
|
166
|
+
const numValue = Number(rawValue);
|
|
167
|
+
if (!isNaN(numValue) && String(numValue) === rawValue) {
|
|
168
|
+
onChange(numValue);
|
|
169
|
+
} else {
|
|
170
|
+
onChange(rawValue);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
props.onChange?.(e);
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
"text",
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return renderInput(
|
|
184
|
+
form && name
|
|
185
|
+
? form.register(name, type === "number" ? { valueAsNumber: true } : {})
|
|
186
|
+
: {},
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
) as <T extends FieldValues>(
|
|
190
|
+
props: InputProps<T> & { ref?: React.ForwardedRef<HTMLInputElement> },
|
|
191
|
+
) => React.ReactElement;
|