@ews-admin/global-design-system 1.1.12 → 1.1.14
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 +3 -3
- 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/index.css +1 -1
- package/dist/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +140 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +140 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +22 -17
- package/src/components/DropdownMultiSelect/DropdownMultiSelect.tsx +271 -0
- package/src/components/DropdownMultiSelect/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +2 -2
- package/src/index.ts +3 -0
- package/src/utils/index.ts +1 -1
package/package.json
CHANGED
|
@@ -6,7 +6,14 @@ export interface ButtonProps
|
|
|
6
6
|
/**
|
|
7
7
|
* Button variant
|
|
8
8
|
*/
|
|
9
|
-
variant?:
|
|
9
|
+
variant?:
|
|
10
|
+
| "ews-primary"
|
|
11
|
+
| "ews-secondary"
|
|
12
|
+
| "success"
|
|
13
|
+
| "warning"
|
|
14
|
+
| "error"
|
|
15
|
+
| "outline"
|
|
16
|
+
| "ghost";
|
|
10
17
|
/**
|
|
11
18
|
* Button size
|
|
12
19
|
*/
|
|
@@ -33,7 +40,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
33
40
|
(
|
|
34
41
|
{
|
|
35
42
|
className,
|
|
36
|
-
variant = "primary",
|
|
43
|
+
variant = "ews-primary",
|
|
37
44
|
size = "md",
|
|
38
45
|
loading = false,
|
|
39
46
|
fullWidth = false,
|
|
@@ -46,21 +53,19 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
46
53
|
ref
|
|
47
54
|
) => {
|
|
48
55
|
const baseStyles =
|
|
49
|
-
"inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none
|
|
56
|
+
"inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none disabled:opacity-50 disabled:pointer-events-none";
|
|
50
57
|
|
|
51
58
|
const variants = {
|
|
52
|
-
primary:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"bg-
|
|
60
|
-
error:
|
|
61
|
-
"bg-ews-error text-white hover:bg-ews-error-hover focus:ring-ews-error",
|
|
59
|
+
"ews-primary": "bg-ews-primary text-white hover:bg-ews-primary-hover",
|
|
60
|
+
"ews-secondary":
|
|
61
|
+
"bg-ews-secondary text-white hover:bg-ews-secondary-hover",
|
|
62
|
+
success: "bg-ews-success text-white hover:bg-ews-success-hover",
|
|
63
|
+
warning: "bg-ews-warning text-white hover:bg-ews-warning-hover",
|
|
64
|
+
error: "bg-ews-error text-white hover:bg-ews-error-hover",
|
|
65
|
+
outline:
|
|
66
|
+
"bg-transparent text-sm font-medium text-ews-primary hover:text-ews-primary/80",
|
|
62
67
|
ghost:
|
|
63
|
-
"
|
|
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",
|
|
64
69
|
};
|
|
65
70
|
|
|
66
71
|
const sizes = {
|
|
@@ -90,7 +95,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
90
95
|
>
|
|
91
96
|
{loading && (
|
|
92
97
|
<svg
|
|
93
|
-
className="
|
|
98
|
+
className="mr-2 -ml-1 w-4 h-4 animate-spin"
|
|
94
99
|
xmlns="http://www.w3.org/2000/svg"
|
|
95
100
|
fill="none"
|
|
96
101
|
viewBox="0 0 24 24"
|
|
@@ -111,13 +116,13 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
111
116
|
</svg>
|
|
112
117
|
)}
|
|
113
118
|
{!loading && leftIcon && (
|
|
114
|
-
<span className={cn("
|
|
119
|
+
<span className={cn("flex items-center mr-2", iconSizes[size])}>
|
|
115
120
|
{leftIcon}
|
|
116
121
|
</span>
|
|
117
122
|
)}
|
|
118
123
|
{children}
|
|
119
124
|
{!loading && rightIcon && (
|
|
120
|
-
<span className={cn("
|
|
125
|
+
<span className={cn("flex items-center ml-2", iconSizes[size])}>
|
|
121
126
|
{rightIcon}
|
|
122
127
|
</span>
|
|
123
128
|
)}
|
|
@@ -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 };
|
|
@@ -231,7 +231,7 @@ const Modal = ({
|
|
|
231
231
|
<div className="flex justify-end items-center p-6 pt-0 space-x-3">
|
|
232
232
|
{secondaryAction && (
|
|
233
233
|
<Button
|
|
234
|
-
variant="
|
|
234
|
+
variant="outline"
|
|
235
235
|
onClick={onSecondaryAction || onClose}
|
|
236
236
|
disabled={isLoading}
|
|
237
237
|
>
|
|
@@ -240,7 +240,7 @@ const Modal = ({
|
|
|
240
240
|
)}
|
|
241
241
|
{primaryAction && (
|
|
242
242
|
<Button
|
|
243
|
-
variant={variant === "error" ? "error" : "primary"}
|
|
243
|
+
variant={variant === "error" ? "error" : "ews-primary"}
|
|
244
244
|
onClick={onPrimaryAction}
|
|
245
245
|
loading={isLoading}
|
|
246
246
|
>
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ export { MultiSearchAutocomplete } from "./components/MultiSearchAutocomplete";
|
|
|
16
16
|
export { Modal } from "./components/Modal";
|
|
17
17
|
export type { ErrorField, ErrorObject, ModalProps } from "./components/Modal";
|
|
18
18
|
|
|
19
|
+
export { DropdownMultiSelect } from "./components/DropdownMultiSelect";
|
|
20
|
+
export type { DropdownMultiSelectProps } from "./components/DropdownMultiSelect";
|
|
21
|
+
|
|
19
22
|
export { Logo } from "./components/Logo";
|
|
20
23
|
export type { LogoProps } from "./components/Logo";
|
|
21
24
|
|
package/src/utils/index.ts
CHANGED
|
@@ -89,6 +89,6 @@ export const formatNumeric = (value: string): string => {
|
|
|
89
89
|
*/
|
|
90
90
|
export function isValidPhoneNumber(value: string): boolean {
|
|
91
91
|
const trimmedValue = value.trim();
|
|
92
|
-
const phoneRegex = /^[0-9]\d{1,
|
|
92
|
+
const phoneRegex = /^[0-9]\d{1,17}$/;
|
|
93
93
|
return phoneRegex.test(trimmedValue);
|
|
94
94
|
}
|