@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.
- package/dist/components/Button/Button.d.ts +1 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts +22 -0
- package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts.map +1 -0
- package/dist/components/DropdownMultiSelect/index.d.ts +3 -0
- package/dist/components/DropdownMultiSelect/index.d.ts.map +1 -0
- package/dist/components/Logo/Logo.d.ts +3 -27
- package/dist/components/Logo/Logo.d.ts.map +1 -1
- package/dist/components/Logo/Logo.types.d.ts +41 -0
- package/dist/components/Logo/Logo.types.d.ts.map +1 -0
- package/dist/components/Logo/index.d.ts +1 -1
- package/dist/components/Logo/index.d.ts.map +1 -1
- package/dist/components/Logo/logoAssets.d.ts +1 -0
- package/dist/components/Logo/logoAssets.d.ts.map +1 -0
- package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts +1 -1
- package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts +3 -3
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/hooks/useSelectField.d.ts +4 -4
- package/dist/hooks/useSelectField.d.ts.map +1 -1
- package/dist/icons/Icon.d.ts +1 -1
- package/dist/icons/Icon.d.ts.map +1 -1
- package/dist/index.css +2 -2
- package/dist/index.d.ts +54 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +2 -2
- package/dist/index.esm.js +184 -22
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +183 -20
- package/dist/index.js.map +1 -1
- package/dist/styles/theme-variables.css +62 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +4 -1
- package/src/components/DropdownMultiSelect/DropdownMultiSelect.tsx +271 -0
- package/src/components/DropdownMultiSelect/index.ts +2 -0
- package/src/components/Logo/Logo.tsx +65 -45
- package/src/components/Logo/Logo.types.ts +42 -0
- package/src/components/Logo/index.ts +1 -1
- package/src/components/SearchAutocomplete/SearchAutocomplete.tsx +1 -1
- package/src/components/Select/Select.tsx +21 -8
- package/src/hooks/useSelectField.ts +7 -2
- package/src/icons/Icon.tsx +1 -1
- package/src/index.ts +3 -0
- package/src/styles/index.css +0 -32
- package/src/utils/index.ts +5 -3
- 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
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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:
|
|
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-
|
|
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,
|
|
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
|
@@ -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 };
|
|
@@ -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 =
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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={
|
|
74
|
-
tabIndex={
|
|
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=
|
|
79
|
-
className="w-full h-full
|
|
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(
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex items-center",
|
|
87
|
+
sizes[size],
|
|
88
|
+
isClickable && "cursor-pointer",
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
88
91
|
onClick={onClick}
|
|
89
|
-
role={
|
|
90
|
-
tabIndex={
|
|
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=
|
|
96
|
-
className="
|
|
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";
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { ChevronDown, Search, X } from "lucide-react";
|
|
2
|
-
import 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 =
|
|
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 =
|
|
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
|
-
}, [
|
|
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
|
|
351
|
+
onChange?.(undefined as unknown, {} as SelectOption);
|
|
339
352
|
};
|
|
340
353
|
|
|
341
354
|
// Handle toggle
|