@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ews-admin/global-design-system",
3
- "version": "1.14.0",
3
+ "version": "1.17.1",
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",
@@ -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
- /** File input accept attribute (default: "image/*") */
33
- accept?: string;
34
- /** Max allowed file size in MB (default: 3) */
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
- /** Called when a valid file is selected */
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
- accept = "image/*",
118
- maxFileSizeMB = 3,
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 = ""; // allow re-selecting the same file
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={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 */}
@@ -1,2 +1,5 @@
1
1
  export { ProfileImageUpload } from "./ProfileImageUpload";
2
- export type { ProfileImageUploadProps } from "./ProfileImageUpload";
2
+ export type {
3
+ AcceptedImageMimeType,
4
+ ProfileImageUploadProps,
5
+ } from "./ProfileImageUpload";
@@ -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
- <div className="flex-1 min-w-0">
440
- {selectedOption ? (
441
- renderValue ? (
442
- renderValue(selectedOption)
443
- ) : (
444
- <span className="truncate">{selectedOption.label}</span>
445
- )
446
- ) : (
447
- <span className="text-ews-gray-500">{placeholder}</span>
448
- )}
449
- </div>
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
- <div className="flex items-center ml-2 space-x-1">
452
- {clearable && selectedOption && !disabled && (
453
- <button
454
- type="button"
455
- onClick={handleClear}
456
- className="p-1 rounded hover:bg-ews-gray-100"
457
- >
458
- <X
459
- className={cn(iconSizeClasses[size], "text-ews-gray-400")}
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
- </button>
462
- )}
463
- <ChevronDown
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,2 @@
1
+ export { Tag } from "./Tag";
2
+ export type { TagProps } from "./Tag";
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ export { TagList } from "./TagList";
2
+ export type { TagListProps } from "./TagList";
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 { ProfileImageUploadProps } from "./components/ProfileImageUpload";
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";