@ews-admin/global-design-system 1.0.0 → 1.1.1
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/README.md +50 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Input/Input.d.ts +4 -0
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Logo/Logo.d.ts +29 -0
- package/dist/components/Logo/Logo.d.ts.map +1 -0
- package/dist/components/Logo/index.d.ts +3 -0
- package/dist/components/Logo/index.d.ts.map +1 -0
- package/dist/components/Modal/Modal.d.ts +72 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/index.d.ts +3 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/MultiSearchAutocomplete/MultiSearchAutocomplete.d.ts +25 -0
- package/dist/components/MultiSearchAutocomplete/MultiSearchAutocomplete.d.ts.map +1 -0
- package/dist/components/MultiSearchAutocomplete/index.d.ts +2 -0
- package/dist/components/MultiSearchAutocomplete/index.d.ts.map +1 -0
- package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts +22 -0
- package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts.map +1 -0
- package/dist/components/SearchAutocomplete/index.d.ts +3 -0
- package/dist/components/SearchAutocomplete/index.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useDebounce.d.ts +15 -0
- package/dist/hooks/useDebounce.d.ts.map +1 -0
- package/dist/icons/Icon.d.ts +5 -4
- package/dist/icons/Icon.d.ts.map +1 -1
- package/dist/icons/index.d.ts +1 -3
- package/dist/icons/index.d.ts.map +1 -1
- package/dist/index.css +3 -1
- package/dist/index.d.ts +185 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +3 -1
- package/dist/index.esm.js +763 -29
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +768 -27
- package/dist/index.js.map +1 -1
- package/dist/molecules/SpecialtySearchAutocomplete/SpecialtySearchAutocomplete.d.ts +18 -0
- package/dist/molecules/SpecialtySearchAutocomplete/SpecialtySearchAutocomplete.d.ts.map +1 -0
- package/dist/molecules/SpecialtySearchAutocomplete/index.d.ts +3 -0
- package/dist/molecules/SpecialtySearchAutocomplete/index.d.ts.map +1 -0
- package/dist/molecules/index.d.ts +3 -0
- package/dist/molecules/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +5 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +17 -2
- package/src/assets/favicon.svg +6 -0
- package/src/assets/logo.svg +17 -0
- package/src/components/Button/Button.tsx +22 -8
- package/src/components/Input/Input.tsx +42 -16
- package/src/components/Logo/Logo.tsx +100 -0
- package/src/components/Logo/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +257 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/MultiSearchAutocomplete/MultiSearchAutocomplete.tsx +319 -0
- package/src/components/MultiSearchAutocomplete/index.ts +1 -0
- package/src/components/SearchAutocomplete/SearchAutocomplete.tsx +315 -0
- package/src/components/SearchAutocomplete/index.ts +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useDebounce.ts +64 -0
- package/src/icons/Icon.tsx +15 -16
- package/src/icons/index.ts +39 -3
- package/src/index.ts +19 -0
- package/src/molecules/SpecialtySearchAutocomplete/SpecialtySearchAutocomplete.tsx +203 -0
- package/src/molecules/SpecialtySearchAutocomplete/index.ts +5 -0
- package/src/molecules/index.ts +5 -0
- package/src/styles/index.css +8 -5
- package/src/styles/tailwind.css +3 -0
- package/src/utils/index.ts +7 -2
package/dist/index.esm.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { forwardRef, createElement, useState, useEffect, useRef, useCallback } from 'react';
|
|
3
3
|
|
|
4
4
|
function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=" "),n+=f);}else for(f in e)e[f]&&(n&&(n+=" "),n+=f);return n}function clsx(){for(var e,t,f=0,n="",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=" "),n+=t);return n}
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Default currency for price formatting
|
|
8
|
+
*/
|
|
9
|
+
const CURRENCY = "XOF";
|
|
6
10
|
/**
|
|
7
11
|
* Utility function to merge class names
|
|
8
12
|
* @param inputs - Class values to merge
|
|
@@ -14,10 +18,10 @@ function cn(...inputs) {
|
|
|
14
18
|
/**
|
|
15
19
|
* Utility function to format currency
|
|
16
20
|
* @param amount - Amount to format
|
|
17
|
-
* @param currency - Currency code (default:
|
|
21
|
+
* @param currency - Currency code (default: CURRENCY constant)
|
|
18
22
|
* @returns Formatted currency string
|
|
19
23
|
*/
|
|
20
|
-
function formatCurrency(amount, currency =
|
|
24
|
+
function formatCurrency(amount, currency = CURRENCY) {
|
|
21
25
|
return new Intl.NumberFormat("en-US", {
|
|
22
26
|
style: "currency",
|
|
23
27
|
currency,
|
|
@@ -63,31 +67,330 @@ function generateId(prefix = "ews") {
|
|
|
63
67
|
const Button = React.forwardRef(({ className, variant = "primary", size = "md", loading = false, fullWidth = false, leftIcon, rightIcon, children, disabled, ...props }, ref) => {
|
|
64
68
|
const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none";
|
|
65
69
|
const variants = {
|
|
66
|
-
primary: "bg-
|
|
67
|
-
secondary: "bg-
|
|
68
|
-
success: "bg-
|
|
69
|
-
warning: "bg-
|
|
70
|
-
error: "bg-
|
|
71
|
-
ghost: "bg-transparent text-
|
|
70
|
+
primary: "bg-ews-primary text-white hover:bg-ews-primary-hover focus:ring-ews-primary",
|
|
71
|
+
secondary: "bg-ews-secondary text-white hover:bg-ews-secondary-hover focus:ring-ews-secondary",
|
|
72
|
+
success: "bg-ews-success text-white hover:bg-ews-success-hover focus:ring-ews-success",
|
|
73
|
+
warning: "bg-ews-warning text-white hover:bg-ews-warning-hover focus:ring-ews-warning",
|
|
74
|
+
error: "bg-ews-error text-white hover:bg-ews-error-hover focus:ring-ews-error",
|
|
75
|
+
ghost: "bg-transparent text-ews-gray-700 hover:bg-ews-gray-100 focus:ring-ews-gray-500",
|
|
72
76
|
};
|
|
73
77
|
const sizes = {
|
|
74
78
|
sm: "px-3 py-1.5 text-sm",
|
|
75
79
|
md: "px-4 py-2 text-base",
|
|
76
80
|
lg: "px-6 py-3 text-lg",
|
|
77
81
|
};
|
|
78
|
-
|
|
82
|
+
const iconSizes = {
|
|
83
|
+
sm: "h-4 w-4",
|
|
84
|
+
md: "h-5 w-5",
|
|
85
|
+
lg: "h-6 w-6",
|
|
86
|
+
};
|
|
87
|
+
return (jsxs("button", { className: cn(baseStyles, variants[variant], sizes[size], fullWidth && "w-full", className), ref: ref, disabled: disabled || loading, ...props, children: [loading && (jsxs("svg", { className: "animate-spin -ml-1 mr-2 h-4 w-4", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] })), !loading && leftIcon && (jsx("span", { className: cn("mr-2 flex items-center", iconSizes[size]), children: leftIcon })), children, !loading && rightIcon && (jsx("span", { className: cn("ml-2 flex items-center", iconSizes[size]), children: rightIcon }))] }));
|
|
79
88
|
});
|
|
80
89
|
Button.displayName = "Button";
|
|
81
90
|
|
|
82
|
-
|
|
91
|
+
/**
|
|
92
|
+
* @license lucide-react v0.544.0 - ISC
|
|
93
|
+
*
|
|
94
|
+
* This source code is licensed under the ISC license.
|
|
95
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
99
|
+
const toCamelCase = (string) => string.replace(
|
|
100
|
+
/^([A-Z])|[\s-_]+(\w)/g,
|
|
101
|
+
(match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()
|
|
102
|
+
);
|
|
103
|
+
const toPascalCase = (string) => {
|
|
104
|
+
const camelCase = toCamelCase(string);
|
|
105
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
106
|
+
};
|
|
107
|
+
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
|
|
108
|
+
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
|
|
109
|
+
}).join(" ").trim();
|
|
110
|
+
const hasA11yProp = (props) => {
|
|
111
|
+
for (const prop in props) {
|
|
112
|
+
if (prop.startsWith("aria-") || prop === "role" || prop === "title") {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @license lucide-react v0.544.0 - ISC
|
|
120
|
+
*
|
|
121
|
+
* This source code is licensed under the ISC license.
|
|
122
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
var defaultAttributes = {
|
|
126
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
127
|
+
width: 24,
|
|
128
|
+
height: 24,
|
|
129
|
+
viewBox: "0 0 24 24",
|
|
130
|
+
fill: "none",
|
|
131
|
+
stroke: "currentColor",
|
|
132
|
+
strokeWidth: 2,
|
|
133
|
+
strokeLinecap: "round",
|
|
134
|
+
strokeLinejoin: "round"
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @license lucide-react v0.544.0 - ISC
|
|
139
|
+
*
|
|
140
|
+
* This source code is licensed under the ISC license.
|
|
141
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const Icon$1 = forwardRef(
|
|
146
|
+
({
|
|
147
|
+
color = "currentColor",
|
|
148
|
+
size = 24,
|
|
149
|
+
strokeWidth = 2,
|
|
150
|
+
absoluteStrokeWidth,
|
|
151
|
+
className = "",
|
|
152
|
+
children,
|
|
153
|
+
iconNode,
|
|
154
|
+
...rest
|
|
155
|
+
}, ref) => createElement(
|
|
156
|
+
"svg",
|
|
157
|
+
{
|
|
158
|
+
ref,
|
|
159
|
+
...defaultAttributes,
|
|
160
|
+
width: size,
|
|
161
|
+
height: size,
|
|
162
|
+
stroke: color,
|
|
163
|
+
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
|
|
164
|
+
className: mergeClasses("lucide", className),
|
|
165
|
+
...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
|
|
166
|
+
...rest
|
|
167
|
+
},
|
|
168
|
+
[
|
|
169
|
+
...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
|
|
170
|
+
...Array.isArray(children) ? children : [children]
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @license lucide-react v0.544.0 - ISC
|
|
177
|
+
*
|
|
178
|
+
* This source code is licensed under the ISC license.
|
|
179
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
const createLucideIcon = (iconName, iconNode) => {
|
|
184
|
+
const Component = forwardRef(
|
|
185
|
+
({ className, ...props }, ref) => createElement(Icon$1, {
|
|
186
|
+
ref,
|
|
187
|
+
iconNode,
|
|
188
|
+
className: mergeClasses(
|
|
189
|
+
`lucide-${toKebabCase(toPascalCase(iconName))}`,
|
|
190
|
+
`lucide-${iconName}`,
|
|
191
|
+
className
|
|
192
|
+
),
|
|
193
|
+
...props
|
|
194
|
+
})
|
|
195
|
+
);
|
|
196
|
+
Component.displayName = toPascalCase(iconName);
|
|
197
|
+
return Component;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @license lucide-react v0.544.0 - ISC
|
|
202
|
+
*
|
|
203
|
+
* This source code is licensed under the ISC license.
|
|
204
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
const __iconNode$9 = [
|
|
209
|
+
["path", { d: "M5 12h14", key: "1ays0h" }],
|
|
210
|
+
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
211
|
+
];
|
|
212
|
+
const ArrowRight = createLucideIcon("arrow-right", __iconNode$9);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @license lucide-react v0.544.0 - ISC
|
|
216
|
+
*
|
|
217
|
+
* This source code is licensed under the ISC license.
|
|
218
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
const __iconNode$8 = [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]];
|
|
223
|
+
const Check = createLucideIcon("check", __iconNode$8);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @license lucide-react v0.544.0 - ISC
|
|
227
|
+
*
|
|
228
|
+
* This source code is licensed under the ISC license.
|
|
229
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
230
|
+
*/
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
const __iconNode$7 = [
|
|
234
|
+
["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
|
|
235
|
+
["line", { x1: "12", x2: "12", y1: "8", y2: "12", key: "1pkeuh" }],
|
|
236
|
+
["line", { x1: "12", x2: "12.01", y1: "16", y2: "16", key: "4dfq90" }]
|
|
237
|
+
];
|
|
238
|
+
const CircleAlert = createLucideIcon("circle-alert", __iconNode$7);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @license lucide-react v0.544.0 - ISC
|
|
242
|
+
*
|
|
243
|
+
* This source code is licensed under the ISC license.
|
|
244
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
const __iconNode$6 = [
|
|
249
|
+
["path", { d: "M21.801 10A10 10 0 1 1 17 3.335", key: "yps3ct" }],
|
|
250
|
+
["path", { d: "m9 11 3 3L22 4", key: "1pflzl" }]
|
|
251
|
+
];
|
|
252
|
+
const CircleCheckBig = createLucideIcon("circle-check-big", __iconNode$6);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @license lucide-react v0.544.0 - ISC
|
|
256
|
+
*
|
|
257
|
+
* This source code is licensed under the ISC license.
|
|
258
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
const __iconNode$5 = [
|
|
263
|
+
[
|
|
264
|
+
"path",
|
|
265
|
+
{
|
|
266
|
+
d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",
|
|
267
|
+
key: "ct8e1f"
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
["path", { d: "M14.084 14.158a3 3 0 0 1-4.242-4.242", key: "151rxh" }],
|
|
271
|
+
[
|
|
272
|
+
"path",
|
|
273
|
+
{
|
|
274
|
+
d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143",
|
|
275
|
+
key: "13bj9a"
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
["path", { d: "m2 2 20 20", key: "1ooewy" }]
|
|
279
|
+
];
|
|
280
|
+
const EyeOff = createLucideIcon("eye-off", __iconNode$5);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @license lucide-react v0.544.0 - ISC
|
|
284
|
+
*
|
|
285
|
+
* This source code is licensed under the ISC license.
|
|
286
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
const __iconNode$4 = [
|
|
291
|
+
[
|
|
292
|
+
"path",
|
|
293
|
+
{
|
|
294
|
+
d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",
|
|
295
|
+
key: "1nclc0"
|
|
296
|
+
}
|
|
297
|
+
],
|
|
298
|
+
["circle", { cx: "12", cy: "12", r: "3", key: "1v7zrd" }]
|
|
299
|
+
];
|
|
300
|
+
const Eye = createLucideIcon("eye", __iconNode$4);
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @license lucide-react v0.544.0 - ISC
|
|
304
|
+
*
|
|
305
|
+
* This source code is licensed under the ISC license.
|
|
306
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
const __iconNode$3 = [
|
|
311
|
+
["path", { d: "m21 21-4.34-4.34", key: "14j7rj" }],
|
|
312
|
+
["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }]
|
|
313
|
+
];
|
|
314
|
+
const Search = createLucideIcon("search", __iconNode$3);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @license lucide-react v0.544.0 - ISC
|
|
318
|
+
*
|
|
319
|
+
* This source code is licensed under the ISC license.
|
|
320
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
321
|
+
*/
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
const __iconNode$2 = [
|
|
325
|
+
["path", { d: "M11 2v2", key: "1539x4" }],
|
|
326
|
+
["path", { d: "M5 2v2", key: "1yf1q8" }],
|
|
327
|
+
["path", { d: "M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1", key: "rb5t3r" }],
|
|
328
|
+
["path", { d: "M8 15a6 6 0 0 0 12 0v-3", key: "x18d4x" }],
|
|
329
|
+
["circle", { cx: "20", cy: "10", r: "2", key: "ts1r5v" }]
|
|
330
|
+
];
|
|
331
|
+
const Stethoscope = createLucideIcon("stethoscope", __iconNode$2);
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @license lucide-react v0.544.0 - ISC
|
|
335
|
+
*
|
|
336
|
+
* This source code is licensed under the ISC license.
|
|
337
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
338
|
+
*/
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
const __iconNode$1 = [
|
|
342
|
+
[
|
|
343
|
+
"path",
|
|
344
|
+
{
|
|
345
|
+
d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",
|
|
346
|
+
key: "wmoenq"
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
["path", { d: "M12 9v4", key: "juzpu7" }],
|
|
350
|
+
["path", { d: "M12 17h.01", key: "p32p05" }]
|
|
351
|
+
];
|
|
352
|
+
const TriangleAlert = createLucideIcon("triangle-alert", __iconNode$1);
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @license lucide-react v0.544.0 - ISC
|
|
356
|
+
*
|
|
357
|
+
* This source code is licensed under the ISC license.
|
|
358
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
const __iconNode = [
|
|
363
|
+
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
364
|
+
["path", { d: "m6 6 12 12", key: "d8bk6v" }]
|
|
365
|
+
];
|
|
366
|
+
const X = createLucideIcon("x", __iconNode);
|
|
367
|
+
|
|
368
|
+
const Icon = React.forwardRef(({ size = "md", icon: IconComponent, className, ...props }, ref) => {
|
|
369
|
+
const sizes = {
|
|
370
|
+
sm: 16,
|
|
371
|
+
md: 20,
|
|
372
|
+
lg: 24,
|
|
373
|
+
xl: 32,
|
|
374
|
+
};
|
|
375
|
+
const iconSize = typeof size === "number" ? size : sizes[size];
|
|
376
|
+
return (jsx(IconComponent, { ref: ref, size: iconSize, className: className, ...props }));
|
|
377
|
+
});
|
|
378
|
+
Icon.displayName = "Icon";
|
|
379
|
+
|
|
380
|
+
const Input = React.forwardRef(({ className, variant = "default", size = "md", label, helperText, error, leftIcon, rightIcon, fullWidth = false, showPasswordToggle = false, id, type = "text", ...props }, ref) => {
|
|
83
381
|
const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
|
|
84
382
|
const hasError = Boolean(error);
|
|
85
383
|
const actualVariant = hasError ? "error" : variant;
|
|
86
|
-
|
|
384
|
+
// Password visibility state
|
|
385
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
386
|
+
const isPasswordInput = type === "password";
|
|
387
|
+
const shouldShowPasswordToggle = showPasswordToggle && isPasswordInput;
|
|
388
|
+
const actualType = isPasswordInput && showPassword ? "text" : type;
|
|
389
|
+
const baseStyles = "block w-full rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-0 hover:border-ews-primary";
|
|
87
390
|
const variants = {
|
|
88
|
-
default: "border-
|
|
89
|
-
error: "border-
|
|
90
|
-
success: "border-
|
|
391
|
+
default: "border-ews-gray-300 focus:border-ews-primary focus:ring-ews-primary",
|
|
392
|
+
error: "border-ews-error focus:border-ews-error focus:ring-ews-error",
|
|
393
|
+
success: "border-ews-success focus:border-ews-success focus:ring-ews-success",
|
|
91
394
|
};
|
|
92
395
|
const sizes = {
|
|
93
396
|
sm: "px-3 py-1.5 text-sm",
|
|
@@ -99,26 +402,457 @@ const Input = React.forwardRef(({ className, variant = "default", size = "md", l
|
|
|
99
402
|
md: "h-5 w-5",
|
|
100
403
|
lg: "h-6 w-6",
|
|
101
404
|
};
|
|
102
|
-
return (jsxs("div", { className: cn("space-y-1", fullWidth ? "w-full" : "w-auto"), children: [label && (jsx("label", { htmlFor: inputId, className: "block text-sm font-medium text-
|
|
405
|
+
return (jsxs("div", { className: cn("space-y-1", fullWidth ? "w-full" : "w-auto"), children: [label && (jsx("label", { htmlFor: inputId, className: "block text-sm font-medium text-ews-gray-700", children: label })), jsxs("div", { className: "relative", children: [leftIcon && (jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", children: jsx("span", { className: cn("text-ews-gray-400", iconSizes[size]), children: leftIcon }) })), jsx("input", { id: inputId, type: actualType, className: cn(baseStyles, variants[actualVariant], sizes[size], leftIcon && "pl-10", (rightIcon || shouldShowPasswordToggle) && "pr-10", className), ref: ref, ...props }), rightIcon && !shouldShowPasswordToggle && (jsx("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none", children: jsx("span", { className: cn("text-ews-gray-400", iconSizes[size]), children: rightIcon }) })), shouldShowPasswordToggle && (jsx("button", { type: "button", className: "absolute inset-y-0 right-0 pr-3 flex items-center", onClick: () => setShowPassword(!showPassword), tabIndex: -1, children: jsx("span", { className: cn("text-ews-gray-400 hover:text-ews-gray-600 transition-colors", iconSizes[size]), children: showPassword ? jsx(EyeOff, { size: 16 }) : jsx(Eye, { size: 16 }) }) }))] }), (error || helperText) && (jsx("p", { className: cn("text-sm", error ? "text-ews-error" : "text-ews-gray-500"), children: error || helperText }))] }));
|
|
103
406
|
});
|
|
104
407
|
Input.displayName = "Input";
|
|
105
408
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
409
|
+
const DEFAULT_DELAY = 500;
|
|
410
|
+
/**
|
|
411
|
+
* A custom hook that debounces a value
|
|
412
|
+
* @param value - The value to debounce
|
|
413
|
+
* @param delay - The delay in milliseconds (optional, defaults to 300ms)
|
|
414
|
+
* @returns The debounced value
|
|
415
|
+
*/
|
|
416
|
+
function useDebounce(value, delay) {
|
|
417
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
418
|
+
const actualDelay = delay ?? DEFAULT_DELAY;
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
const handler = setTimeout(() => {
|
|
421
|
+
setDebouncedValue(value);
|
|
422
|
+
}, actualDelay);
|
|
423
|
+
return () => {
|
|
424
|
+
clearTimeout(handler);
|
|
425
|
+
};
|
|
426
|
+
}, [value, actualDelay]);
|
|
427
|
+
return debouncedValue;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* A custom hook that provides a debounced callback function
|
|
431
|
+
* @param callback - The function to debounce
|
|
432
|
+
* @param delay - The delay in milliseconds (optional, defaults to 300ms)
|
|
433
|
+
* @returns A debounced version of the callback
|
|
434
|
+
*/
|
|
435
|
+
function useDebouncedCallback(callback, delay) {
|
|
436
|
+
const [debounceTimer, setDebounceTimer] = useState(null);
|
|
437
|
+
const debouncedCallback = ((...args) => {
|
|
438
|
+
if (debounceTimer) {
|
|
439
|
+
clearTimeout(debounceTimer);
|
|
440
|
+
}
|
|
441
|
+
const actualDelay = delay ?? DEFAULT_DELAY;
|
|
442
|
+
const newTimer = setTimeout(() => {
|
|
443
|
+
callback(...args);
|
|
444
|
+
}, actualDelay);
|
|
445
|
+
setDebounceTimer(newTimer);
|
|
446
|
+
});
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
return () => {
|
|
449
|
+
if (debounceTimer) {
|
|
450
|
+
clearTimeout(debounceTimer);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}, [debounceTimer]);
|
|
454
|
+
return debouncedCallback;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function SearchAutocomplete({ onSelect, selectedId, searchFunction, getEntityById, getDisplayValue, getSecondaryText, placeholder, icon = Search, disabled = false, minSearchLength = 2, debounceTime = 300, error, }) {
|
|
458
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
459
|
+
const [entities, setEntities] = useState([]);
|
|
460
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
461
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
462
|
+
const [selectedEntity, setSelectedEntity] = useState(null);
|
|
463
|
+
const [initialLoadDone, setInitialLoadDone] = useState(false);
|
|
464
|
+
const [searchError, setSearchError] = useState(null);
|
|
465
|
+
const dropdownRef = useRef(null);
|
|
466
|
+
const debouncedSearchTerm = useDebounce(searchTerm, debounceTime);
|
|
467
|
+
// Track if this is an initial load or a user-initiated search
|
|
468
|
+
const isUserSearch = useRef(false);
|
|
469
|
+
// Only fetch the selected entity by ID once on initial load
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
const fetchSelectedEntity = async () => {
|
|
472
|
+
// Only fetch if we have a selectedId, getEntityById function, and haven't loaded yet
|
|
473
|
+
if (!selectedId || !getEntityById || initialLoadDone)
|
|
474
|
+
return;
|
|
475
|
+
setIsLoading(true);
|
|
476
|
+
try {
|
|
477
|
+
const entity = await getEntityById(selectedId);
|
|
478
|
+
if (entity) {
|
|
479
|
+
setSelectedEntity(entity);
|
|
480
|
+
setSearchTerm(getDisplayValue(entity));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.error("Error fetching selected entity:", error);
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
setIsLoading(false);
|
|
488
|
+
setInitialLoadDone(true);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
fetchSelectedEntity();
|
|
492
|
+
}, [selectedId, getEntityById, getDisplayValue, initialLoadDone]);
|
|
493
|
+
// Handle search term changes - only for user-initiated searches
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
// Skip if this is just setting the initial value
|
|
496
|
+
if (!isUserSearch.current)
|
|
497
|
+
return;
|
|
498
|
+
// Clear entities if search term is too short
|
|
499
|
+
if (searchTerm.trim().length < minSearchLength) {
|
|
500
|
+
setEntities([]);
|
|
501
|
+
setSearchError(null);
|
|
502
|
+
}
|
|
503
|
+
}, [searchTerm, minSearchLength]);
|
|
504
|
+
// Fetch entities when debounced search term changes - only for user searches
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
const fetchEntities = async () => {
|
|
507
|
+
if (!debouncedSearchTerm ||
|
|
508
|
+
debouncedSearchTerm.length < minSearchLength) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
setIsLoading(true);
|
|
512
|
+
setSearchError(null);
|
|
513
|
+
try {
|
|
514
|
+
const results = await searchFunction(debouncedSearchTerm);
|
|
515
|
+
setEntities(results);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
console.error("Error fetching search results:", error);
|
|
519
|
+
setSearchError(error instanceof Error ? error.message : "Search failed");
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
setIsLoading(false);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
// Only fetch if this is a user-initiated search
|
|
526
|
+
if (isUserSearch.current) {
|
|
527
|
+
fetchEntities();
|
|
528
|
+
}
|
|
529
|
+
}, [debouncedSearchTerm, searchFunction, minSearchLength]);
|
|
530
|
+
// Handle click outside to close dropdown
|
|
531
|
+
useEffect(() => {
|
|
532
|
+
const handleClickOutside = (event) => {
|
|
533
|
+
if (dropdownRef.current &&
|
|
534
|
+
!dropdownRef.current.contains(event.target)) {
|
|
535
|
+
setShowDropdown(false);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
539
|
+
return () => {
|
|
540
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
541
|
+
};
|
|
542
|
+
}, []);
|
|
543
|
+
const handleSearchChange = (e) => {
|
|
544
|
+
const value = e.target.value;
|
|
545
|
+
// Mark this as a user-initiated search
|
|
546
|
+
isUserSearch.current = true;
|
|
547
|
+
setSearchTerm(value);
|
|
548
|
+
if (selectedEntity) {
|
|
549
|
+
setSelectedEntity(null);
|
|
550
|
+
onSelect("");
|
|
551
|
+
}
|
|
552
|
+
setShowDropdown(value.trim().length >= minSearchLength);
|
|
112
553
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
554
|
+
const handleEntitySelect = (entity) => {
|
|
555
|
+
// This is a user selection, not an API fetch
|
|
556
|
+
isUserSearch.current = false;
|
|
557
|
+
console.log(`Selected entity with ID: ${entity.id}`);
|
|
558
|
+
setSelectedEntity(entity);
|
|
559
|
+
setSearchTerm(getDisplayValue(entity));
|
|
560
|
+
onSelect(entity.id);
|
|
561
|
+
setShowDropdown(false);
|
|
562
|
+
};
|
|
563
|
+
const handleClearSelection = () => {
|
|
564
|
+
// This is a user action, but not a search
|
|
565
|
+
isUserSearch.current = false;
|
|
566
|
+
setSelectedEntity(null);
|
|
567
|
+
setSearchTerm("");
|
|
568
|
+
onSelect("");
|
|
569
|
+
};
|
|
570
|
+
return (jsxs("div", { className: "relative", ref: dropdownRef, children: [jsxs("div", { className: "relative", children: [jsx(Input, { type: "text", placeholder: `${placeholder} (min. ${minSearchLength} characters)`, value: selectedEntity ? "" : searchTerm, onChange: handleSearchChange, onFocus: () => searchTerm.length >= minSearchLength &&
|
|
571
|
+
isUserSearch.current &&
|
|
572
|
+
setShowDropdown(true), disabled: disabled, autoComplete: "off", name: "search-autocomplete", leftIcon: React.createElement(icon, { size: 16 }) }), selectedEntity && (jsx("button", { type: "button", className: "flex absolute inset-y-0 right-0 items-center pr-3", onClick: handleClearSelection, disabled: disabled, children: jsx(X, { className: "w-5 h-5 text-gray-400 hover:text-gray-500" }) }))] }), selectedEntity && (jsx("div", { className: "p-4 mt-3 rounded-lg border bg-neutral-50 border-neutral-200", children: jsxs("div", { className: "flex justify-between items-center", children: [jsxs("div", { className: "flex items-center", children: [selectedEntity.photoUrl ? (jsx("img", { src: selectedEntity.photoUrl, alt: getDisplayValue(selectedEntity), className: "object-cover mr-3 w-10 h-10 rounded-full" })) : (React.createElement(icon, {
|
|
573
|
+
className: "w-10 h-10 text-neutral-400 mr-3",
|
|
574
|
+
})), jsxs("div", { children: [jsx("div", { className: "font-medium text-neutral-900", children: getDisplayValue(selectedEntity) }), getSecondaryText && getSecondaryText(selectedEntity) && (jsx("div", { className: "text-sm text-neutral-500", children: getSecondaryText(selectedEntity) }))] })] }), jsx("button", { type: "button", onClick: handleClearSelection, className: "transition-colors text-neutral-400 hover:text-neutral-600", disabled: disabled, children: jsx(X, { className: "w-4 h-4" }) })] }) })), showDropdown && entities?.length > 0 && (jsx("div", { className: "overflow-auto absolute z-10 mt-1 w-full max-h-60 bg-white rounded-md shadow-lg", children: jsx("ul", { className: "py-1", children: entities.map((entity) => (jsx("li", { className: "px-4 py-2 cursor-pointer hover:bg-gray-100", onClick: () => handleEntitySelect(entity), children: jsxs("div", { className: "flex items-center", children: [React.createElement(icon, {
|
|
575
|
+
className: "w-4 h-4 text-gray-400",
|
|
576
|
+
}), jsxs("div", { className: "ml-2", children: [jsx("div", { className: "font-medium", children: getDisplayValue(entity) }), getSecondaryText && getSecondaryText(entity) && (jsx("div", { className: "text-xs text-gray-500", children: getSecondaryText(entity) }))] })] }) }, entity.id))) }) })), showDropdown && searchError && (jsx("div", { className: "absolute z-10 p-4 mt-1 w-full text-center text-red-600 bg-white rounded-md shadow-lg border border-red-200", children: jsxs("div", { className: "flex items-center justify-center", children: [jsx(X, { className: "w-4 h-4 mr-2" }), "Error: ", searchError] }) })), showDropdown && error && !searchError && (jsx("div", { className: "absolute z-10 p-4 mt-1 w-full text-center text-red-600 bg-white rounded-md shadow-lg border border-red-200", children: jsxs("div", { className: "flex items-center justify-center", children: [jsx(X, { className: "w-4 h-4 mr-2" }), error] }) })), showDropdown &&
|
|
577
|
+
debouncedSearchTerm &&
|
|
578
|
+
entities?.length === 0 &&
|
|
579
|
+
!isLoading &&
|
|
580
|
+
!searchError &&
|
|
581
|
+
!error && (jsx("div", { className: "absolute z-10 p-4 mt-1 w-full text-center text-gray-500 bg-white rounded-md shadow-lg", children: debouncedSearchTerm.length < minSearchLength
|
|
582
|
+
? `Type at least ${minSearchLength} characters to search`
|
|
583
|
+
: "No results found. Try a different search term." })), isLoading && showDropdown && (jsx("div", { className: "absolute z-10 p-4 mt-1 w-full text-center text-gray-500 bg-white rounded-md shadow-lg", children: jsxs("div", { className: "flex items-center justify-center", children: [jsx("div", { className: "animate-spin rounded-full h-4 w-4 border-b-2 border-ews-primary mr-2" }), "Loading..."] }) }))] }));
|
|
584
|
+
}
|
|
116
585
|
|
|
117
|
-
|
|
586
|
+
function MultiSearchAutocomplete({ items, selectedItems, onSelectionChange, onSearch, getEntityById, getPrimaryText, getSecondaryText, placeholder, disabled = false, loading = false, multiple = true, className, renderSelectedItem, renderListItem, keepOpenOnSelect = true, error, minSearchLength = 2, debounceTime = 300, }) {
|
|
587
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
588
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
589
|
+
const [filteredItems, setFilteredItems] = useState(items);
|
|
590
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
591
|
+
const [searchError, setSearchError] = useState(null);
|
|
592
|
+
const dropdownRef = useRef(null);
|
|
593
|
+
const inputRef = useRef(null);
|
|
594
|
+
const debouncedSearchTerm = useDebounce(searchTerm, debounceTime);
|
|
595
|
+
// Handle search term changes
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
// If search term is empty, show all items
|
|
598
|
+
if (!searchTerm.trim()) {
|
|
599
|
+
setFilteredItems(items);
|
|
600
|
+
setSearchError(null);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
// If search term is too short, don't search
|
|
604
|
+
if (searchTerm.length < minSearchLength) {
|
|
605
|
+
setFilteredItems([]);
|
|
606
|
+
setSearchError(null);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
}, [searchTerm, items, minSearchLength]);
|
|
610
|
+
// Debounced search with API calls
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
// If debounced search term is empty or too short, don't search
|
|
613
|
+
if (!debouncedSearchTerm.trim() ||
|
|
614
|
+
debouncedSearchTerm.length < minSearchLength) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const performSearch = async () => {
|
|
618
|
+
try {
|
|
619
|
+
setIsSearching(true);
|
|
620
|
+
setSearchError(null);
|
|
621
|
+
await onSearch(debouncedSearchTerm);
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
setSearchError(err instanceof Error ? err.message : "Search failed");
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
setIsSearching(false);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
performSearch();
|
|
631
|
+
}, [debouncedSearchTerm, onSearch, minSearchLength]);
|
|
632
|
+
// Update filtered items when items prop changes
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
setFilteredItems(items);
|
|
635
|
+
}, [items]);
|
|
636
|
+
const handleItemToggle = useCallback((item) => {
|
|
637
|
+
if (multiple) {
|
|
638
|
+
const isSelected = selectedItems.some((selected) => selected.id === item.id);
|
|
639
|
+
if (isSelected) {
|
|
640
|
+
onSelectionChange(selectedItems.filter((selected) => selected.id !== item.id));
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
onSelectionChange([...selectedItems, item]);
|
|
644
|
+
}
|
|
645
|
+
// Keep dropdown open for multiple selection
|
|
646
|
+
if (keepOpenOnSelect) {
|
|
647
|
+
setIsOpen(true);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
onSelectionChange([item]);
|
|
652
|
+
setIsOpen(false);
|
|
653
|
+
}
|
|
654
|
+
}, [selectedItems, onSelectionChange, multiple, keepOpenOnSelect]);
|
|
655
|
+
const handleRemoveItem = useCallback((itemToRemove) => {
|
|
656
|
+
onSelectionChange(selectedItems.filter((item) => item.id !== itemToRemove.id));
|
|
657
|
+
}, [selectedItems, onSelectionChange]);
|
|
658
|
+
const handleInputFocus = () => {
|
|
659
|
+
if (!disabled) {
|
|
660
|
+
setIsOpen(true);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
const handleInputBlur = () => {
|
|
664
|
+
// Don't close immediately to allow for clicks on dropdown items
|
|
665
|
+
// Only close if keepOpenOnSelect is false
|
|
666
|
+
if (!keepOpenOnSelect) {
|
|
667
|
+
setTimeout(() => setIsOpen(false), 150);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
// Close dropdown when clicking outside
|
|
671
|
+
useEffect(() => {
|
|
672
|
+
const handleClickOutside = (event) => {
|
|
673
|
+
if (dropdownRef.current &&
|
|
674
|
+
!dropdownRef.current.contains(event.target)) {
|
|
675
|
+
setIsOpen(false);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
const handleEscapeKey = (event) => {
|
|
679
|
+
if (event.key === "Escape") {
|
|
680
|
+
setIsOpen(false);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
if (isOpen) {
|
|
684
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
685
|
+
document.addEventListener("keydown", handleEscapeKey);
|
|
686
|
+
}
|
|
687
|
+
return () => {
|
|
688
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
689
|
+
document.removeEventListener("keydown", handleEscapeKey);
|
|
690
|
+
};
|
|
691
|
+
}, [isOpen]);
|
|
692
|
+
// Default render functions
|
|
693
|
+
const defaultRenderSelectedItem = (entity) => (jsxs("span", { className: "inline-flex items-center px-3 py-1 text-sm font-medium rounded-full bg-ews-primary/10 text-ews-primary border border-ews-primary/20", children: [getPrimaryText(entity), jsx("button", { type: "button", onClick: () => handleRemoveItem(entity), className: "ml-2 text-ews-primary/60 hover:text-ews-primary", disabled: disabled, title: "Remove item", children: jsx(X, { className: "w-3 h-3" }) })] }));
|
|
694
|
+
const defaultRenderListItem = (entity, isSelected) => (jsxs("div", { className: "flex items-center space-x-3", children: [jsx("div", { className: cn("w-5 h-5 border-2 rounded flex items-center justify-center", isSelected ? "bg-ews-primary border-ews-primary" : "border-gray-300"), children: isSelected && jsx(Check, { className: "w-3 h-3 text-white" }) }), jsxs("div", { className: "flex flex-col", children: [jsx("span", { className: cn("font-medium", isSelected ? "text-ews-primary" : "text-gray-900"), children: getPrimaryText(entity) }), getSecondaryText && (jsx("span", { className: cn("text-sm", isSelected ? "text-ews-primary/70" : "text-gray-500"), children: getSecondaryText(entity) }))] })] }));
|
|
695
|
+
return (jsxs("div", { className: cn("relative", className), ref: dropdownRef, children: [selectedItems.length > 0 && (jsx("div", { className: "flex flex-wrap gap-2 mb-3", children: selectedItems.map((item) => (jsx("div", { children: renderSelectedItem
|
|
696
|
+
? renderSelectedItem(item)
|
|
697
|
+
: defaultRenderSelectedItem(item) }, item.id))) })), jsx(Input, { ref: inputRef, type: "text", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), onFocus: handleInputFocus, onBlur: handleInputBlur, placeholder: placeholder, disabled: disabled, leftIcon: jsx(Search, { className: "w-4 h-4" }), className: "w-full" }), isOpen && !disabled && (jsx("div", { className: "absolute z-10 mt-1 w-full max-h-60 bg-white rounded-lg border border-gray-200 shadow-lg overflow-auto", children: loading || isSearching ? (jsxs("div", { className: "px-4 py-3 text-sm text-gray-500 flex items-center", children: [jsx("div", { className: "animate-spin rounded-full h-4 w-4 border-b-2 border-ews-primary mr-2" }), isSearching ? "Searching..." : "Loading..."] })) : searchError ? (jsx("div", { className: "px-4 py-3 text-sm text-red-600", children: jsxs("div", { className: "flex items-center", children: [jsx(X, { className: "w-4 h-4 mr-2" }), "Error: ", searchError] }) })) : error ? (jsx("div", { className: "px-4 py-3 text-sm text-red-600", children: jsxs("div", { className: "flex items-center", children: [jsx(X, { className: "w-4 h-4 mr-2" }), error] }) })) : filteredItems.length === 0 ? (jsx("div", { className: "px-4 py-3 text-sm text-gray-500", children: searchTerm.length < minSearchLength
|
|
698
|
+
? `Type at least ${minSearchLength} characters to search`
|
|
699
|
+
: "No items found" })) : (jsx("div", { className: "py-1", children: filteredItems.map((item) => {
|
|
700
|
+
const isSelected = selectedItems.some((selected) => selected.id === item.id);
|
|
701
|
+
return (jsx("button", { type: "button", onClick: () => handleItemToggle(item), className: cn("w-full px-4 py-3 text-left flex items-center transition-colors", isSelected
|
|
702
|
+
? "bg-ews-primary/10 text-ews-primary border-l-4 border-ews-primary"
|
|
703
|
+
: "hover:bg-ews-primary/5 text-gray-900"), children: renderListItem
|
|
704
|
+
? renderListItem(item, isSelected)
|
|
705
|
+
: defaultRenderListItem(item, isSelected) }, item.id));
|
|
706
|
+
}) })) }))] }));
|
|
707
|
+
}
|
|
118
708
|
|
|
119
|
-
const
|
|
709
|
+
const Modal = ({ isOpen, onClose, title, children, variant = "info", primaryAction, secondaryAction, onPrimaryAction, onSecondaryAction, isLoading = false, closeOnOverlayClick = true, className, contentClassName, error, }) => {
|
|
710
|
+
// Handle escape key press
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
const handleEscape = (event) => {
|
|
713
|
+
if (event.key === "Escape" && isOpen) {
|
|
714
|
+
onClose();
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
if (isOpen) {
|
|
718
|
+
document.addEventListener("keydown", handleEscape);
|
|
719
|
+
// Prevent body scroll when modal is open
|
|
720
|
+
document.body.style.overflow = "hidden";
|
|
721
|
+
}
|
|
722
|
+
return () => {
|
|
723
|
+
document.removeEventListener("keydown", handleEscape);
|
|
724
|
+
document.body.style.overflow = "unset";
|
|
725
|
+
};
|
|
726
|
+
}, [isOpen, onClose]);
|
|
727
|
+
if (!isOpen)
|
|
728
|
+
return null;
|
|
729
|
+
const getVariantStyles = () => {
|
|
730
|
+
switch (variant) {
|
|
731
|
+
case "error":
|
|
732
|
+
return {
|
|
733
|
+
icon: jsx(CircleAlert, { className: "w-6 h-6 text-ews-error" }),
|
|
734
|
+
iconBg: "bg-ews-error/10",
|
|
735
|
+
titleColor: "text-ews-error",
|
|
736
|
+
borderColor: "border-ews-error/20",
|
|
737
|
+
};
|
|
738
|
+
case "warning":
|
|
739
|
+
return {
|
|
740
|
+
icon: jsx(TriangleAlert, { className: "w-6 h-6 text-ews-warning" }),
|
|
741
|
+
iconBg: "bg-ews-warning/10",
|
|
742
|
+
titleColor: "text-ews-warning",
|
|
743
|
+
borderColor: "border-ews-warning/20",
|
|
744
|
+
};
|
|
745
|
+
case "confirmation":
|
|
746
|
+
return {
|
|
747
|
+
icon: jsx(CircleCheckBig, { className: "w-6 h-6 text-ews-success" }),
|
|
748
|
+
iconBg: "bg-ews-success/10",
|
|
749
|
+
titleColor: "text-ews-success",
|
|
750
|
+
borderColor: "border-ews-success/20",
|
|
751
|
+
};
|
|
752
|
+
default:
|
|
753
|
+
return {
|
|
754
|
+
icon: jsx(CircleAlert, { className: "w-6 h-6 text-ews-primary" }),
|
|
755
|
+
iconBg: "bg-ews-primary/10",
|
|
756
|
+
titleColor: "text-ews-primary",
|
|
757
|
+
borderColor: "border-ews-primary/20",
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const variantStyles = getVariantStyles();
|
|
762
|
+
const handleOverlayClick = (e) => {
|
|
763
|
+
if (e.target === e.currentTarget && closeOnOverlayClick) {
|
|
764
|
+
onClose();
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
return (jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: handleOverlayClick }), jsxs("div", { className: cn("relative w-full max-w-md mx-4 bg-white rounded-lg shadow-xl transform transition-all", "animate-in fade-in-0 zoom-in-95 duration-200", className), role: "dialog", "aria-modal": "true", "aria-labelledby": "modal-title", children: [jsxs("div", { className: cn("flex items-center justify-between p-6 border-b", variantStyles.borderColor), children: [jsxs("div", { className: "flex items-center space-x-3", children: [jsx("div", { className: cn("p-2 rounded-full", variantStyles.iconBg), children: variantStyles.icon }), jsx("h2", { id: "modal-title", className: cn("text-lg font-semibold", variantStyles.titleColor), children: title })] }), jsx("button", { onClick: onClose, className: "p-1 text-gray-400 hover:text-gray-600 transition-colors", "aria-label": "Close modal", children: jsx(X, { className: "w-5 h-5" }) })] }), jsx("div", { className: cn("p-6", contentClassName), children: jsx("div", { className: "text-gray-700 leading-relaxed", children: error && variant === "error" ? (jsxs("div", { className: "space-y-3", children: [jsx("p", { children: error.message }), error.fields && error.fields.length > 0 && (jsxs("div", { children: [jsx("p", { className: "font-semibold text-gray-900", children: "Erreurs de champ:" }), jsx("ul", { className: "mt-2 space-y-1", children: error.fields.map((field, index) => (jsxs("li", { className: "text-ews-error", children: ["\u2022 ", field.path, ": ", field.message] }, index))) })] }))] })) : (children) }) }), (primaryAction || secondaryAction) && (jsxs("div", { className: "flex items-center justify-end space-x-3 p-6 pt-0", children: [secondaryAction && (jsx(Button, { variant: "ghost", onClick: onSecondaryAction || onClose, disabled: isLoading, children: secondaryAction })), primaryAction && (jsx(Button, { variant: variant === "error" ? "error" : "primary", onClick: onPrimaryAction, loading: isLoading, children: primaryAction }))] }))] })] }));
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const Logo = ({ size = "md", showTagline = true, iconOnly = false, variant = "normal", className, onClick, }) => {
|
|
771
|
+
const sizes = {
|
|
772
|
+
sm: "h-8",
|
|
773
|
+
md: "h-12",
|
|
774
|
+
lg: "h-16",
|
|
775
|
+
xl: "h-24",
|
|
776
|
+
};
|
|
777
|
+
const iconSizes = {
|
|
778
|
+
sm: "h-6 w-6",
|
|
779
|
+
md: "h-8 w-8",
|
|
780
|
+
lg: "h-12 w-12",
|
|
781
|
+
xl: "h-16 w-16",
|
|
782
|
+
};
|
|
783
|
+
// Get the appropriate logo image based on variant
|
|
784
|
+
// For iconOnly, always use favicon.ico
|
|
785
|
+
const logoSrc = iconOnly
|
|
786
|
+
? "/favicon.ico"
|
|
787
|
+
: variant === "white"
|
|
788
|
+
? "/image/logoWhite.png"
|
|
789
|
+
: variant === "favicon"
|
|
790
|
+
? "/favicon.ico"
|
|
791
|
+
: "/image/logo.png";
|
|
792
|
+
if (iconOnly) {
|
|
793
|
+
return (jsx("div", { className: cn("flex items-center justify-center", iconSizes[size], className), onClick: onClick, role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, children: jsx("img", { src: logoSrc, alt: "MEDECINE 360 Logo", className: "w-full h-full object-contain" }) }));
|
|
794
|
+
}
|
|
795
|
+
return (jsx("div", { className: cn("flex items-center", sizes[size], className), onClick: onClick, role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, children: jsx("img", { src: logoSrc, alt: "MEDECINE 360 Logo", className: "h-full w-auto object-contain" }) }));
|
|
796
|
+
};
|
|
120
797
|
|
|
121
|
-
const
|
|
798
|
+
const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesChange, placeholder = "Search and select medical specialties...", className = "", disabled = false, maxSelections, showSelectedCount = true, }) => {
|
|
799
|
+
const [specialties, setSpecialties] = useState([]);
|
|
800
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
801
|
+
// Mock API call - replace with actual API
|
|
802
|
+
const fetchSpecialties = useCallback(async (searchTerm) => {
|
|
803
|
+
setIsLoading(true);
|
|
804
|
+
try {
|
|
805
|
+
// Simulate API delay
|
|
806
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
807
|
+
// Mock data - replace with actual API call
|
|
808
|
+
const mockSpecialties = [
|
|
809
|
+
{ id: "1", code: "CARD", label: "Cardiology" },
|
|
810
|
+
{ id: "2", code: "DERM", label: "Dermatology" },
|
|
811
|
+
{ id: "3", code: "ENDO", label: "Endocrinology" },
|
|
812
|
+
{ id: "4", code: "GAST", label: "Gastroenterology" },
|
|
813
|
+
{ id: "5", code: "HEMA", label: "Hematology" },
|
|
814
|
+
{ id: "6", code: "NEUR", label: "Neurology" },
|
|
815
|
+
{ id: "7", code: "ONCO", label: "Oncology" },
|
|
816
|
+
{ id: "8", code: "ORTH", label: "Orthopedics" },
|
|
817
|
+
{ id: "9", code: "PED", label: "Pediatrics" },
|
|
818
|
+
{ id: "10", code: "PSYC", label: "Psychiatry" },
|
|
819
|
+
{ id: "11", code: "RAD", label: "Radiology" },
|
|
820
|
+
{ id: "12", code: "SURG", label: "Surgery" },
|
|
821
|
+
{ id: "13", code: "UROL", label: "Urology" },
|
|
822
|
+
{ id: "14", code: "GYN", label: "Gynecology" },
|
|
823
|
+
{ id: "15", code: "OPHT", label: "Ophthalmology" },
|
|
824
|
+
];
|
|
825
|
+
// Filter based on search term
|
|
826
|
+
const filtered = mockSpecialties.filter((specialty) => specialty.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
827
|
+
specialty.code.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
828
|
+
setSpecialties(filtered);
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
console.error("Error fetching specialties:", error);
|
|
832
|
+
setSpecialties([]);
|
|
833
|
+
}
|
|
834
|
+
finally {
|
|
835
|
+
setIsLoading(false);
|
|
836
|
+
}
|
|
837
|
+
}, []);
|
|
838
|
+
// Initial load
|
|
839
|
+
useEffect(() => {
|
|
840
|
+
fetchSpecialties("");
|
|
841
|
+
}, [fetchSpecialties]);
|
|
842
|
+
const handleSelectionChange = useCallback((items) => {
|
|
843
|
+
// Check max selections limit
|
|
844
|
+
if (maxSelections && items.length > maxSelections) {
|
|
845
|
+
return; // Don't update if exceeding max selections
|
|
846
|
+
}
|
|
847
|
+
onSpecialtiesChange(items);
|
|
848
|
+
}, [onSpecialtiesChange, maxSelections]);
|
|
849
|
+
const getEntityById = useCallback(async (id) => {
|
|
850
|
+
return specialties.find((specialty) => specialty.id === id);
|
|
851
|
+
}, [specialties]);
|
|
852
|
+
return (jsxs("div", { className: cn("space-y-3", className), children: [jsxs("div", { className: "flex items-center space-x-2", children: [jsx("div", { className: "flex justify-center items-center w-8 h-8 rounded-lg bg-secondary-100", children: jsx(Stethoscope, { className: "w-4 h-4 text-secondary-600" }) }), jsx("h3", { className: "text-lg font-semibold text-gray-900", children: "Medical Specialties" })] }), jsx("div", { children: jsx("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "Select Specialties" }) }), jsx(MultiSearchAutocomplete, { items: specialties, selectedItems: selectedSpecialties, onSelectionChange: handleSelectionChange, onSearch: fetchSpecialties, getEntityById: getEntityById, getPrimaryText: (specialty) => specialty.label, getSecondaryText: (specialty) => specialty.code, placeholder: placeholder, disabled: disabled, loading: isLoading, multiple: true, keepOpenOnSelect: true, className: "w-full", renderSelectedItem: (specialty) => (jsx("span", { className: "inline-flex items-center px-3 py-1 text-sm font-medium rounded-full bg-ews-primary/10 text-ews-primary border border-ews-primary/20", children: specialty.label })), renderListItem: (specialty, isSelected) => (jsxs("div", { className: "flex items-center space-x-3", children: [jsx("div", { className: cn("w-5 h-5 border-2 rounded flex items-center justify-center", isSelected
|
|
853
|
+
? "bg-ews-primary border-ews-primary"
|
|
854
|
+
: "border-gray-300"), children: isSelected && (jsx("svg", { className: "w-3 h-3 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) })) }), jsxs("div", { className: "flex flex-col", children: [jsx("span", { className: cn("font-medium", isSelected ? "text-ews-primary" : "text-gray-900"), children: specialty.label }), jsx("span", { className: cn("text-sm", isSelected ? "text-ews-primary/70" : "text-gray-500"), children: specialty.code })] })] })) }), showSelectedCount && selectedSpecialties.length > 0 && (jsxs("div", { className: "flex items-center justify-between text-sm text-gray-600", children: [jsxs("span", { children: [selectedSpecialties.length, " specialty", selectedSpecialties.length !== 1 ? "ies" : "", " selected"] }), maxSelections && (jsxs("span", { className: "text-gray-400", children: [selectedSpecialties.length, "/", maxSelections] }))] }))] }));
|
|
855
|
+
};
|
|
122
856
|
|
|
123
|
-
export { ArrowRight, Button, Check, Icon, Input, Search, cn, debounce, formatCurrency, formatDate, generateId };
|
|
857
|
+
export { ArrowRight, Button, Check, Icon, Input, Logo, Modal, MultiSearchAutocomplete, Search, SearchAutocomplete, SpecialtySearchAutocomplete, cn, debounce, formatCurrency, formatDate, generateId, useDebounce, useDebouncedCallback };
|
|
124
858
|
//# sourceMappingURL=index.esm.js.map
|