@ews-admin/global-design-system 1.1.13 → 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/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.14",
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";
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
 
@@ -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,14}$/;
92
+ const phoneRegex = /^[0-9]\d{1,17}$/;
93
93
  return phoneRegex.test(trimmedValue);
94
94
  }