@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ews-admin/global-design-system",
3
- "version": "1.1.12",
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",
@@ -6,7 +6,14 @@ export interface ButtonProps
6
6
  /**
7
7
  * Button variant
8
8
  */
9
- variant?: "primary" | "secondary" | "success" | "warning" | "error" | "ghost";
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 focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-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
- "bg-ews-primary text-white hover:bg-ews-primary-hover focus:ring-ews-primary",
54
- secondary:
55
- "bg-ews-secondary text-white hover:bg-ews-secondary-hover focus:ring-ews-secondary",
56
- success:
57
- "bg-ews-success text-white hover:bg-ews-success-hover focus:ring-ews-success",
58
- warning:
59
- "bg-ews-warning text-white hover:bg-ews-warning-hover focus:ring-ews-warning",
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
- "bg-transparent text-ews-gray-700 hover:bg-ews-gray-100 focus:ring-ews-gray-500",
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="animate-spin -ml-1 mr-2 h-4 w-4"
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("mr-2 flex items-center", iconSizes[size])}>
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("ml-2 flex items-center", iconSizes[size])}>
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 };
@@ -0,0 +1,2 @@
1
+ export { DropdownMultiSelect } from "./DropdownMultiSelect";
2
+ export type { DropdownMultiSelectProps } from "./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="ghost"
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
 
@@ -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
  }