warqadui 0.0.4 → 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,479 @@
1
+ import React, { useState, useEffect, useRef, forwardRef } from "react";
2
+ import { Search, Loader2, ChevronDown, Check, X } from "lucide-react";
3
+ import useApi from "../../hooks/Fetches/useApis";
4
+ import { useWarqadConfig } from "../../providers/WarqadProvider";
5
+ import { Controller } from "react-hook-form";
6
+
7
+ export interface SearchApiProps extends Omit<
8
+ React.InputHTMLAttributes<HTMLInputElement>,
9
+ "onChange" | "value"
10
+ > {
11
+ label?: string;
12
+ apiUrl: string;
13
+ placeholder?: string;
14
+ value?: any;
15
+ onChange?: (value: any) => void;
16
+ queryKey?: string;
17
+ labelKey?: string;
18
+ valueKey?: string;
19
+ error?: string;
20
+ containerClassName?: string;
21
+ name?: any;
22
+ form?: any;
23
+ obj?: any;
24
+ filter?: any[];
25
+ v?: number;
26
+ }
27
+
28
+ interface SearchApiContentProps extends SearchApiProps {
29
+ currentValue: any;
30
+ onChangeHandler: (value: any) => void;
31
+ fieldInternalProps?: {
32
+ ref?: React.Ref<any>;
33
+ onBlur?: () => void;
34
+ };
35
+ }
36
+
37
+ const SearchApiContent = ({
38
+ label,
39
+ apiUrl,
40
+ placeholder = "Search...",
41
+ currentValue,
42
+ onChangeHandler,
43
+ queryKey = "search",
44
+ labelKey = "name",
45
+ valueKey = "_id",
46
+ error,
47
+ containerClassName = "",
48
+ className = "",
49
+ disabled,
50
+ name,
51
+ obj,
52
+ form,
53
+ filter = [],
54
+ required,
55
+ v = 2,
56
+ fieldInternalProps,
57
+ ...props
58
+ }: SearchApiContentProps) => {
59
+ const [searchTerm, setSearchTerm] = useState("");
60
+ const { theme } = useWarqadConfig();
61
+ const primaryColor = theme?.primaryColor;
62
+ const [options, setOptions] = useState<any[]>([]);
63
+ const [isOpen, setIsOpen] = useState(false);
64
+ const [isFocused, setIsFocused] = useState(false);
65
+ const [activeIndex, setActiveIndex] = useState(-1);
66
+ const [selectedObject, setSelectedObject] = useState<any>(undefined);
67
+ const [hasAttemptedHydration, setHasAttemptedHydration] = useState(false);
68
+ const containerRef = useRef<HTMLDivElement>(null);
69
+ const inputRef = useRef<HTMLInputElement>(null);
70
+ const prevApiUrlRef = useRef(apiUrl);
71
+
72
+ const setInputRef = (element: HTMLInputElement | null) => {
73
+ (inputRef as any).current = element;
74
+ if (fieldInternalProps?.ref) {
75
+ if (typeof fieldInternalProps.ref === "function") {
76
+ fieldInternalProps.ref(element);
77
+ } else {
78
+ (fieldInternalProps.ref as any).current = element;
79
+ }
80
+ }
81
+ };
82
+
83
+ const { get, isLoading, error: errorMessage } = useApi();
84
+
85
+ const fetchOptions = async (query: string) => {
86
+ try {
87
+ const params: Record<string, any> = {};
88
+ if (query) params[queryKey] = query;
89
+
90
+ const response = await get({
91
+ url: apiUrl,
92
+ params,
93
+ v: v as any,
94
+ });
95
+
96
+ if (Array.isArray(response)) {
97
+ setOptions(response);
98
+ } else if (response?.data && Array.isArray(response.data)) {
99
+ setOptions(response.data);
100
+ } else if (response?.items && Array.isArray(response.items)) {
101
+ setOptions(response.items);
102
+ } else {
103
+ setOptions([]);
104
+ }
105
+ } catch (err: any) {
106
+ console.error("Failed to fetch options:", err);
107
+ if (form && name) {
108
+ form.setError(name, {
109
+ type: "manual",
110
+ message: err.message || "Failed to fetch options",
111
+ });
112
+ }
113
+ setOptions([]);
114
+ }
115
+ };
116
+
117
+ useEffect(() => {
118
+ const timer = setTimeout(() => {
119
+ if (isOpen) {
120
+ fetchOptions(searchTerm);
121
+ }
122
+ }, 300);
123
+ return () => clearTimeout(timer);
124
+ }, [searchTerm, apiUrl, isOpen]);
125
+
126
+ useEffect(() => {
127
+ if (isOpen && options.length === 0) {
128
+ fetchOptions("");
129
+ }
130
+ }, [isOpen]);
131
+
132
+ useEffect(() => {
133
+ if (currentValue && !selectedObject) {
134
+ setHasAttemptedHydration(false);
135
+ fetchOptions(currentValue).finally(() => {
136
+ setHasAttemptedHydration(true);
137
+ });
138
+ } else if (!currentValue) {
139
+ setHasAttemptedHydration(true);
140
+ if (selectedObject) setSelectedObject(undefined);
141
+ }
142
+ }, [currentValue, selectedObject]);
143
+
144
+ const getOptionValue = (option: any) => {
145
+ if (
146
+ valueKey === "_id" &&
147
+ option._id === undefined &&
148
+ option.id !== undefined
149
+ ) {
150
+ return option.id;
151
+ }
152
+ return option[valueKey];
153
+ };
154
+
155
+ useEffect(() => {
156
+ if (currentValue && !selectedObject && options.length > 0) {
157
+ const found = options.find((o) => getOptionValue(o) === currentValue);
158
+ if (found) {
159
+ setSelectedObject(found);
160
+ if (form && obj) {
161
+ form.setValue(obj, found);
162
+ }
163
+ }
164
+ }
165
+ }, [currentValue, options, selectedObject, form, obj]);
166
+
167
+ useEffect(() => {
168
+ if (prevApiUrlRef.current !== apiUrl) {
169
+ prevApiUrlRef.current = apiUrl;
170
+ setSearchTerm("");
171
+ setOptions([]);
172
+ setSelectedObject(undefined);
173
+
174
+ if (form) {
175
+ if (name) {
176
+ form.setValue(name, undefined, {
177
+ shouldValidate: true,
178
+ shouldDirty: true,
179
+ });
180
+ }
181
+ if (obj) {
182
+ form.setValue(obj, undefined, {
183
+ shouldValidate: true,
184
+ shouldDirty: true,
185
+ });
186
+ }
187
+ } else {
188
+ onChangeHandler(undefined);
189
+ }
190
+ fetchOptions("");
191
+ }
192
+ }, [apiUrl, form, name, obj, onChangeHandler]);
193
+
194
+ useEffect(() => {
195
+ const handleClickOutside = (event: MouseEvent) => {
196
+ if (
197
+ containerRef.current &&
198
+ !containerRef.current.contains(event.target as Node)
199
+ ) {
200
+ setIsOpen(false);
201
+ setIsFocused(false);
202
+ setSearchTerm("");
203
+ fieldInternalProps?.onBlur?.();
204
+ }
205
+ };
206
+ document.addEventListener("mousedown", handleClickOutside);
207
+ return () => document.removeEventListener("mousedown", handleClickOutside);
208
+ }, [fieldInternalProps]);
209
+
210
+ const getDisplayValue = () => {
211
+ if (selectedObject) return selectedObject[labelKey];
212
+ if (options.length > 0 && currentValue) {
213
+ const found = options.find((o) => getOptionValue(o) === currentValue);
214
+ if (found) return found[labelKey];
215
+ }
216
+ if (isLoading || (currentValue && !hasAttemptedHydration))
217
+ return "Loading...";
218
+ return currentValue || "";
219
+ };
220
+
221
+ const handleSelect = (option: any) => {
222
+ const value = getOptionValue(option);
223
+ setSelectedObject(option);
224
+ onChangeHandler(value);
225
+ if (form) {
226
+ if (obj) form.setValue(obj, option);
227
+ if (name) form.clearErrors(name);
228
+ }
229
+ setIsOpen(false);
230
+ setSearchTerm("");
231
+ setActiveIndex(-1);
232
+ fieldInternalProps?.onBlur?.();
233
+ };
234
+
235
+ const handleKeyDown = (e: React.KeyboardEvent) => {
236
+ if (!isOpen) {
237
+ if (e.key === "ArrowDown" || e.key === "Enter") setIsOpen(true);
238
+ return;
239
+ }
240
+ switch (e.key) {
241
+ case "ArrowDown":
242
+ e.preventDefault();
243
+ setActiveIndex((prev) => (prev < options.length - 1 ? prev + 1 : prev));
244
+ break;
245
+ case "ArrowUp":
246
+ e.preventDefault();
247
+ setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
248
+ break;
249
+ case "Enter":
250
+ e.preventDefault();
251
+ if (activeIndex >= 0 && activeIndex < options.length)
252
+ handleSelect(options[activeIndex]);
253
+ break;
254
+ case "Escape":
255
+ setIsOpen(false);
256
+ break;
257
+ }
258
+ };
259
+
260
+ const clearSelection = (e: React.MouseEvent) => {
261
+ e.stopPropagation();
262
+ if (form) {
263
+ if (name)
264
+ form.setValue(name, undefined, {
265
+ shouldValidate: true,
266
+ shouldDirty: true,
267
+ });
268
+ if (obj)
269
+ form.setValue(obj, undefined, {
270
+ shouldValidate: true,
271
+ shouldDirty: true,
272
+ });
273
+ } else {
274
+ onChangeHandler(undefined);
275
+ }
276
+ setSearchTerm("");
277
+ };
278
+
279
+ const filteredOptions = options.filter(
280
+ (option) => !filter.includes(getOptionValue(option)),
281
+ );
282
+
283
+ useEffect(() => {
284
+ if (form && name && !form.watch(name)) {
285
+ form.setValue(name, undefined);
286
+ if (obj) form.setValue(obj, undefined);
287
+ }
288
+ }, [form, name, obj]);
289
+
290
+ let message = error;
291
+ if (form && name) {
292
+ const {
293
+ formState: { errors },
294
+ } = form;
295
+ const nameParts = name.split(".");
296
+ let currentError = errors;
297
+ for (const part of nameParts) {
298
+ currentError = currentError?.[part];
299
+ }
300
+ message = currentError?.message || message;
301
+ }
302
+
303
+ return (
304
+ <div ref={containerRef} className={`space-y-2 group ${containerClassName}`}>
305
+ {label && (
306
+ <label
307
+ className={`block text-xs font-medium transition-colors duration-200`}
308
+ style={{ color: isFocused || isOpen ? primaryColor : undefined }}
309
+ >
310
+ {label}
311
+ {required && <span className="text-red-500 ml-1">*</span>}
312
+ </label>
313
+ )}
314
+
315
+ <div className="relative">
316
+ <div
317
+ className={`flex items-center w-full h-8 rounded-lg border bg-gray-50 dark:bg-zinc-900/50 text-gray-900 dark:text-white transition-all duration-200 cursor-text ${
318
+ isFocused || isOpen
319
+ ? "ring-2 bg-white dark:bg-zinc-900"
320
+ : "border-gray-200 dark:border-zinc-800"
321
+ } ${className}`}
322
+ style={{
323
+ borderColor: isFocused || isOpen ? primaryColor : undefined,
324
+ boxShadow:
325
+ isFocused || isOpen
326
+ ? `${primaryColor}33 0px 0px 0px 2px`
327
+ : undefined,
328
+ }}
329
+ onClick={() => {
330
+ if (disabled) return;
331
+ inputRef.current?.focus();
332
+ setIsOpen(true);
333
+ }}
334
+ >
335
+ <div className="pl-3 text-gray-400">
336
+ <Search className="h-4 w-4" />
337
+ </div>
338
+ <div className="flex-1 relative">
339
+ {!isOpen && currentValue && (
340
+ <div className="absolute inset-0 flex items-center px-3 truncate select-none">
341
+ {getDisplayValue()}
342
+ </div>
343
+ )}
344
+ <input
345
+ ref={setInputRef}
346
+ type="text"
347
+ className={`block w-full bg-transparent border-none focus:ring-0 h-8 py-0 pr-8 placeholder-gray-400 focus:outline-none text-sm ${
348
+ !isOpen && currentValue ? "opacity-0" : "opacity-100"
349
+ }`}
350
+ placeholder={!currentValue ? placeholder : ""}
351
+ value={searchTerm}
352
+ onChange={(e) => setSearchTerm(e.target.value)}
353
+ onFocus={() => {
354
+ setIsFocused(true);
355
+ setIsOpen(true);
356
+ }}
357
+ onKeyDown={handleKeyDown}
358
+ disabled={disabled}
359
+ {...props}
360
+ />
361
+ </div>
362
+ <div className="pr-3 flex items-center gap-1">
363
+ {isLoading ? (
364
+ <Loader2 className="h-4 w-4 animate-spin text-blue-500" />
365
+ ) : (
366
+ <>
367
+ {currentValue && !disabled && (
368
+ <button
369
+ onClick={clearSelection}
370
+ type="button"
371
+ className="p-0.5 hover:bg-gray-200 dark:hover:bg-zinc-700 rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
372
+ >
373
+ <X className="h-3 w-3" />
374
+ </button>
375
+ )}
376
+ <ChevronDown
377
+ className={`h-4 w-4 text-gray-400 transition-transform ${isOpen ? "rotate-180" : ""}`}
378
+ />
379
+ </>
380
+ )}
381
+ </div>
382
+ </div>
383
+
384
+ {isOpen && (
385
+ <div className="absolute z-50 w-full mt-1 bg-white dark:bg-zinc-900 rounded-xl border border-gray-200 dark:border-zinc-800 shadow-lg max-h-60 overflow-y-auto overflow-x-hidden">
386
+ {isLoading && options.length === 0 ? (
387
+ <div className="p-4 text-center text-sm text-gray-500">
388
+ <Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />
389
+ Loading options...
390
+ </div>
391
+ ) : filteredOptions.length > 0 ? (
392
+ <div className="py-1">
393
+ {filteredOptions.map((option, index) => {
394
+ const optValue = getOptionValue(option);
395
+ const isSelected = currentValue === optValue;
396
+ const isHighlighted = index === activeIndex;
397
+ return (
398
+ <button
399
+ key={optValue || index}
400
+ type="button"
401
+ className={`w-full text-left px-3 py-2 text-sm flex items-center justify-between transition-colors ${
402
+ isSelected
403
+ ? "font-medium"
404
+ : isHighlighted
405
+ ? "bg-gray-100 dark:bg-zinc-800 text-gray-900 dark:text-gray-100"
406
+ : "text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-zinc-800"
407
+ }`}
408
+ style={{
409
+ backgroundColor: isSelected
410
+ ? `${primaryColor}1A`
411
+ : undefined,
412
+ color: isSelected ? primaryColor : undefined,
413
+ }}
414
+ onClick={() => handleSelect(option)}
415
+ onMouseEnter={() => setActiveIndex(index)}
416
+ >
417
+ <span className="truncate">{option[labelKey]}</span>
418
+ {isSelected && (
419
+ <Check
420
+ className="h-4 w-4"
421
+ style={{ color: primaryColor }}
422
+ />
423
+ )}
424
+ </button>
425
+ );
426
+ })}
427
+ </div>
428
+ ) : (
429
+ <div className="p-4 text-center text-sm text-gray-500">
430
+ {errorMessage ||
431
+ (searchTerm
432
+ ? `No results for "${searchTerm}"`
433
+ : "No options available")}
434
+ </div>
435
+ )}
436
+ </div>
437
+ )}
438
+ </div>
439
+ {message && (
440
+ <p className="text-sm text-red-600 dark:text-red-400">{message}</p>
441
+ )}
442
+ </div>
443
+ );
444
+ };
445
+
446
+ export const SearchApi = forwardRef<HTMLDivElement, SearchApiProps>(
447
+ (props, _) => {
448
+ const { form, name, onChange, value } = props;
449
+
450
+ if (form && name) {
451
+ return (
452
+ <Controller
453
+ name={name}
454
+ control={form.control}
455
+ render={({
456
+ field: { onChange: formOnChange, value: formValue, ref, onBlur },
457
+ }) => (
458
+ <SearchApiContent
459
+ {...props}
460
+ currentValue={formValue}
461
+ onChangeHandler={formOnChange}
462
+ fieldInternalProps={{ ref, onBlur }}
463
+ />
464
+ )}
465
+ />
466
+ );
467
+ }
468
+
469
+ return (
470
+ <SearchApiContent
471
+ {...props}
472
+ currentValue={value}
473
+ onChangeHandler={onChange || (() => {})}
474
+ />
475
+ );
476
+ },
477
+ );
478
+
479
+ SearchApi.displayName = "SearchApi";
@@ -0,0 +1,131 @@
1
+ import { forwardRef, useState } from "react";
2
+ import { ChevronDown } from "lucide-react";
3
+ import { useWarqadConfig } from "../../providers/WarqadProvider";
4
+
5
+ export interface Option {
6
+ value: string | number | boolean;
7
+ label: string;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export interface SelectProps extends Omit<
12
+ React.SelectHTMLAttributes<HTMLSelectElement>,
13
+ "onChange"
14
+ > {
15
+ label: string;
16
+ options: Option[];
17
+ icon?: React.ReactNode;
18
+ error?: string;
19
+ containerClassName?: string;
20
+ name?: any;
21
+ form?: any;
22
+ onChange?: (value: string | number | boolean) => void;
23
+ }
24
+
25
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
26
+ (
27
+ {
28
+ label,
29
+ options,
30
+ icon,
31
+ error,
32
+ containerClassName = "",
33
+ name,
34
+ form,
35
+ onChange,
36
+ onFocus,
37
+ onBlur,
38
+ className = "",
39
+ ...props
40
+ },
41
+ ref,
42
+ ) => {
43
+ const [isFocused, setIsFocused] = useState(false);
44
+ const { theme } = useWarqadConfig();
45
+ const primaryColor = theme?.primaryColor;
46
+
47
+ let message = error;
48
+ if (form) {
49
+ const {
50
+ formState: { errors },
51
+ } = form;
52
+ const nameI = name.split(".");
53
+ const names: string = nameI.length > 1 ? nameI[1] : nameI[0];
54
+ message = errors?.[names ? names : name]?.message;
55
+ }
56
+
57
+ return (
58
+ <div className={`space-y-2 group ${containerClassName}`}>
59
+ <label
60
+ htmlFor={props.id}
61
+ className={`block text-xs font-medium transition-colors duration-200`}
62
+ style={{ color: isFocused ? primaryColor : undefined }}
63
+ >
64
+ {label}
65
+ {props.required && <span className="text-red-500 ml-1">*</span>}
66
+ </label>
67
+ <div className="relative">
68
+ {icon && (
69
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
70
+ <div
71
+ className={`transition-colors duration-200`}
72
+ style={{ color: isFocused ? primaryColor : "#9ca3af" }}
73
+ >
74
+ {icon}
75
+ </div>
76
+ </div>
77
+ )}
78
+ <select
79
+ ref={ref}
80
+ onFocus={(e) => {
81
+ setIsFocused(true);
82
+ onFocus?.(e);
83
+ }}
84
+ {...(form ? form.register(name as any) : {})}
85
+ onBlur={(e) => {
86
+ setIsFocused(false);
87
+ onBlur?.(e);
88
+ }}
89
+ onChange={(e) => {
90
+ onChange?.(e.target.value);
91
+ if (form) {
92
+ form.setValue(name, e.target.value);
93
+ }
94
+ }}
95
+ className={`block w-full ${
96
+ icon ? "pl-10" : "pl-3"
97
+ } pr-10 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 appearance-none text-sm ${className}`}
98
+ style={{
99
+ borderColor: isFocused ? primaryColor : undefined,
100
+ boxShadow: isFocused
101
+ ? `${primaryColor}33 0px 0px 0px 2px`
102
+ : undefined,
103
+ }}
104
+ {...props}
105
+ >
106
+ <option value="" disabled>
107
+ Select {label}
108
+ </option>
109
+ {options.map((option) => (
110
+ <option
111
+ key={String(option.value)}
112
+ value={String(option.value)}
113
+ disabled={option.disabled}
114
+ >
115
+ {option.label}
116
+ </option>
117
+ ))}
118
+ </select>
119
+ <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
120
+ <ChevronDown className="h-4 w-4 text-gray-400" />
121
+ </div>
122
+ </div>
123
+ {message && (
124
+ <p className="text-sm text-red-600 dark:text-red-400">{message}</p>
125
+ )}
126
+ </div>
127
+ );
128
+ },
129
+ );
130
+
131
+ Select.displayName = "Select";