@ews-admin/global-design-system 1.1.14 → 1.1.16
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 +4 -0
- package/dist/components/Input/Input.d.ts +22 -0
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Input/index.d.ts +1 -1
- package/dist/components/Input/index.d.ts.map +1 -1
- 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 +52 -14
- package/dist/index.esm.css +2 -2
- package/dist/index.esm.js +77 -23
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +76 -22
- 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/Input/Input.tsx +112 -3
- package/src/components/Input/index.ts +1 -1
- 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/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
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ChevronDown } from "lucide-react";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
3
|
import { Eye, EyeOff } from "../../icons";
|
|
3
4
|
import { cn } from "../../utils";
|
|
4
5
|
|
|
6
|
+
export interface CountryCodeOption {
|
|
7
|
+
code: string;
|
|
8
|
+
country: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CountryCodeSelectProps {
|
|
12
|
+
/**
|
|
13
|
+
* Array of country code options
|
|
14
|
+
*/
|
|
15
|
+
options: CountryCodeOption[];
|
|
16
|
+
/**
|
|
17
|
+
* Currently selected country code
|
|
18
|
+
*/
|
|
19
|
+
value: string;
|
|
20
|
+
/**
|
|
21
|
+
* Handler for when country code changes
|
|
22
|
+
*/
|
|
23
|
+
onChange: (code: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
export interface InputProps
|
|
6
27
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
7
28
|
/**
|
|
@@ -44,6 +65,10 @@ export interface InputProps
|
|
|
44
65
|
* Whether the input is required (shows red asterisk)
|
|
45
66
|
*/
|
|
46
67
|
required?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Country code dropdown configuration for phone inputs
|
|
70
|
+
*/
|
|
71
|
+
countryCodeSelect?: CountryCodeSelectProps;
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
@@ -60,6 +85,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
60
85
|
fullWidth = false,
|
|
61
86
|
showPasswordToggle = false,
|
|
62
87
|
required = false,
|
|
88
|
+
countryCodeSelect,
|
|
63
89
|
id,
|
|
64
90
|
type = "text",
|
|
65
91
|
...props
|
|
@@ -76,6 +102,25 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
76
102
|
const shouldShowPasswordToggle = showPasswordToggle && isPasswordInput;
|
|
77
103
|
const actualType = isPasswordInput && showPassword ? "text" : type;
|
|
78
104
|
|
|
105
|
+
// Country code dropdown state
|
|
106
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
107
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
108
|
+
|
|
109
|
+
// Close dropdown when clicking outside
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
112
|
+
if (
|
|
113
|
+
dropdownRef.current &&
|
|
114
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
115
|
+
) {
|
|
116
|
+
setIsDropdownOpen(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
121
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
79
124
|
const baseStyles =
|
|
80
125
|
"block w-full rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-0 hover:border-ews-primary";
|
|
81
126
|
|
|
@@ -167,7 +212,69 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
167
212
|
</label>
|
|
168
213
|
)}
|
|
169
214
|
<div className="relative">
|
|
170
|
-
{
|
|
215
|
+
{countryCodeSelect && (
|
|
216
|
+
<div className="absolute inset-y-0 left-0 flex items-center pl-1 z-10" ref={dropdownRef}>
|
|
217
|
+
<div className="relative">
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
221
|
+
className={cn(
|
|
222
|
+
"flex items-center gap-1 px-2 py-1 rounded text-ews-gray-700",
|
|
223
|
+
"hover:bg-ews-gray-50 transition-colors cursor-pointer",
|
|
224
|
+
"focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-0",
|
|
225
|
+
"border-0 bg-transparent text-base"
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
<span className="font-medium">{countryCodeSelect.value}</span>
|
|
229
|
+
<ChevronDown
|
|
230
|
+
className={cn(
|
|
231
|
+
"w-4 h-4 text-ews-gray-400 transition-transform",
|
|
232
|
+
isDropdownOpen && "rotate-180"
|
|
233
|
+
)}
|
|
234
|
+
/>
|
|
235
|
+
</button>
|
|
236
|
+
|
|
237
|
+
{isDropdownOpen && (
|
|
238
|
+
<div
|
|
239
|
+
className={cn(
|
|
240
|
+
"absolute top-full left-0 mt-1 bg-white rounded-md border shadow-lg",
|
|
241
|
+
"border-ews-gray-300 min-w-[120px] z-50"
|
|
242
|
+
)}
|
|
243
|
+
style={{ maxHeight: "200px", overflowY: "auto" }}
|
|
244
|
+
>
|
|
245
|
+
{countryCodeSelect.options.map((item) => {
|
|
246
|
+
const isSelected = item.code === countryCodeSelect.value;
|
|
247
|
+
return (
|
|
248
|
+
<div
|
|
249
|
+
key={item.code}
|
|
250
|
+
onClick={() => {
|
|
251
|
+
countryCodeSelect.onChange(item.code);
|
|
252
|
+
setIsDropdownOpen(false);
|
|
253
|
+
}}
|
|
254
|
+
className={cn(
|
|
255
|
+
"px-3 py-2 text-sm cursor-pointer transition-colors",
|
|
256
|
+
isSelected && "bg-ews-primary text-white",
|
|
257
|
+
!isSelected && "hover:bg-ews-gray-50"
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
260
|
+
<span className="font-medium">{item.code}</span>
|
|
261
|
+
{item.country && (
|
|
262
|
+
<span className={cn(
|
|
263
|
+
"ml-2 text-xs",
|
|
264
|
+
isSelected ? "text-white/80" : "text-ews-gray-500"
|
|
265
|
+
)}>
|
|
266
|
+
{item.country}
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
})}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
{leftIcon && !countryCodeSelect && (
|
|
171
278
|
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
|
172
279
|
<span className={cn("text-ews-gray-400", iconSizes[size])}>
|
|
173
280
|
{leftIcon}
|
|
@@ -181,7 +288,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
181
288
|
baseStyles,
|
|
182
289
|
variants[actualVariant],
|
|
183
290
|
sizes[size],
|
|
184
|
-
|
|
291
|
+
countryCodeSelect && "pl-24",
|
|
292
|
+
leftIcon && !countryCodeSelect && "pl-10",
|
|
185
293
|
(rightIcon || shouldShowPasswordToggle) && "pr-10",
|
|
186
294
|
className
|
|
187
295
|
)}
|
|
@@ -231,3 +339,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
231
339
|
Input.displayName = "Input";
|
|
232
340
|
|
|
233
341
|
export { Input };
|
|
342
|
+
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Input } from "./Input";
|
|
2
|
-
export type { InputProps } from "./Input";
|
|
2
|
+
export type { CountryCodeOption, CountryCodeSelectProps, InputProps } from "./Input";
|
|
@@ -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
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Control,
|
|
3
3
|
FieldPath,
|
|
4
|
+
FieldPathValue,
|
|
4
5
|
FieldValues,
|
|
6
|
+
RegisterOptions,
|
|
5
7
|
useController,
|
|
6
8
|
} from "react-hook-form";
|
|
7
9
|
import { SelectOption, SelectProps } from "../components/Select";
|
|
@@ -13,8 +15,11 @@ export interface UseSelectFieldProps<
|
|
|
13
15
|
name: TName;
|
|
14
16
|
control: Control<TFieldValues>;
|
|
15
17
|
options: SelectOption[];
|
|
16
|
-
rules?:
|
|
17
|
-
|
|
18
|
+
rules?: Omit<
|
|
19
|
+
RegisterOptions<TFieldValues, TName>,
|
|
20
|
+
"disabled" | "valueAsNumber" | "valueAsDate" | "setValueAs"
|
|
21
|
+
>;
|
|
22
|
+
defaultValue?: FieldPathValue<TFieldValues, TName>;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export function useSelectField<
|
package/src/icons/Icon.tsx
CHANGED
package/src/styles/index.css
CHANGED
|
@@ -287,36 +287,4 @@ div[tabindex]:focus {
|
|
|
287
287
|
.hover-scale:hover {
|
|
288
288
|
transform: scale(1.02);
|
|
289
289
|
}
|
|
290
|
-
|
|
291
|
-
/* Custom checkbox styling for better control */
|
|
292
|
-
input[type="checkbox"] {
|
|
293
|
-
width: 1rem;
|
|
294
|
-
height: 1rem;
|
|
295
|
-
border-radius: 0.25rem;
|
|
296
|
-
border: 2px solid var(--ews-gray-300);
|
|
297
|
-
background-color: white;
|
|
298
|
-
cursor: pointer;
|
|
299
|
-
transition: all 0.2s ease-in-out;
|
|
300
|
-
accent-color: var(--ews-primary);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
input[type="checkbox"]:hover {
|
|
304
|
-
border-color: var(--ews-primary);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
input[type="checkbox"]:focus {
|
|
308
|
-
outline: none;
|
|
309
|
-
border-color: var(--ews-primary);
|
|
310
|
-
box-shadow: 0 0 0 2px var(--ews-primary);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
input[type="checkbox"]:checked {
|
|
314
|
-
background-color: var(--ews-primary);
|
|
315
|
-
border-color: var(--ews-primary);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
input[type="checkbox"]:disabled {
|
|
319
|
-
opacity: 0.5;
|
|
320
|
-
cursor: not-allowed;
|
|
321
|
-
}
|
|
322
290
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function formatDate(
|
|
|
52
52
|
* @param wait - Wait time in milliseconds
|
|
53
53
|
* @returns Debounced function
|
|
54
54
|
*/
|
|
55
|
-
export function debounce<T extends (...args:
|
|
55
|
+
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
56
56
|
func: T,
|
|
57
57
|
wait: number
|
|
58
58
|
): (...args: Parameters<T>) => void {
|
|
@@ -83,12 +83,14 @@ export const formatNumeric = (value: string): string => {
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Utility function to validate phone numbers
|
|
86
|
-
* Validates phone numbers with 1-
|
|
86
|
+
* Validates phone numbers with 1-17 digits, optionally starting with + symbol
|
|
87
87
|
* @param value - Phone number string to validate
|
|
88
88
|
* @returns Boolean indicating if the phone number is valid
|
|
89
89
|
*/
|
|
90
90
|
export function isValidPhoneNumber(value: string): boolean {
|
|
91
91
|
const trimmedValue = value.trim();
|
|
92
|
-
|
|
92
|
+
// Allow + at the beginning, followed by 1-17 digits
|
|
93
|
+
// Or just 1-17 digits without +
|
|
94
|
+
const phoneRegex = /^(\+\d{1,17}|\d{1,17})$/;
|
|
93
95
|
return phoneRegex.test(trimmedValue);
|
|
94
96
|
}
|