@ews-admin/global-design-system 1.14.0 → 1.17.1
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/ProfileImageUpload/ProfileImageUpload.d.ts +18 -5
- package/dist/components/ProfileImageUpload/ProfileImageUpload.d.ts.map +1 -1
- package/dist/components/ProfileImageUpload/index.d.ts +1 -1
- package/dist/components/ProfileImageUpload/index.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts +7 -0
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Tag/Tag.d.ts +8 -0
- package/dist/components/Tag/Tag.d.ts.map +1 -0
- package/dist/components/Tag/index.d.ts +3 -0
- package/dist/components/Tag/index.d.ts.map +1 -0
- package/dist/components/TagList/TagList.d.ts +14 -0
- package/dist/components/TagList/TagList.d.ts.map +1 -0
- package/dist/components/TagList/index.d.ts +3 -0
- package/dist/components/TagList/index.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +37 -24
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +39 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ProfileImageUpload/ProfileImageUpload.tsx +36 -9
- package/src/components/ProfileImageUpload/index.ts +4 -1
- package/src/components/Select/Select.tsx +48 -50
- package/src/components/Tag/Tag.tsx +33 -0
- package/src/components/Tag/index.ts +2 -0
- package/src/components/TagList/TagList.tsx +60 -0
- package/src/components/TagList/index.ts +2 -0
- package/src/index.ts +10 -1
package/package.json
CHANGED
|
@@ -4,8 +4,11 @@ import React, { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { cn } from "../../utils";
|
|
5
5
|
import { Modal } from "../Modal/Modal";
|
|
6
6
|
|
|
7
|
+
const ACCEPTED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"] as const;
|
|
8
|
+
export type AcceptedImageMimeType = (typeof ACCEPTED_MIME_TYPES)[number];
|
|
9
|
+
|
|
7
10
|
export interface ProfileImageUploadProps {
|
|
8
|
-
/** Current image URL */
|
|
11
|
+
/** Current image URL — pass your default placeholder URL when no custom image is set */
|
|
9
12
|
imageUrl: string;
|
|
10
13
|
/** Alt text for accessibility */
|
|
11
14
|
altText: string;
|
|
@@ -29,14 +32,24 @@ export interface ProfileImageUploadProps {
|
|
|
29
32
|
* Pass `false` when `imageUrl` is a default/fallback image.
|
|
30
33
|
*/
|
|
31
34
|
showDeleteButton?: boolean;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Accepted MIME types (default: JPEG, PNG, WEBP).
|
|
37
|
+
* Used for both the file-input `accept` attribute and client-side validation.
|
|
38
|
+
*/
|
|
39
|
+
acceptedFormats?: AcceptedImageMimeType[];
|
|
40
|
+
/** Max allowed file size in MB (default: 5) */
|
|
35
41
|
maxFileSizeMB?: number;
|
|
36
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Optional aspect-ratio hint shown below the avatar (e.g. "1:1", "4:3").
|
|
44
|
+
* Pure informational — no cropping is enforced client-side.
|
|
45
|
+
*/
|
|
46
|
+
aspectRatioHint?: string;
|
|
47
|
+
/** Called with the validated File once the user picks an acceptable file */
|
|
37
48
|
onFileSelect: (file: File) => void;
|
|
38
49
|
/** Called when the selected file exceeds `maxFileSizeMB` */
|
|
39
50
|
onFileSizeExceeded?: () => void;
|
|
51
|
+
/** Called when the selected file's MIME type is not in `acceptedFormats` */
|
|
52
|
+
onInvalidFormat?: () => void;
|
|
40
53
|
/** Called when the user confirms deletion in the modal */
|
|
41
54
|
onDeleteConfirm: () => void;
|
|
42
55
|
/** Deletion confirmation modal — title text */
|
|
@@ -114,10 +127,12 @@ export const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({
|
|
|
114
127
|
uploadProgress = 0,
|
|
115
128
|
isLoading = false,
|
|
116
129
|
showDeleteButton = true,
|
|
117
|
-
|
|
118
|
-
maxFileSizeMB =
|
|
130
|
+
acceptedFormats = [...ACCEPTED_MIME_TYPES],
|
|
131
|
+
maxFileSizeMB = 5,
|
|
132
|
+
aspectRatioHint,
|
|
119
133
|
onFileSelect,
|
|
120
134
|
onFileSizeExceeded,
|
|
135
|
+
onInvalidFormat,
|
|
121
136
|
onDeleteConfirm,
|
|
122
137
|
deleteConfirmTitle,
|
|
123
138
|
deleteConfirmMessage,
|
|
@@ -128,12 +143,20 @@ export const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({
|
|
|
128
143
|
const [isHovered, setIsHovered] = useState(false);
|
|
129
144
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
130
145
|
|
|
146
|
+
const acceptAttr = acceptedFormats.join(",");
|
|
147
|
+
|
|
131
148
|
const handleEditClick = () => fileInputRef.current?.click();
|
|
132
149
|
|
|
133
150
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
134
151
|
const file = e.target.files?.[0];
|
|
135
152
|
if (!file) return;
|
|
136
153
|
|
|
154
|
+
if (acceptedFormats.indexOf(file.type as AcceptedImageMimeType) === -1) {
|
|
155
|
+
onInvalidFormat?.();
|
|
156
|
+
e.target.value = "";
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
if (file.size > maxFileSizeMB * 1024 * 1024) {
|
|
138
161
|
onFileSizeExceeded?.();
|
|
139
162
|
e.target.value = "";
|
|
@@ -141,7 +164,7 @@ export const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({
|
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
onFileSelect(file);
|
|
144
|
-
e.target.value = "";
|
|
167
|
+
e.target.value = "";
|
|
145
168
|
};
|
|
146
169
|
|
|
147
170
|
const handleConfirmDelete = () => {
|
|
@@ -177,7 +200,7 @@ export const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({
|
|
|
177
200
|
<input
|
|
178
201
|
type="file"
|
|
179
202
|
ref={fileInputRef}
|
|
180
|
-
accept={
|
|
203
|
+
accept={acceptAttr}
|
|
181
204
|
className="hidden"
|
|
182
205
|
onChange={handleFileChange}
|
|
183
206
|
/>
|
|
@@ -210,6 +233,10 @@ export const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({
|
|
|
210
233
|
</div>
|
|
211
234
|
|
|
212
235
|
<UploadProgressBar progress={uploadProgress} isLoading={isLoading} />
|
|
236
|
+
|
|
237
|
+
{aspectRatioHint && (
|
|
238
|
+
<p className="mt-1 text-xs text-gray-400">Ratio: {aspectRatioHint}</p>
|
|
239
|
+
)}
|
|
213
240
|
</div>
|
|
214
241
|
|
|
215
242
|
{/* Delete confirmation */}
|
|
@@ -100,6 +100,13 @@ export interface SelectProps<T = unknown> {
|
|
|
100
100
|
* Custom render function for selected value
|
|
101
101
|
*/
|
|
102
102
|
renderValue?: (option: SelectOption<T> | null) => React.ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* Custom render function for the trigger button content.
|
|
105
|
+
* When provided, replaces the selected-value/placeholder area and hides
|
|
106
|
+
* the built-in chevron so the trigger can render its own UI (e.g. a user
|
|
107
|
+
* avatar menu).
|
|
108
|
+
*/
|
|
109
|
+
renderTrigger?: () => React.ReactNode;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
const Select = forwardRef<HTMLDivElement, SelectProps>(
|
|
@@ -125,6 +132,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
|
|
125
132
|
clearable = false,
|
|
126
133
|
renderOption,
|
|
127
134
|
renderValue,
|
|
135
|
+
renderTrigger,
|
|
128
136
|
...props
|
|
129
137
|
},
|
|
130
138
|
ref
|
|
@@ -177,17 +185,6 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
|
|
177
185
|
// Add some buffer (20px) to prevent edge cases
|
|
178
186
|
const buffer = 20;
|
|
179
187
|
|
|
180
|
-
console.log("Position calculation:", {
|
|
181
|
-
spaceBelow,
|
|
182
|
-
spaceAbove,
|
|
183
|
-
dropdownHeight,
|
|
184
|
-
viewportHeight,
|
|
185
|
-
containerBottom: containerRect.bottom,
|
|
186
|
-
shouldOpenTop:
|
|
187
|
-
spaceBelow < dropdownHeight + buffer &&
|
|
188
|
-
spaceAbove > dropdownHeight + buffer,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
188
|
// If there's not enough space below but enough space above, position on top
|
|
192
189
|
if (
|
|
193
190
|
spaceBelow < dropdownHeight + buffer &&
|
|
@@ -211,14 +208,6 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
|
|
211
208
|
const spaceAbove = containerRect.top;
|
|
212
209
|
const actualDropdownHeight = dropdownRect.height;
|
|
213
210
|
|
|
214
|
-
console.log("Position calculation with element:", {
|
|
215
|
-
spaceBelow,
|
|
216
|
-
spaceAbove,
|
|
217
|
-
actualDropdownHeight,
|
|
218
|
-
viewportHeight,
|
|
219
|
-
containerBottom: containerRect.bottom,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
211
|
// If there's not enough space below but enough space above, position on top
|
|
223
212
|
if (
|
|
224
213
|
spaceBelow < actualDropdownHeight &&
|
|
@@ -436,39 +425,48 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
|
|
436
425
|
{...props}
|
|
437
426
|
>
|
|
438
427
|
<div className="flex justify-between items-center">
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
428
|
+
{renderTrigger ? (
|
|
429
|
+
renderTrigger()
|
|
430
|
+
) : (
|
|
431
|
+
<>
|
|
432
|
+
<div className="flex-1 min-w-0">
|
|
433
|
+
{selectedOption ? (
|
|
434
|
+
renderValue ? (
|
|
435
|
+
renderValue(selectedOption)
|
|
436
|
+
) : (
|
|
437
|
+
<span className="truncate">{selectedOption.label}</span>
|
|
438
|
+
)
|
|
439
|
+
) : (
|
|
440
|
+
<span className="text-ews-gray-500">{placeholder}</span>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
450
443
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
444
|
+
<div className="flex items-center ml-2 space-x-1">
|
|
445
|
+
{clearable && selectedOption && !disabled && (
|
|
446
|
+
<button
|
|
447
|
+
type="button"
|
|
448
|
+
onClick={handleClear}
|
|
449
|
+
className="p-1 rounded hover:bg-ews-gray-100"
|
|
450
|
+
>
|
|
451
|
+
<X
|
|
452
|
+
className={cn(
|
|
453
|
+
iconSizeClasses[size],
|
|
454
|
+
"text-ews-gray-400"
|
|
455
|
+
)}
|
|
456
|
+
/>
|
|
457
|
+
</button>
|
|
458
|
+
)}
|
|
459
|
+
<ChevronDown
|
|
460
|
+
className={cn(
|
|
461
|
+
iconSizeClasses[size],
|
|
462
|
+
hasError ? "text-ews-error" : "text-ews-gray-400",
|
|
463
|
+
disabled && "text-ews-gray-300",
|
|
464
|
+
isOpen && "rotate-180 transition-transform"
|
|
465
|
+
)}
|
|
460
466
|
/>
|
|
461
|
-
</
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
className={cn(
|
|
465
|
-
iconSizeClasses[size],
|
|
466
|
-
hasError ? "text-ews-error" : "text-ews-gray-400",
|
|
467
|
-
disabled && "text-ews-gray-300",
|
|
468
|
-
isOpen && "rotate-180 transition-transform"
|
|
469
|
-
)}
|
|
470
|
-
/>
|
|
471
|
-
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</>
|
|
469
|
+
)}
|
|
472
470
|
</div>
|
|
473
471
|
</div>
|
|
474
472
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../../utils";
|
|
3
|
+
|
|
4
|
+
export interface TagProps {
|
|
5
|
+
label: string;
|
|
6
|
+
variant?: "primary" | "secondary" | "plain";
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Tag: React.FC<TagProps> = ({
|
|
11
|
+
label,
|
|
12
|
+
variant = "plain",
|
|
13
|
+
className,
|
|
14
|
+
}) => {
|
|
15
|
+
const variantClass =
|
|
16
|
+
variant === "primary"
|
|
17
|
+
? "bg-ews-primary text-white"
|
|
18
|
+
: variant === "secondary"
|
|
19
|
+
? "bg-ews-secondary text-white"
|
|
20
|
+
: "bg-gray-200 text-gray-800";
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<span
|
|
24
|
+
className={cn(
|
|
25
|
+
"inline-block rounded-md px-2.5 py-1 text-sm font-medium",
|
|
26
|
+
variantClass,
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
{label}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { cn } from "../../utils";
|
|
3
|
+
import { Tag, TagProps } from "../Tag/Tag";
|
|
4
|
+
|
|
5
|
+
export interface TagListProps {
|
|
6
|
+
items: string[];
|
|
7
|
+
maxVisible?: number;
|
|
8
|
+
showMoreLabel: string;
|
|
9
|
+
showLessLabel: string;
|
|
10
|
+
variant?: TagProps["variant"];
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Custom renderer for each tag. Overrides the default Tag component. */
|
|
13
|
+
renderTag?: (label: string, index: number) => React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MAX = 3;
|
|
17
|
+
|
|
18
|
+
export const TagList: React.FC<TagListProps> = ({
|
|
19
|
+
items,
|
|
20
|
+
maxVisible = DEFAULT_MAX,
|
|
21
|
+
showMoreLabel,
|
|
22
|
+
showLessLabel,
|
|
23
|
+
variant = "secondary",
|
|
24
|
+
className,
|
|
25
|
+
renderTag,
|
|
26
|
+
}) => {
|
|
27
|
+
const [expanded, setExpanded] = useState(false);
|
|
28
|
+
|
|
29
|
+
const hasMore = items.length > maxVisible;
|
|
30
|
+
const visible = expanded ? items : items.slice(0, maxVisible);
|
|
31
|
+
|
|
32
|
+
const handleToggle = (e: React.MouseEvent) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
setExpanded(prev => !prev);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("flex flex-wrap items-center gap-2", className)}>
|
|
40
|
+
{visible.map((label, i) =>
|
|
41
|
+
renderTag ? (
|
|
42
|
+
renderTag(label, i)
|
|
43
|
+
) : (
|
|
44
|
+
<Tag key={i} label={label} variant={variant} />
|
|
45
|
+
)
|
|
46
|
+
)}
|
|
47
|
+
{hasMore && !expanded && (
|
|
48
|
+
<span className="text-sm text-gray-400">...</span>
|
|
49
|
+
)}
|
|
50
|
+
{hasMore && (
|
|
51
|
+
<button
|
|
52
|
+
onClick={handleToggle}
|
|
53
|
+
className="text-xs text-ews-primary hover:underline"
|
|
54
|
+
>
|
|
55
|
+
{expanded ? showLessLabel : showMoreLabel}
|
|
56
|
+
</button>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,10 @@ export { Modal } from "./components/Modal";
|
|
|
17
17
|
export type { ErrorField, ErrorObject, ModalProps } from "./components/Modal";
|
|
18
18
|
|
|
19
19
|
export { ProfileImageUpload } from "./components/ProfileImageUpload";
|
|
20
|
-
export type {
|
|
20
|
+
export type {
|
|
21
|
+
AcceptedImageMimeType,
|
|
22
|
+
ProfileImageUploadProps,
|
|
23
|
+
} from "./components/ProfileImageUpload";
|
|
21
24
|
|
|
22
25
|
export { DropdownMultiSelect } from "./components/DropdownMultiSelect";
|
|
23
26
|
export type { DropdownMultiSelectProps } from "./components/DropdownMultiSelect";
|
|
@@ -37,6 +40,12 @@ export type { ThemeDebuggerProps } from "./components/ThemeDebugger";
|
|
|
37
40
|
export { RegionSelect } from "./components/RegionSelect";
|
|
38
41
|
export type { RegionSelectProps } from "./components/RegionSelect";
|
|
39
42
|
|
|
43
|
+
export { Tag } from "./components/Tag";
|
|
44
|
+
export type { TagProps } from "./components/Tag";
|
|
45
|
+
|
|
46
|
+
export { TagList } from "./components/TagList";
|
|
47
|
+
export type { TagListProps } from "./components/TagList";
|
|
48
|
+
|
|
40
49
|
// Molecules
|
|
41
50
|
export { SpecialtySearchAutocomplete } from "./molecules";
|
|
42
51
|
export type { Specialty, SpecialtySearchAutocompleteProps } from "./molecules";
|