warqadui 0.0.5 → 0.0.8

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