@ews-admin/global-design-system 1.1.13 → 1.1.15

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.
Files changed (48) hide show
  1. package/dist/components/Button/Button.d.ts +1 -1
  2. package/dist/components/Button/Button.d.ts.map +1 -1
  3. package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts +22 -0
  4. package/dist/components/DropdownMultiSelect/DropdownMultiSelect.d.ts.map +1 -0
  5. package/dist/components/DropdownMultiSelect/index.d.ts +3 -0
  6. package/dist/components/DropdownMultiSelect/index.d.ts.map +1 -0
  7. package/dist/components/Logo/Logo.d.ts +3 -27
  8. package/dist/components/Logo/Logo.d.ts.map +1 -1
  9. package/dist/components/Logo/Logo.types.d.ts +41 -0
  10. package/dist/components/Logo/Logo.types.d.ts.map +1 -0
  11. package/dist/components/Logo/index.d.ts +1 -1
  12. package/dist/components/Logo/index.d.ts.map +1 -1
  13. package/dist/components/Logo/logoAssets.d.ts +1 -0
  14. package/dist/components/Logo/logoAssets.d.ts.map +1 -0
  15. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts +1 -1
  16. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts.map +1 -1
  17. package/dist/components/Select/Select.d.ts +3 -3
  18. package/dist/components/Select/Select.d.ts.map +1 -1
  19. package/dist/hooks/useSelectField.d.ts +4 -4
  20. package/dist/hooks/useSelectField.d.ts.map +1 -1
  21. package/dist/icons/Icon.d.ts +1 -1
  22. package/dist/icons/Icon.d.ts.map +1 -1
  23. package/dist/index.css +2 -2
  24. package/dist/index.d.ts +54 -18
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.esm.css +2 -2
  27. package/dist/index.esm.js +184 -22
  28. package/dist/index.esm.js.map +1 -1
  29. package/dist/index.js +183 -20
  30. package/dist/index.js.map +1 -1
  31. package/dist/styles/theme-variables.css +62 -0
  32. package/dist/utils/index.d.ts +2 -2
  33. package/dist/utils/index.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/Button/Button.tsx +4 -1
  36. package/src/components/DropdownMultiSelect/DropdownMultiSelect.tsx +271 -0
  37. package/src/components/DropdownMultiSelect/index.ts +2 -0
  38. package/src/components/Logo/Logo.tsx +65 -45
  39. package/src/components/Logo/Logo.types.ts +42 -0
  40. package/src/components/Logo/index.ts +1 -1
  41. package/src/components/SearchAutocomplete/SearchAutocomplete.tsx +1 -1
  42. package/src/components/Select/Select.tsx +21 -8
  43. package/src/hooks/useSelectField.ts +7 -2
  44. package/src/icons/Icon.tsx +1 -1
  45. package/src/index.ts +3 -0
  46. package/src/styles/index.css +0 -32
  47. package/src/utils/index.ts +5 -3
  48. package/tailwind.preset.js +23 -23
package/dist/index.js CHANGED
@@ -75,13 +75,15 @@ const formatNumeric = (value) => {
75
75
  };
76
76
  /**
77
77
  * Utility function to validate phone numbers
78
- * Validates phone numbers with 1-15 digits, starting with a non-zero digit
78
+ * Validates phone numbers with 1-17 digits, optionally starting with + symbol
79
79
  * @param value - Phone number string to validate
80
80
  * @returns Boolean indicating if the phone number is valid
81
81
  */
82
82
  function isValidPhoneNumber(value) {
83
83
  const trimmedValue = value.trim();
84
- const phoneRegex = /^[0-9]\d{1,14}$/;
84
+ // Allow + at the beginning, followed by 1-17 digits
85
+ // Or just 1-17 digits without +
86
+ const phoneRegex = /^(\+\d{1,17}|\d{1,17})$/;
85
87
  return phoneRegex.test(trimmedValue);
86
88
  }
87
89
 
@@ -94,6 +96,7 @@ const Button = React.forwardRef(({ className, variant = "ews-primary", size = "m
94
96
  warning: "bg-ews-warning text-white hover:bg-ews-warning-hover",
95
97
  error: "bg-ews-error text-white hover:bg-ews-error-hover",
96
98
  outline: "bg-transparent text-sm font-medium text-ews-primary hover:text-ews-primary/80",
99
+ ghost: "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",
97
100
  };
98
101
  const sizes = {
99
102
  sm: "px-3 py-1.5 text-sm",
@@ -516,7 +519,7 @@ const Select = React.forwardRef(({ options = [], value, onChange, placeholder =
516
519
  ? options.filter((option) => option.label.toLowerCase().includes(searchTerm.toLowerCase()))
517
520
  : options;
518
521
  // Calculate dropdown position based on available space
519
- const calculateDropdownPosition = () => {
522
+ const calculateDropdownPosition = React.useCallback(() => {
520
523
  if (!containerRef.current)
521
524
  return;
522
525
  const containerRect = containerRef.current.getBoundingClientRect();
@@ -547,7 +550,7 @@ const Select = React.forwardRef(({ options = [], value, onChange, placeholder =
547
550
  else {
548
551
  setDropdownPosition("bottom");
549
552
  }
550
- };
553
+ }, [filteredOptions.length, searchable, maxHeight]);
551
554
  // Alternative calculation using actual dropdown element when available
552
555
  const calculateDropdownPositionWithElement = () => {
553
556
  if (!containerRef.current || !dropdownRef.current)
@@ -597,7 +600,13 @@ const Select = React.forwardRef(({ options = [], value, onChange, placeholder =
597
600
  calculateDropdownPositionWithElement();
598
601
  });
599
602
  }
600
- }, [isOpen, filteredOptions.length, searchable, maxHeight]);
603
+ }, [
604
+ isOpen,
605
+ filteredOptions.length,
606
+ searchable,
607
+ maxHeight,
608
+ calculateDropdownPosition,
609
+ ]);
601
610
  // Recalculate position on window resize
602
611
  React.useEffect(() => {
603
612
  const handleResize = () => {
@@ -611,7 +620,7 @@ const Select = React.forwardRef(({ options = [], value, onChange, placeholder =
611
620
  window.removeEventListener("resize", handleResize);
612
621
  window.removeEventListener("scroll", handleResize);
613
622
  };
614
- }, [isOpen]);
623
+ }, [isOpen, calculateDropdownPosition]);
615
624
  // Handle keyboard navigation
616
625
  const handleKeyDown = (event) => {
617
626
  if (disabled)
@@ -1257,6 +1266,50 @@ function useController(props) {
1257
1266
  }), [field, formState, fieldState]);
1258
1267
  }
1259
1268
 
1269
+ /**
1270
+ * Component based on `useController` hook to work with controlled component.
1271
+ *
1272
+ * @remarks
1273
+ * [API](https://react-hook-form.com/docs/usecontroller/controller) • [Demo](https://codesandbox.io/s/react-hook-form-v6-controller-ts-jwyzw) • [Video](https://www.youtube.com/watch?v=N2UNk_UCVyA)
1274
+ *
1275
+ * @param props - the path name to the form field value, and validation rules.
1276
+ *
1277
+ * @returns provide field handler functions, field and form state.
1278
+ *
1279
+ * @example
1280
+ * ```tsx
1281
+ * function App() {
1282
+ * const { control } = useForm<FormValues>({
1283
+ * defaultValues: {
1284
+ * test: ""
1285
+ * }
1286
+ * });
1287
+ *
1288
+ * return (
1289
+ * <form>
1290
+ * <Controller
1291
+ * control={control}
1292
+ * name="test"
1293
+ * render={({ field: { onChange, onBlur, value, ref }, formState, fieldState }) => (
1294
+ * <>
1295
+ * <input
1296
+ * onChange={onChange} // send value to hook form
1297
+ * onBlur={onBlur} // notify when input is touched
1298
+ * value={value} // return updated value
1299
+ * ref={ref} // set ref for focus management
1300
+ * />
1301
+ * <p>{formState.isSubmitted ? "submitted" : ""}</p>
1302
+ * <p>{fieldState.isTouched ? "touched" : ""}</p>
1303
+ * </>
1304
+ * )}
1305
+ * />
1306
+ * </form>
1307
+ * );
1308
+ * }
1309
+ * ```
1310
+ */
1311
+ const Controller = (props) => props.render(useController(props));
1312
+
1260
1313
  function useSelectField({ name, control, options: _options, rules, defaultValue, }) {
1261
1314
  const { field, fieldState: { error, invalid }, } = useController({
1262
1315
  name,
@@ -1593,7 +1646,90 @@ const Modal = ({ isOpen, onClose, title, children, variant = "info", primaryActi
1593
1646
  return (jsxRuntime.jsxs("div", { className: "flex fixed inset-0 z-50 justify-center items-center", children: [jsxRuntime.jsx("div", { className: "absolute inset-0 backdrop-blur-sm bg-black/50", onClick: handleOverlayClick }), jsxRuntime.jsxs("div", { className: cn("relative mx-4 w-full max-w-md bg-white rounded-lg shadow-xl transition-all transform", "duration-200 animate-in fade-in-0 zoom-in-95", className), role: "dialog", "aria-modal": "true", "aria-labelledby": "modal-title", children: [jsxRuntime.jsxs("div", { className: cn("flex items-center justify-between p-6 border-b", variantStyles.borderColor), children: [jsxRuntime.jsxs("div", { className: "flex items-center space-x-3", children: [jsxRuntime.jsx("div", { className: cn("p-2 rounded-full", variantStyles.iconBg), children: variantStyles.icon }), jsxRuntime.jsx("h2", { id: "modal-title", className: cn("text-lg font-semibold", variantStyles.titleColor), children: title })] }), jsxRuntime.jsx("button", { onClick: onClose, className: "p-1 text-gray-400 transition-colors hover:text-gray-600", "aria-label": "Close modal", children: jsxRuntime.jsx(X, { className: "w-5 h-5" }) })] }), jsxRuntime.jsx("div", { className: cn("p-6", contentClassName), children: jsxRuntime.jsx("div", { className: "leading-relaxed text-gray-700", children: error && variant === "error" ? (jsxRuntime.jsxs("div", { className: "space-y-3", children: [jsxRuntime.jsx("p", { children: error.message }), error.fields && error.fields.length > 0 && (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("p", { className: "font-semibold text-gray-900", children: "Erreurs de champ:" }), jsxRuntime.jsx("ul", { className: "mt-2 space-y-1", children: error.fields.map((field, index) => (jsxRuntime.jsxs("li", { className: "text-ews-error", children: ["\u2022 ", field.path, ": ", field.message] }, index))) })] }))] })) : (children) }) }), (primaryAction || secondaryAction) && (jsxRuntime.jsxs("div", { className: "flex justify-end items-center p-6 pt-0 space-x-3", children: [secondaryAction && (jsxRuntime.jsx(Button, { variant: "outline", onClick: onSecondaryAction || onClose, disabled: isLoading, children: secondaryAction })), primaryAction && (jsxRuntime.jsx(Button, { variant: variant === "error" ? "error" : "ews-primary", onClick: onPrimaryAction, loading: isLoading, children: primaryAction }))] }))] })] }));
1594
1647
  };
1595
1648
 
1596
- const Logo = ({ size = "md", showTagline: _showTagline = true, iconOnly = false, variant = "normal", className, onClick, }) => {
1649
+ const DropdownMultiSelect = ({ options, name, control, placeholder = "Select options", searchPlaceholder = "Search...", onChange, value: controlledValue, defaultValue, onValidate, disabled = false, error, label, className, }) => {
1650
+ const [isOpen, setIsOpen] = React.useState(false);
1651
+ const [searchTerm, setSearchTerm] = React.useState("");
1652
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
1653
+ const dropdownRef = React.useRef(null);
1654
+ const handleToggle = () => {
1655
+ if (!disabled) {
1656
+ setIsOpen(!isOpen);
1657
+ }
1658
+ };
1659
+ const handleClickOutside = (event) => {
1660
+ if (dropdownRef.current &&
1661
+ !dropdownRef.current.contains(event.target)) {
1662
+ setIsOpen(false);
1663
+ setSearchTerm("");
1664
+ }
1665
+ };
1666
+ React.useEffect(() => {
1667
+ document.addEventListener("mousedown", handleClickOutside);
1668
+ return () => {
1669
+ document.removeEventListener("mousedown", handleClickOutside);
1670
+ };
1671
+ }, []);
1672
+ const removeAccents = (str) => str
1673
+ .normalize("NFD")
1674
+ .replace(/[\u0300-\u036f]/g, "")
1675
+ .replace(/ç/g, "c")
1676
+ .replace(/é|è|ê|ë/g, "e")
1677
+ .replace(/à|á|â|ã|ä/g, "a")
1678
+ .replace(/î|ï/g, "i")
1679
+ .replace(/ô|ö/g, "o")
1680
+ .replace(/ù|ú|û|ü/g, "u");
1681
+ const getDisplayValue = (value) => {
1682
+ if (typeof value === "string" || typeof value === "number") {
1683
+ return String(value);
1684
+ }
1685
+ return options.find((opt) => opt.value === value)?.label || "";
1686
+ };
1687
+ const filteredOptions = options.filter((option) => removeAccents(option.label.toLowerCase()).includes(removeAccents(searchTerm.toLowerCase())));
1688
+ const renderDropdown = ({ value = [], onChange: fieldOnChange, }) => (jsxRuntime.jsxs("div", { className: cn("relative", className), ref: dropdownRef, children: [jsxRuntime.jsxs("button", { type: "button", onClick: handleToggle, "aria-label": name, disabled: disabled, className: cn("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", isOpen ? "rounded-b-none border-b-0" : "rounded-md", error && "border-ews-error focus:ring-ews-error"), children: [jsxRuntime.jsx("span", { className: cn("truncate", !value?.length && "text-ews-gray-500"), children: value?.length > 0
1689
+ ? value.map((v) => getDisplayValue(v)).join(", ")
1690
+ : placeholder }), jsxRuntime.jsx("span", { className: cn("ml-2 w-4 h-4 transition-transform transform", isOpen ? "rotate-180" : "rotate-0"), children: jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) }) })] }), isOpen && (jsxRuntime.jsxs("div", { className: "absolute z-50 w-full bg-white rounded-b-md border border-t-0 shadow-lg border-ews-gray-300", children: [jsxRuntime.jsx("div", { className: "p-2 border-b border-ews-gray-200", children: jsxRuntime.jsx(Input, { type: "text", placeholder: searchPlaceholder, value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "p-0 border-0 shadow-none focus:ring-0", size: "sm" }) }), jsxRuntime.jsx("div", { className: "overflow-y-auto max-h-48", children: filteredOptions.length > 0 ? (filteredOptions.map((option) => (jsxRuntime.jsxs("div", { className: "flex items-center p-2 cursor-pointer hover:bg-ews-gray-100", onClick: () => {
1691
+ const currentValue = value ?? [];
1692
+ const isSelected = currentValue.some((item) => JSON.stringify(item) === JSON.stringify(option.value));
1693
+ const newValue = isSelected
1694
+ ? currentValue.filter((item) => JSON.stringify(item) !==
1695
+ JSON.stringify(option.value))
1696
+ : [...currentValue, option.value];
1697
+ if (onValidate && !onValidate(newValue)) {
1698
+ return;
1699
+ }
1700
+ fieldOnChange(newValue);
1701
+ onChange?.(newValue);
1702
+ }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: (value ?? []).some((item) => JSON.stringify(item) === JSON.stringify(option.value)), onChange: (e) => {
1703
+ e.stopPropagation();
1704
+ const currentValue = value ?? [];
1705
+ const isSelected = currentValue.some((item) => JSON.stringify(item) === JSON.stringify(option.value));
1706
+ const newValue = isSelected
1707
+ ? currentValue.filter((item) => JSON.stringify(item) !==
1708
+ JSON.stringify(option.value))
1709
+ : [...currentValue, option.value];
1710
+ if (onValidate && !onValidate(newValue)) {
1711
+ return;
1712
+ }
1713
+ fieldOnChange(newValue);
1714
+ onChange?.(newValue);
1715
+ }, onClick: (e) => e.stopPropagation(), className: "mr-3 w-4 h-4 rounded border-ews-gray-300 text-ews-primary focus:ring-ews-primary" }), jsxRuntime.jsx("label", { className: "text-sm cursor-pointer text-ews-gray-700", children: option.label })] }, getDisplayValue(option.value))))) : (jsxRuntime.jsx("div", { className: "p-2 text-sm text-ews-gray-500", children: "No options found" })) })] }))] }));
1716
+ // Render controlled version with react-hook-form
1717
+ if (control) {
1718
+ return (jsxRuntime.jsx(Controller, { name: name, control: control, render: ({ field: { value, onChange } }) => (jsxRuntime.jsxs("div", { children: [label && (jsxRuntime.jsx("label", { className: "block mb-1 text-sm font-medium text-ews-gray-700", children: label })), renderDropdown({ value, onChange }), error && jsxRuntime.jsx("p", { className: "mt-1 text-sm text-ews-error", children: error })] })) }));
1719
+ }
1720
+ // Render uncontrolled version
1721
+ return (jsxRuntime.jsxs("div", { children: [label && (jsxRuntime.jsx("label", { className: "block mb-1 text-sm font-medium text-ews-gray-700", children: label })), renderDropdown({
1722
+ value: controlledValue ?? uncontrolledValue,
1723
+ onChange: (newValue) => {
1724
+ if (controlledValue === undefined) {
1725
+ setUncontrolledValue(newValue);
1726
+ }
1727
+ onChange?.(newValue);
1728
+ },
1729
+ }), error && jsxRuntime.jsx("p", { className: "mt-1 text-sm text-ews-error", children: error })] }));
1730
+ };
1731
+
1732
+ const Logo = ({ size = "md", showTagline: _showTagline = true, iconOnly = false, variant = "normal", className, onClick, customSrc, alt = "MEDECINE 360 Logo", clickable = false, }) => {
1597
1733
  const sizes = {
1598
1734
  sm: "h-8",
1599
1735
  md: "h-12",
@@ -1606,21 +1742,47 @@ const Logo = ({ size = "md", showTagline: _showTagline = true, iconOnly = false,
1606
1742
  lg: "h-12 w-12",
1607
1743
  xl: "h-16 w-16",
1608
1744
  };
1609
- // Get the appropriate logo image based on variant
1610
- // For iconOnly, always use favicon.ico
1611
- const logoSrc = iconOnly
1612
- ? "/favicon.ico"
1613
- : variant === "white"
1614
- ? "/image/logoWhite.png"
1615
- : variant === "fullWhite"
1616
- ? "/image/logoFullWhite.png"
1617
- : variant === "favicon"
1618
- ? "/favicon.ico"
1619
- : "/image/logo.png";
1745
+ // Get the appropriate logo image based on variant or custom source
1746
+ // For iconOnly, always use favicon.ico unless customSrc is provided
1747
+ const logoSrc = customSrc ||
1748
+ (iconOnly
1749
+ ? "/favicon.ico"
1750
+ : variant === "white"
1751
+ ? "/image/logoWhite.png"
1752
+ : variant === "fullWhite"
1753
+ ? "/image/logoFullWhite.png"
1754
+ : variant === "favicon"
1755
+ ? "/favicon.ico"
1756
+ : "/image/logo.png");
1757
+ const isClickable = clickable || !!onClick;
1620
1758
  if (iconOnly) {
1621
- return (jsxRuntime.jsx("div", { className: cn("flex items-center justify-center", iconSizes[size], className), onClick: onClick, role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, children: jsxRuntime.jsx("img", { src: logoSrc, alt: "MEDECINE 360 Logo", className: "w-full h-full object-contain" }) }));
1759
+ return (jsxRuntime.jsx("div", { className: cn("flex items-center justify-center", iconSizes[size], isClickable && "cursor-pointer", className), onClick: onClick, role: isClickable ? "button" : undefined, tabIndex: isClickable ? 0 : undefined, onKeyDown: isClickable
1760
+ ? (e) => {
1761
+ if (e.key === "Enter" || e.key === " ") {
1762
+ e.preventDefault();
1763
+ onClick?.();
1764
+ }
1765
+ }
1766
+ : undefined, children: jsxRuntime.jsx("img", { src: logoSrc, alt: alt, className: "object-contain w-full h-full", onError: (e) => {
1767
+ // Fallback to favicon if image fails to load
1768
+ if (logoSrc !== "/favicon.ico") {
1769
+ e.target.src = "/favicon.ico";
1770
+ }
1771
+ } }) }));
1622
1772
  }
1623
- return (jsxRuntime.jsx("div", { className: cn("flex items-center", sizes[size], className), onClick: onClick, role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, children: jsxRuntime.jsx("img", { src: logoSrc, alt: "MEDECINE 360 Logo", className: "h-full w-auto object-contain" }) }));
1773
+ return (jsxRuntime.jsx("div", { className: cn("flex items-center", sizes[size], isClickable && "cursor-pointer", className), onClick: onClick, role: isClickable ? "button" : undefined, tabIndex: isClickable ? 0 : undefined, onKeyDown: isClickable
1774
+ ? (e) => {
1775
+ if (e.key === "Enter" || e.key === " ") {
1776
+ e.preventDefault();
1777
+ onClick?.();
1778
+ }
1779
+ }
1780
+ : undefined, children: jsxRuntime.jsx("img", { src: logoSrc, alt: alt, className: "object-contain w-auto h-full", onError: (e) => {
1781
+ // Fallback to favicon if image fails to load
1782
+ if (logoSrc !== "/favicon.ico") {
1783
+ e.target.src = "/favicon.ico";
1784
+ }
1785
+ } }) }));
1624
1786
  };
1625
1787
 
1626
1788
  const PROMED_THEME = {
@@ -1792,6 +1954,7 @@ exports.ArrowRight = ArrowRight;
1792
1954
  exports.Button = Button;
1793
1955
  exports.Check = Check;
1794
1956
  exports.DoctorIcon = DoctorIcon;
1957
+ exports.DropdownMultiSelect = DropdownMultiSelect;
1795
1958
  exports.Icon = Icon;
1796
1959
  exports.Input = Input;
1797
1960
  exports.Logo = Logo;