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.
Files changed (179) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/apps/dev-app/.env +1 -0
  3. package/apps/dev-app/errors.log +0 -0
  4. package/apps/dev-app/index.html +12 -0
  5. package/apps/dev-app/node_modules/.vite/deps/@tanstack_react-table.js +3254 -0
  6. package/apps/dev-app/node_modules/.vite/deps/@tanstack_react-table.js.map +7 -0
  7. package/apps/dev-app/node_modules/.vite/deps/_metadata.json +178 -0
  8. package/apps/dev-app/node_modules/.vite/deps/antd.js +108982 -0
  9. package/apps/dev-app/node_modules/.vite/deps/antd.js.map +7 -0
  10. package/apps/dev-app/node_modules/.vite/deps/axios.js +2751 -0
  11. package/apps/dev-app/node_modules/.vite/deps/axios.js.map +7 -0
  12. package/apps/dev-app/node_modules/.vite/deps/chunk-5OG7DCD7.js +41 -0
  13. package/apps/dev-app/node_modules/.vite/deps/chunk-5OG7DCD7.js.map +7 -0
  14. package/apps/dev-app/node_modules/.vite/deps/chunk-7YRZYZRE.js +7807 -0
  15. package/apps/dev-app/node_modules/.vite/deps/chunk-7YRZYZRE.js.map +7 -0
  16. package/apps/dev-app/node_modules/.vite/deps/chunk-DC5AMYBS.js +39 -0
  17. package/apps/dev-app/node_modules/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
  18. package/apps/dev-app/node_modules/.vite/deps/chunk-DKXRQMOD.js +135 -0
  19. package/apps/dev-app/node_modules/.vite/deps/chunk-DKXRQMOD.js.map +7 -0
  20. package/apps/dev-app/node_modules/.vite/deps/chunk-EL47BWQR.js +37 -0
  21. package/apps/dev-app/node_modules/.vite/deps/chunk-EL47BWQR.js.map +7 -0
  22. package/apps/dev-app/node_modules/.vite/deps/chunk-HHL3MHGV.js +288 -0
  23. package/apps/dev-app/node_modules/.vite/deps/chunk-HHL3MHGV.js.map +7 -0
  24. package/apps/dev-app/node_modules/.vite/deps/chunk-IGGUWUPT.js +60 -0
  25. package/apps/dev-app/node_modules/.vite/deps/chunk-IGGUWUPT.js.map +7 -0
  26. package/apps/dev-app/node_modules/.vite/deps/chunk-IGXZPJXT.js +928 -0
  27. package/apps/dev-app/node_modules/.vite/deps/chunk-IGXZPJXT.js.map +7 -0
  28. package/apps/dev-app/node_modules/.vite/deps/chunk-L2GCM37S.js +21628 -0
  29. package/apps/dev-app/node_modules/.vite/deps/chunk-L2GCM37S.js.map +7 -0
  30. package/apps/dev-app/node_modules/.vite/deps/chunk-LDRT62EN.js +14806 -0
  31. package/apps/dev-app/node_modules/.vite/deps/chunk-LDRT62EN.js.map +7 -0
  32. package/apps/dev-app/node_modules/.vite/deps/chunk-M7DZDBHW.js +14 -0
  33. package/apps/dev-app/node_modules/.vite/deps/chunk-M7DZDBHW.js.map +7 -0
  34. package/apps/dev-app/node_modules/.vite/deps/chunk-S54SBVCU.js +1906 -0
  35. package/apps/dev-app/node_modules/.vite/deps/chunk-S54SBVCU.js.map +7 -0
  36. package/apps/dev-app/node_modules/.vite/deps/chunk-WFNHCR67.js +21 -0
  37. package/apps/dev-app/node_modules/.vite/deps/chunk-WFNHCR67.js.map +7 -0
  38. package/apps/dev-app/node_modules/.vite/deps/clsx.js +10 -0
  39. package/apps/dev-app/node_modules/.vite/deps/clsx.js.map +7 -0
  40. package/apps/dev-app/node_modules/.vite/deps/dayjs.js +6 -0
  41. package/apps/dev-app/node_modules/.vite/deps/dayjs.js.map +7 -0
  42. package/apps/dev-app/node_modules/.vite/deps/dayjs_plugin_customParseFormat.js +6 -0
  43. package/apps/dev-app/node_modules/.vite/deps/dayjs_plugin_customParseFormat.js.map +7 -0
  44. package/apps/dev-app/node_modules/.vite/deps/framer-motion.js +12388 -0
  45. package/apps/dev-app/node_modules/.vite/deps/framer-motion.js.map +7 -0
  46. package/apps/dev-app/node_modules/.vite/deps/html2canvas-pro.js +9713 -0
  47. package/apps/dev-app/node_modules/.vite/deps/html2canvas-pro.js.map +7 -0
  48. package/apps/dev-app/node_modules/.vite/deps/html2canvas.esm-VL7GM4AH.js +8 -0
  49. package/apps/dev-app/node_modules/.vite/deps/html2canvas.esm-VL7GM4AH.js.map +7 -0
  50. package/apps/dev-app/node_modules/.vite/deps/index.es-3WTXOFZ2.js +10392 -0
  51. package/apps/dev-app/node_modules/.vite/deps/index.es-3WTXOFZ2.js.map +7 -0
  52. package/apps/dev-app/node_modules/.vite/deps/jspdf.js +41 -0
  53. package/apps/dev-app/node_modules/.vite/deps/jspdf.js.map +7 -0
  54. package/apps/dev-app/node_modules/.vite/deps/lucide-react.js +31586 -0
  55. package/apps/dev-app/node_modules/.vite/deps/lucide-react.js.map +7 -0
  56. package/apps/dev-app/node_modules/.vite/deps/package.json +3 -0
  57. package/apps/dev-app/node_modules/.vite/deps/purify.es-JNLDEIMX.js +1029 -0
  58. package/apps/dev-app/node_modules/.vite/deps/purify.es-JNLDEIMX.js.map +7 -0
  59. package/apps/dev-app/node_modules/.vite/deps/react-dom.js +7 -0
  60. package/apps/dev-app/node_modules/.vite/deps/react-dom.js.map +7 -0
  61. package/apps/dev-app/node_modules/.vite/deps/react-dom_client.js +8 -0
  62. package/apps/dev-app/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  63. package/apps/dev-app/node_modules/.vite/deps/react-hook-form.js +2233 -0
  64. package/apps/dev-app/node_modules/.vite/deps/react-hook-form.js.map +7 -0
  65. package/apps/dev-app/node_modules/.vite/deps/react-phone-number-input.js +9307 -0
  66. package/apps/dev-app/node_modules/.vite/deps/react-phone-number-input.js.map +7 -0
  67. package/apps/dev-app/node_modules/.vite/deps/react-router-dom.js +14234 -0
  68. package/apps/dev-app/node_modules/.vite/deps/react-router-dom.js.map +7 -0
  69. package/apps/dev-app/node_modules/.vite/deps/react-to-pdf.js +268 -0
  70. package/apps/dev-app/node_modules/.vite/deps/react-to-pdf.js.map +7 -0
  71. package/apps/dev-app/node_modules/.vite/deps/react.js +6 -0
  72. package/apps/dev-app/node_modules/.vite/deps/react.js.map +7 -0
  73. package/apps/dev-app/node_modules/.vite/deps/react_jsx-dev-runtime.js +913 -0
  74. package/apps/dev-app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  75. package/apps/dev-app/node_modules/.vite/deps/react_jsx-runtime.js +7 -0
  76. package/apps/dev-app/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  77. package/apps/dev-app/node_modules/.vite/deps/tailwind-merge.js +2534 -0
  78. package/apps/dev-app/node_modules/.vite/deps/tailwind-merge.js.map +7 -0
  79. package/apps/dev-app/node_modules/tailwindcss/LICENSE +21 -0
  80. package/apps/dev-app/node_modules/tailwindcss/README.md +36 -0
  81. package/apps/dev-app/node_modules/tailwindcss/dist/chunk-L5IEUH3R.mjs +38 -0
  82. package/apps/dev-app/node_modules/tailwindcss/dist/chunk-UWKE2Z6N.mjs +1 -0
  83. package/apps/dev-app/node_modules/tailwindcss/dist/chunk-X4GG3EDV.mjs +1 -0
  84. package/apps/dev-app/node_modules/tailwindcss/dist/colors-C__qRT83.d.ts +347 -0
  85. package/apps/dev-app/node_modules/tailwindcss/dist/colors.d.mts +347 -0
  86. package/apps/dev-app/node_modules/tailwindcss/dist/colors.d.ts +5 -0
  87. package/apps/dev-app/node_modules/tailwindcss/dist/colors.js +1 -0
  88. package/apps/dev-app/node_modules/tailwindcss/dist/colors.mjs +1 -0
  89. package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.d.mts +1199 -0
  90. package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.d.ts +1199 -0
  91. package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.js +1 -0
  92. package/apps/dev-app/node_modules/tailwindcss/dist/default-theme.mjs +1 -0
  93. package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.d.mts +6 -0
  94. package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.d.ts +6 -0
  95. package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.js +3 -0
  96. package/apps/dev-app/node_modules/tailwindcss/dist/flatten-color-palette.mjs +1 -0
  97. package/apps/dev-app/node_modules/tailwindcss/dist/lib.d.mts +378 -0
  98. package/apps/dev-app/node_modules/tailwindcss/dist/lib.d.ts +3 -0
  99. package/apps/dev-app/node_modules/tailwindcss/dist/lib.js +38 -0
  100. package/apps/dev-app/node_modules/tailwindcss/dist/lib.mjs +1 -0
  101. package/apps/dev-app/node_modules/tailwindcss/dist/plugin.d.mts +11 -0
  102. package/apps/dev-app/node_modules/tailwindcss/dist/plugin.d.ts +134 -0
  103. package/apps/dev-app/node_modules/tailwindcss/dist/plugin.js +1 -0
  104. package/apps/dev-app/node_modules/tailwindcss/dist/plugin.mjs +1 -0
  105. package/apps/dev-app/node_modules/tailwindcss/dist/resolve-config-B4yBzhca.d.ts +29 -0
  106. package/apps/dev-app/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts +190 -0
  107. package/apps/dev-app/node_modules/tailwindcss/dist/types-CJYAW1ql.d.mts +128 -0
  108. package/apps/dev-app/node_modules/tailwindcss/index.css +944 -0
  109. package/apps/dev-app/node_modules/tailwindcss/package.json +89 -0
  110. package/apps/dev-app/node_modules/tailwindcss/preflight.css +393 -0
  111. package/apps/dev-app/node_modules/tailwindcss/theme.css +510 -0
  112. package/apps/dev-app/node_modules/tailwindcss/utilities.css +1 -0
  113. package/apps/dev-app/package.json +35 -0
  114. package/apps/dev-app/src/App.tsx +74 -0
  115. package/apps/dev-app/src/index.css +18 -0
  116. package/apps/dev-app/src/main.tsx +18 -0
  117. package/apps/dev-app/src/pages/Buttons.tsx +122 -0
  118. package/apps/dev-app/src/pages/DataTable.tsx +208 -0
  119. package/apps/dev-app/src/pages/Fields.tsx +342 -0
  120. package/apps/dev-app/src/pages/Modals.tsx +151 -0
  121. package/apps/dev-app/src/pages/Spins.tsx +161 -0
  122. package/apps/dev-app/ts_errors.txt +0 -0
  123. package/apps/dev-app/tsconfig.json +25 -0
  124. package/apps/dev-app/tsconfig.node.json +10 -0
  125. package/apps/dev-app/vite.config.ts +11 -0
  126. package/package.json +10 -49
  127. package/packages/ui/dist/index.d.mts +356 -0
  128. package/packages/ui/dist/index.d.ts +356 -0
  129. package/packages/ui/dist/index.js +2296 -0
  130. package/packages/ui/dist/index.mjs +2249 -0
  131. package/packages/ui/dist/styles.js +26 -0
  132. package/packages/ui/dist/styles.mjs +24 -0
  133. package/packages/ui/log.txt +0 -0
  134. package/packages/ui/package.json +68 -0
  135. package/packages/ui/postcss.config.js +6 -0
  136. package/packages/ui/src/components/Button.tsx +85 -0
  137. package/packages/ui/src/components/Card.tsx +97 -0
  138. package/packages/ui/src/components/CodeBlock.tsx +53 -0
  139. package/packages/ui/src/components/DashboardLayout.tsx +442 -0
  140. package/packages/ui/src/components/Fields/Input.tsx +191 -0
  141. package/packages/ui/src/components/Fields/PhoneInput.tsx +134 -0
  142. package/packages/ui/src/components/Fields/date.tsx +165 -0
  143. package/packages/ui/src/components/Fields/index.tsx +17 -0
  144. package/packages/ui/src/components/Fields/searchApi.tsx +479 -0
  145. package/packages/ui/src/components/Fields/select.tsx +131 -0
  146. package/packages/ui/src/components/Fields/textArea.tsx +121 -0
  147. package/packages/ui/src/components/LoadingBox.tsx +11 -0
  148. package/packages/ui/src/components/PageHeader.tsx +34 -0
  149. package/packages/ui/src/components/ThemeToggle.tsx +35 -0
  150. package/packages/ui/src/components/modal/Modal.tsx +81 -0
  151. package/packages/ui/src/components/spins/ClassicSpin.tsx +18 -0
  152. package/packages/ui/src/components/spins/LoadingSpin.tsx +45 -0
  153. package/packages/ui/src/components/spins/OverlaySpin.tsx +10 -0
  154. package/packages/ui/src/components/spins/index.tsx +13 -0
  155. package/packages/ui/src/components/tables/DataTable.tsx +261 -0
  156. package/packages/ui/src/components/tables/index.ts +1 -0
  157. package/packages/ui/src/hooks/Fetches/useApis.tsx +197 -0
  158. package/packages/ui/src/hooks/ThemeContext.tsx +56 -0
  159. package/packages/ui/src/hooks/useModal.tsx +38 -0
  160. package/packages/ui/src/hooks/useTheme.ts +34 -0
  161. package/packages/ui/src/index.ts +24 -0
  162. package/packages/ui/src/providers/WarqadProvider.tsx +69 -0
  163. package/packages/ui/src/styles.css +26 -0
  164. package/packages/ui/src/utils/cn.ts +6 -0
  165. package/packages/ui/src/utils/pdf.ts +171 -0
  166. package/packages/ui/tailwind.config.js +13 -0
  167. package/packages/ui/tsconfig.json +17 -0
  168. package/packages/ui/warqad-ui-0.0.1.tgz +0 -0
  169. package/packages/ui/warqadui-0.0.3.tgz +0 -0
  170. package/warqad-ui-0.0.1.tgz +0 -0
  171. package/dist/index.d.mts +0 -35
  172. package/dist/index.d.ts +0 -35
  173. package/dist/index.js +0 -470
  174. package/dist/index.mjs +0 -440
  175. package/dist/styles.js +0 -26
  176. package/dist/styles.mjs +0 -24
  177. /package/{dist → packages/ui/dist}/index.css +0 -0
  178. /package/{dist → packages/ui/dist}/styles.d.mts +0 -0
  179. /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;