@ews-admin/global-design-system 1.1.13 → 1.1.15

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 (48) hide show
  1. package/dist/components/Button/Button.d.ts +1 -1
  2. package/dist/components/Button/Button.d.ts.map +1 -1
  3. package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts +22 -0
  4. package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts.map +1 -0
  5. package/dist/components/DropdownMultiSelect/index.d.ts +3 -0
  6. package/dist/components/DropdownMultiSelect/index.d.ts.map +1 -0
  7. package/dist/components/Logo/Logo.d.ts +3 -27
  8. package/dist/components/Logo/Logo.d.ts.map +1 -1
  9. package/dist/components/Logo/Logo.types.d.ts +41 -0
  10. package/dist/components/Logo/Logo.types.d.ts.map +1 -0
  11. package/dist/components/Logo/index.d.ts +1 -1
  12. package/dist/components/Logo/index.d.ts.map +1 -1
  13. package/dist/components/Logo/logoAssets.d.ts +1 -0
  14. package/dist/components/Logo/logoAssets.d.ts.map +1 -0
  15. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts +1 -1
  16. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts.map +1 -1
  17. package/dist/components/Select/Select.d.ts +3 -3
  18. package/dist/components/Select/Select.d.ts.map +1 -1
  19. package/dist/hooks/useSelectField.d.ts +4 -4
  20. package/dist/hooks/useSelectField.d.ts.map +1 -1
  21. package/dist/icons/Icon.d.ts +1 -1
  22. package/dist/icons/Icon.d.ts.map +1 -1
  23. package/dist/index.css +2 -2
  24. package/dist/index.d.ts +54 -18
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.esm.css +2 -2
  27. package/dist/index.esm.js +184 -22
  28. package/dist/index.esm.js.map +1 -1
  29. package/dist/index.js +183 -20
  30. package/dist/index.js.map +1 -1
  31. package/dist/styles/theme-variables.css +62 -0
  32. package/dist/utils/index.d.ts +2 -2
  33. package/dist/utils/index.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/Button/Button.tsx +4 -1
  36. package/src/components/DropdownMultiSelect/DropdownMultiSelect.tsx +271 -0
  37. package/src/components/DropdownMultiSelect/index.ts +2 -0
  38. package/src/components/Logo/Logo.tsx +65 -45
  39. package/src/components/Logo/Logo.types.ts +42 -0
  40. package/src/components/Logo/index.ts +1 -1
  41. package/src/components/SearchAutocomplete/SearchAutocomplete.tsx +1 -1
  42. package/src/components/Select/Select.tsx +21 -8
  43. package/src/hooks/useSelectField.ts +7 -2
  44. package/src/icons/Icon.tsx +1 -1
  45. package/src/index.ts +3 -0
  46. package/src/styles/index.css +0 -32
  47. package/src/utils/index.ts +5 -3
  48. package/tailwind.preset.js +23 -23
@@ -0,0 +1,62 @@
1
+ /* EWS Design System - Theme CSS Variables */
2
+ /* This file should be imported in consuming applications */
3
+
4
+ :root {
5
+ /* PROMED Theme (Default) - Professional theme for doctors/managers */
6
+ --ews-primary: #21596c;
7
+ --ews-primary-hover: #1a4756;
8
+ --ews-primary-light: #c0d0d4;
9
+ --ews-secondary: #3ba1a1;
10
+ --ews-secondary-hover: #308181;
11
+ --ews-success: #059669;
12
+ --ews-success-hover: #047857;
13
+ --ews-warning: #d97706;
14
+ --ews-warning-hover: #b45309;
15
+ --ews-error: #dc2626;
16
+ --ews-error-hover: #b91c1c;
17
+ }
18
+
19
+ /* Default theme when no data-theme is set */
20
+ html:not([data-theme]) {
21
+ --ews-primary: #21596c;
22
+ --ews-primary-hover: #1a4756;
23
+ --ews-primary-light: #c0d0d4;
24
+ --ews-secondary: #3ba1a1;
25
+ --ews-secondary-hover: #308181;
26
+ --ews-success: #059669;
27
+ --ews-success-hover: #047857;
28
+ --ews-warning: #d97706;
29
+ --ews-warning-hover: #b45309;
30
+ --ews-error: #dc2626;
31
+ --ews-error-hover: #b91c1c;
32
+ }
33
+
34
+ /* MED Theme - Patient-friendly theme */
35
+ [data-theme="MED"] {
36
+ --ews-primary: #3ba1a1;
37
+ --ews-primary-hover: #308181;
38
+ --ews-primary-light: #a8d5d5;
39
+ --ews-secondary: #6b73ff;
40
+ --ews-secondary-hover: #5a61e6;
41
+ --ews-success: #059669;
42
+ --ews-success-hover: #047857;
43
+ --ews-warning: #d97706;
44
+ --ews-warning-hover: #b45309;
45
+ --ews-error: #dc2626;
46
+ --ews-error-hover: #b91c1c;
47
+ }
48
+
49
+ /* PROMED Theme - Professional theme */
50
+ [data-theme="PROMED"] {
51
+ --ews-primary: #21596c;
52
+ --ews-primary-hover: #1a4756;
53
+ --ews-primary-light: #c0d0d4;
54
+ --ews-secondary: #3ba1a1;
55
+ --ews-secondary-hover: #308181;
56
+ --ews-success: #059669;
57
+ --ews-success-hover: #047857;
58
+ --ews-warning: #d97706;
59
+ --ews-warning-hover: #b45309;
60
+ --ews-error: #dc2626;
61
+ --ews-error-hover: #b91c1c;
62
+ }
@@ -29,7 +29,7 @@ export declare function formatDate(date: Date | string | number, options?: Intl.
29
29
  * @param wait - Wait time in milliseconds
30
30
  * @returns Debounced function
31
31
  */
32
- export declare function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void;
32
+ export declare function debounce<T extends (...args: unknown[]) => unknown>(func: T, wait: number): (...args: Parameters<T>) => void;
33
33
  /**
34
34
  * Utility function to generate unique ID
35
35
  * @param prefix - Optional prefix for the ID
@@ -44,7 +44,7 @@ export declare function generateId(prefix?: string): string;
44
44
  export declare const formatNumeric: (value: string) => string;
45
45
  /**
46
46
  * Utility function to validate phone numbers
47
- * Validates phone numbers with 1-15 digits, starting with a non-zero digit
47
+ * Validates phone numbers with 1-17 digits, optionally starting with + symbol
48
48
  * @param value - Phone number string to validate
49
49
  * @returns Boolean indicating if the phone number is valid
50
50
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACxD,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAChE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMzD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ews-admin/global-design-system",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "EWS Global Design System - Reusable components for EWS applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -12,7 +12,8 @@ export interface ButtonProps
12
12
  | "success"
13
13
  | "warning"
14
14
  | "error"
15
- | "outline";
15
+ | "outline"
16
+ | "ghost";
16
17
  /**
17
18
  * Button size
18
19
  */
@@ -63,6 +64,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
63
64
  error: "bg-ews-error text-white hover:bg-ews-error-hover",
64
65
  outline:
65
66
  "bg-transparent text-sm font-medium text-ews-primary hover:text-ews-primary/80",
67
+ ghost:
68
+ "border border-ews-primary text-ews-primary hover:bg-ews-primary hover:text-white disabled:border-gray-400 disabled:text-gray-400 focus:ring-2 focus:ring-offset-2 focus:ring-ews-primary",
66
69
  };
67
70
 
68
71
  const sizes = {
@@ -0,0 +1,271 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Control, Controller, FieldValues, Path } from "react-hook-form";
3
+ import { cn } from "../../utils";
4
+ import { Input } from "../Input";
5
+
6
+ export interface DropdownMultiSelectProps<T extends FieldValues, TValue> {
7
+ options: { value: TValue; label: string }[];
8
+ name: Path<T>;
9
+ control?: Control<T>;
10
+ placeholder?: string;
11
+ onChange?: (value: TValue[]) => void;
12
+ value?: TValue[];
13
+ defaultValue?: TValue[];
14
+ onValidate?: (value: TValue[]) => boolean;
15
+ disabled?: boolean;
16
+ error?: string;
17
+ label?: string;
18
+ className?: string;
19
+ searchPlaceholder?: string;
20
+ }
21
+
22
+ const DropdownMultiSelect = <
23
+ T extends FieldValues,
24
+ TValue extends string | number | object
25
+ >({
26
+ options,
27
+ name,
28
+ control,
29
+ placeholder = "Select options",
30
+ searchPlaceholder = "Search...",
31
+ onChange,
32
+ value: controlledValue,
33
+ defaultValue,
34
+ onValidate,
35
+ disabled = false,
36
+ error,
37
+ label,
38
+ className,
39
+ }: DropdownMultiSelectProps<T, TValue>) => {
40
+ const [isOpen, setIsOpen] = useState(false);
41
+ const [searchTerm, setSearchTerm] = useState("");
42
+ const [uncontrolledValue, setUncontrolledValue] = useState<
43
+ TValue[] | undefined
44
+ >(defaultValue);
45
+ const dropdownRef = useRef<HTMLDivElement>(null);
46
+
47
+ const handleToggle = () => {
48
+ if (!disabled) {
49
+ setIsOpen(!isOpen);
50
+ }
51
+ };
52
+
53
+ const handleClickOutside = (event: MouseEvent) => {
54
+ if (
55
+ dropdownRef.current &&
56
+ !dropdownRef.current.contains(event.target as Node)
57
+ ) {
58
+ setIsOpen(false);
59
+ setSearchTerm("");
60
+ }
61
+ };
62
+
63
+ useEffect(() => {
64
+ document.addEventListener("mousedown", handleClickOutside);
65
+ return () => {
66
+ document.removeEventListener("mousedown", handleClickOutside);
67
+ };
68
+ }, []);
69
+
70
+ const removeAccents = (str: string) =>
71
+ str
72
+ .normalize("NFD")
73
+ .replace(/[\u0300-\u036f]/g, "")
74
+ .replace(/ç/g, "c")
75
+ .replace(/é|è|ê|ë/g, "e")
76
+ .replace(/à|á|â|ã|ä/g, "a")
77
+ .replace(/î|ï/g, "i")
78
+ .replace(/ô|ö/g, "o")
79
+ .replace(/ù|ú|û|ü/g, "u");
80
+
81
+ const getDisplayValue = (value: TValue) => {
82
+ if (typeof value === "string" || typeof value === "number") {
83
+ return String(value);
84
+ }
85
+ return options.find((opt) => opt.value === value)?.label || "";
86
+ };
87
+
88
+ const filteredOptions = options.filter((option) =>
89
+ removeAccents(option.label.toLowerCase()).includes(
90
+ removeAccents(searchTerm.toLowerCase())
91
+ )
92
+ );
93
+
94
+ const renderDropdown = ({
95
+ value = [] as TValue[],
96
+ onChange: fieldOnChange,
97
+ }: {
98
+ value: TValue[] | undefined;
99
+ onChange: (value: TValue[]) => void;
100
+ }) => (
101
+ <div className={cn("relative", className)} ref={dropdownRef}>
102
+ <button
103
+ type="button"
104
+ onClick={handleToggle}
105
+ aria-label={name}
106
+ disabled={disabled}
107
+ className={cn(
108
+ "flex w-full items-center justify-between rounded-md border border-ews-gray-300 bg-white px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-0 disabled:bg-ews-gray-50 disabled:text-ews-gray-500",
109
+ isOpen ? "rounded-b-none border-b-0" : "rounded-md",
110
+ error && "border-ews-error focus:ring-ews-error"
111
+ )}
112
+ >
113
+ <span className={cn("truncate", !value?.length && "text-ews-gray-500")}>
114
+ {value?.length > 0
115
+ ? value.map((v) => getDisplayValue(v)).join(", ")
116
+ : placeholder}
117
+ </span>
118
+ <span
119
+ className={cn(
120
+ "ml-2 w-4 h-4 transition-transform transform",
121
+ isOpen ? "rotate-180" : "rotate-0"
122
+ )}
123
+ >
124
+ <svg
125
+ className="w-4 h-4"
126
+ fill="none"
127
+ stroke="currentColor"
128
+ viewBox="0 0 24 24"
129
+ >
130
+ <path
131
+ strokeLinecap="round"
132
+ strokeLinejoin="round"
133
+ strokeWidth={2}
134
+ d="M19 9l-7 7-7-7"
135
+ />
136
+ </svg>
137
+ </span>
138
+ </button>
139
+
140
+ {isOpen && (
141
+ <div className="absolute z-50 w-full bg-white rounded-b-md border border-t-0 shadow-lg border-ews-gray-300">
142
+ {/* Search Input */}
143
+ <div className="p-2 border-b border-ews-gray-200">
144
+ <Input
145
+ type="text"
146
+ placeholder={searchPlaceholder}
147
+ value={searchTerm}
148
+ onChange={(e) => setSearchTerm(e.target.value)}
149
+ className="p-0 border-0 shadow-none focus:ring-0"
150
+ size="sm"
151
+ />
152
+ </div>
153
+
154
+ {/* Scrollable Options List */}
155
+ <div className="overflow-y-auto max-h-48">
156
+ {filteredOptions.length > 0 ? (
157
+ filteredOptions.map((option) => (
158
+ <div
159
+ key={getDisplayValue(option.value)}
160
+ className="flex items-center p-2 cursor-pointer hover:bg-ews-gray-100"
161
+ onClick={() => {
162
+ const currentValue = value ?? [];
163
+ const isSelected = currentValue.some(
164
+ (item: TValue) =>
165
+ JSON.stringify(item) === JSON.stringify(option.value)
166
+ );
167
+ const newValue = isSelected
168
+ ? currentValue.filter(
169
+ (item: TValue) =>
170
+ JSON.stringify(item) !==
171
+ JSON.stringify(option.value)
172
+ )
173
+ : [...currentValue, option.value];
174
+
175
+ if (onValidate && !onValidate(newValue)) {
176
+ return;
177
+ }
178
+ fieldOnChange(newValue);
179
+ onChange?.(newValue);
180
+ }}
181
+ >
182
+ <input
183
+ type="checkbox"
184
+ checked={(value ?? []).some(
185
+ (item: TValue) =>
186
+ JSON.stringify(item) === JSON.stringify(option.value)
187
+ )}
188
+ onChange={(e) => {
189
+ e.stopPropagation();
190
+ const currentValue = value ?? [];
191
+ const isSelected = currentValue.some(
192
+ (item: TValue) =>
193
+ JSON.stringify(item) === JSON.stringify(option.value)
194
+ );
195
+ const newValue = isSelected
196
+ ? currentValue.filter(
197
+ (item: TValue) =>
198
+ JSON.stringify(item) !==
199
+ JSON.stringify(option.value)
200
+ )
201
+ : [...currentValue, option.value];
202
+
203
+ if (onValidate && !onValidate(newValue)) {
204
+ return;
205
+ }
206
+ fieldOnChange(newValue);
207
+ onChange?.(newValue);
208
+ }}
209
+ onClick={(e) => e.stopPropagation()}
210
+ className="mr-3 w-4 h-4 rounded border-ews-gray-300 text-ews-primary focus:ring-ews-primary"
211
+ />
212
+ <label className="text-sm cursor-pointer text-ews-gray-700">
213
+ {option.label}
214
+ </label>
215
+ </div>
216
+ ))
217
+ ) : (
218
+ <div className="p-2 text-sm text-ews-gray-500">
219
+ No options found
220
+ </div>
221
+ )}
222
+ </div>
223
+ </div>
224
+ )}
225
+ </div>
226
+ );
227
+
228
+ // Render controlled version with react-hook-form
229
+ if (control) {
230
+ return (
231
+ <Controller
232
+ name={name}
233
+ control={control}
234
+ render={({ field: { value, onChange } }) => (
235
+ <div>
236
+ {label && (
237
+ <label className="block mb-1 text-sm font-medium text-ews-gray-700">
238
+ {label}
239
+ </label>
240
+ )}
241
+ {renderDropdown({ value, onChange })}
242
+ {error && <p className="mt-1 text-sm text-ews-error">{error}</p>}
243
+ </div>
244
+ )}
245
+ />
246
+ );
247
+ }
248
+
249
+ // Render uncontrolled version
250
+ return (
251
+ <div>
252
+ {label && (
253
+ <label className="block mb-1 text-sm font-medium text-ews-gray-700">
254
+ {label}
255
+ </label>
256
+ )}
257
+ {renderDropdown({
258
+ value: controlledValue ?? uncontrolledValue,
259
+ onChange: (newValue) => {
260
+ if (controlledValue === undefined) {
261
+ setUncontrolledValue(newValue);
262
+ }
263
+ onChange?.(newValue);
264
+ },
265
+ })}
266
+ {error && <p className="mt-1 text-sm text-ews-error">{error}</p>}
267
+ </div>
268
+ );
269
+ };
270
+
271
+ export { DropdownMultiSelect };
@@ -0,0 +1,2 @@
1
+ export { DropdownMultiSelect } from "./DropdownMultiSelect";
2
+ export type { DropdownMultiSelectProps } from "./DropdownMultiSelect";
@@ -1,31 +1,5 @@
1
1
  import { cn } from "../../utils";
2
-
3
- export interface LogoProps {
4
- /**
5
- * Logo size
6
- */
7
- size?: "sm" | "md" | "lg" | "xl";
8
- /**
9
- * Whether to show the tagline
10
- */
11
- showTagline?: boolean;
12
- /**
13
- * Whether to show only the icon (favicon version)
14
- */
15
- iconOnly?: boolean;
16
- /**
17
- * Logo variant - normal, white, or favicon
18
- */
19
- variant?: "normal" | "white" | "fullWhite" | "favicon";
20
- /**
21
- * Custom className
22
- */
23
- className?: string;
24
- /**
25
- * Click handler
26
- */
27
- onClick?: () => void;
28
- }
2
+ import type { LogoProps } from "./Logo.types";
29
3
 
30
4
  const Logo = ({
31
5
  size = "md",
@@ -34,6 +8,9 @@ const Logo = ({
34
8
  variant = "normal",
35
9
  className,
36
10
  onClick,
11
+ customSrc,
12
+ alt = "MEDECINE 360 Logo",
13
+ clickable = false,
37
14
  }: LogoProps) => {
38
15
  const sizes = {
39
16
  sm: "h-8",
@@ -49,17 +26,21 @@ const Logo = ({
49
26
  xl: "h-16 w-16",
50
27
  };
51
28
 
52
- // Get the appropriate logo image based on variant
53
- // For iconOnly, always use favicon.ico
54
- const logoSrc = iconOnly
55
- ? "/favicon.ico"
56
- : variant === "white"
29
+ // Get the appropriate logo image based on variant or custom source
30
+ // For iconOnly, always use favicon.ico unless customSrc is provided
31
+ const logoSrc =
32
+ customSrc ||
33
+ (iconOnly
34
+ ? "/favicon.ico"
35
+ : variant === "white"
57
36
  ? "/image/logoWhite.png"
58
37
  : variant === "fullWhite"
59
- ? "/image/logoFullWhite.png"
60
- : variant === "favicon"
61
- ? "/favicon.ico"
62
- : "/image/logo.png";
38
+ ? "/image/logoFullWhite.png"
39
+ : variant === "favicon"
40
+ ? "/favicon.ico"
41
+ : "/image/logo.png");
42
+
43
+ const isClickable = clickable || !!onClick;
63
44
 
64
45
  if (iconOnly) {
65
46
  return (
@@ -67,16 +48,33 @@ const Logo = ({
67
48
  className={cn(
68
49
  "flex items-center justify-center",
69
50
  iconSizes[size],
51
+ isClickable && "cursor-pointer",
70
52
  className
71
53
  )}
72
54
  onClick={onClick}
73
- role={onClick ? "button" : undefined}
74
- tabIndex={onClick ? 0 : undefined}
55
+ role={isClickable ? "button" : undefined}
56
+ tabIndex={isClickable ? 0 : undefined}
57
+ onKeyDown={
58
+ isClickable
59
+ ? (e) => {
60
+ if (e.key === "Enter" || e.key === " ") {
61
+ e.preventDefault();
62
+ onClick?.();
63
+ }
64
+ }
65
+ : undefined
66
+ }
75
67
  >
76
68
  <img
77
69
  src={logoSrc}
78
- alt="MEDECINE 360 Logo"
79
- className="w-full h-full object-contain"
70
+ alt={alt}
71
+ className="object-contain w-full h-full"
72
+ onError={(e) => {
73
+ // Fallback to favicon if image fails to load
74
+ if (logoSrc !== "/favicon.ico") {
75
+ (e.target as unknown as { src: string }).src = "/favicon.ico";
76
+ }
77
+ }}
80
78
  />
81
79
  </div>
82
80
  );
@@ -84,19 +82,41 @@ const Logo = ({
84
82
 
85
83
  return (
86
84
  <div
87
- className={cn("flex items-center", sizes[size], className)}
85
+ className={cn(
86
+ "flex items-center",
87
+ sizes[size],
88
+ isClickable && "cursor-pointer",
89
+ className
90
+ )}
88
91
  onClick={onClick}
89
- role={onClick ? "button" : undefined}
90
- tabIndex={onClick ? 0 : undefined}
92
+ role={isClickable ? "button" : undefined}
93
+ tabIndex={isClickable ? 0 : undefined}
94
+ onKeyDown={
95
+ isClickable
96
+ ? (e) => {
97
+ if (e.key === "Enter" || e.key === " ") {
98
+ e.preventDefault();
99
+ onClick?.();
100
+ }
101
+ }
102
+ : undefined
103
+ }
91
104
  >
92
105
  {/* Logo Image */}
93
106
  <img
94
107
  src={logoSrc}
95
- alt="MEDECINE 360 Logo"
96
- className="h-full w-auto object-contain"
108
+ alt={alt}
109
+ className="object-contain w-auto h-full"
110
+ onError={(e) => {
111
+ // Fallback to favicon if image fails to load
112
+ if (logoSrc !== "/favicon.ico") {
113
+ (e.target as unknown as { src: string }).src = "/favicon.ico";
114
+ }
115
+ }}
97
116
  />
98
117
  </div>
99
118
  );
100
119
  };
101
120
 
121
+ export type { LogoProps, LogoSize, LogoVariant } from "./Logo.types";
102
122
  export { Logo };
@@ -0,0 +1,42 @@
1
+ export type LogoSize = "sm" | "md" | "lg" | "xl";
2
+
3
+ export type LogoVariant = "normal" | "white" | "fullWhite" | "favicon";
4
+
5
+ export interface LogoProps {
6
+ /**
7
+ * Logo size
8
+ */
9
+ size?: LogoSize;
10
+ /**
11
+ * Whether to show the tagline
12
+ */
13
+ showTagline?: boolean;
14
+ /**
15
+ * Whether to show only the icon (favicon version)
16
+ */
17
+ iconOnly?: boolean;
18
+ /**
19
+ * Logo variant - normal, white, or favicon
20
+ */
21
+ variant?: LogoVariant;
22
+ /**
23
+ * Custom className
24
+ */
25
+ className?: string;
26
+ /**
27
+ * Click handler
28
+ */
29
+ onClick?: () => void;
30
+ /**
31
+ * Custom logo source URL (overrides variant)
32
+ */
33
+ customSrc?: string;
34
+ /**
35
+ * Alt text for the logo image
36
+ */
37
+ alt?: string;
38
+ /**
39
+ * Whether the logo is clickable (adds cursor pointer)
40
+ */
41
+ clickable?: boolean;
42
+ }
@@ -1,2 +1,2 @@
1
1
  export { Logo } from "./Logo";
2
- export type { LogoProps } from "./Logo";
2
+ export type { LogoProps, LogoSize, LogoVariant } from "./Logo";
@@ -6,7 +6,7 @@ import { Input } from "../Input/Input";
6
6
  // Generic interface for searchable entities
7
7
  export interface SearchableEntity {
8
8
  id: string;
9
- [key: string]: any;
9
+ [key: string]: unknown;
10
10
  }
11
11
 
12
12
  interface SearchAutocompleteProps<T extends SearchableEntity> {
@@ -1,15 +1,22 @@
1
1
  import { ChevronDown, Search, X } from "lucide-react";
2
- import React, { forwardRef, useEffect, useId, useRef, useState } from "react";
2
+ import React, {
3
+ forwardRef,
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useRef,
8
+ useState,
9
+ } from "react";
3
10
  import { cn } from "../../utils";
4
11
  import { Input } from "../Input";
5
12
 
6
- export interface SelectOption<T = any> {
13
+ export interface SelectOption<T = unknown> {
7
14
  value: T;
8
15
  label: string;
9
16
  disabled?: boolean;
10
17
  }
11
18
 
12
- export interface SelectProps<T = any> {
19
+ export interface SelectProps<T = unknown> {
13
20
  /**
14
21
  * Array of options to display
15
22
  */
@@ -149,7 +156,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
149
156
  : options;
150
157
 
151
158
  // Calculate dropdown position based on available space
152
- const calculateDropdownPosition = () => {
159
+ const calculateDropdownPosition = useCallback(() => {
153
160
  if (!containerRef.current) return;
154
161
 
155
162
  const containerRect = containerRef.current.getBoundingClientRect();
@@ -190,7 +197,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
190
197
  } else {
191
198
  setDropdownPosition("bottom");
192
199
  }
193
- };
200
+ }, [filteredOptions.length, searchable, maxHeight]);
194
201
 
195
202
  // Alternative calculation using actual dropdown element when available
196
203
  const calculateDropdownPositionWithElement = () => {
@@ -252,7 +259,13 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
252
259
  calculateDropdownPositionWithElement();
253
260
  });
254
261
  }
255
- }, [isOpen, filteredOptions.length, searchable, maxHeight]);
262
+ }, [
263
+ isOpen,
264
+ filteredOptions.length,
265
+ searchable,
266
+ maxHeight,
267
+ calculateDropdownPosition,
268
+ ]);
256
269
 
257
270
  // Recalculate position on window resize
258
271
  useEffect(() => {
@@ -269,7 +282,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
269
282
  window.removeEventListener("resize", handleResize);
270
283
  window.removeEventListener("scroll", handleResize);
271
284
  };
272
- }, [isOpen]);
285
+ }, [isOpen, calculateDropdownPosition]);
273
286
 
274
287
  // Handle keyboard navigation
275
288
  const handleKeyDown = (event: React.KeyboardEvent) => {
@@ -335,7 +348,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
335
348
  // Handle clear
336
349
  const handleClear = (event: React.MouseEvent) => {
337
350
  event.stopPropagation();
338
- onChange?.(undefined as any, {} as SelectOption);
351
+ onChange?.(undefined as unknown, {} as SelectOption);
339
352
  };
340
353
 
341
354
  // Handle toggle