@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/dist/index.esm.js CHANGED
@@ -79,19 +79,20 @@ const formatNumeric = (value) => {
79
79
  */
80
80
  function isValidPhoneNumber(value) {
81
81
  const trimmedValue = value.trim();
82
- const phoneRegex = /^[0-9]\d{1,14}$/;
82
+ const phoneRegex = /^[0-9]\d{1,17}$/;
83
83
  return phoneRegex.test(trimmedValue);
84
84
  }
85
85
 
86
- const Button = React.forwardRef(({ className, variant = "primary", size = "md", loading = false, fullWidth = false, leftIcon, rightIcon, children, disabled, ...props }, ref) => {
87
- const baseStyles = "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";
86
+ const Button = React.forwardRef(({ className, variant = "ews-primary", size = "md", loading = false, fullWidth = false, leftIcon, rightIcon, children, disabled, ...props }, ref) => {
87
+ const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none disabled:opacity-50 disabled:pointer-events-none";
88
88
  const variants = {
89
- primary: "bg-ews-primary text-white hover:bg-ews-primary-hover focus:ring-ews-primary",
90
- secondary: "bg-ews-secondary text-white hover:bg-ews-secondary-hover focus:ring-ews-secondary",
91
- success: "bg-ews-success text-white hover:bg-ews-success-hover focus:ring-ews-success",
92
- warning: "bg-ews-warning text-white hover:bg-ews-warning-hover focus:ring-ews-warning",
93
- error: "bg-ews-error text-white hover:bg-ews-error-hover focus:ring-ews-error",
94
- ghost: "bg-transparent text-ews-gray-700 hover:bg-ews-gray-100 focus:ring-ews-gray-500",
89
+ "ews-primary": "bg-ews-primary text-white hover:bg-ews-primary-hover",
90
+ "ews-secondary": "bg-ews-secondary text-white hover:bg-ews-secondary-hover",
91
+ success: "bg-ews-success text-white hover:bg-ews-success-hover",
92
+ warning: "bg-ews-warning text-white hover:bg-ews-warning-hover",
93
+ error: "bg-ews-error text-white hover:bg-ews-error-hover",
94
+ outline: "bg-transparent text-sm font-medium text-ews-primary hover:text-ews-primary/80",
95
+ 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",
95
96
  };
96
97
  const sizes = {
97
98
  sm: "px-3 py-1.5 text-sm",
@@ -103,7 +104,7 @@ const Button = React.forwardRef(({ className, variant = "primary", size = "md",
103
104
  md: "h-5 w-5",
104
105
  lg: "h-6 w-6",
105
106
  };
106
- return (jsxs("button", { className: cn(baseStyles, variants[variant], sizes[size], fullWidth && "w-full", className), ref: ref, disabled: disabled || loading, ...props, children: [loading && (jsxs("svg", { className: "animate-spin -ml-1 mr-2 h-4 w-4", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] })), !loading && leftIcon && (jsx("span", { className: cn("mr-2 flex items-center", iconSizes[size]), children: leftIcon })), children, !loading && rightIcon && (jsx("span", { className: cn("ml-2 flex items-center", iconSizes[size]), children: rightIcon }))] }));
107
+ return (jsxs("button", { className: cn(baseStyles, variants[variant], sizes[size], fullWidth && "w-full", className), ref: ref, disabled: disabled || loading, ...props, children: [loading && (jsxs("svg", { className: "mr-2 -ml-1 w-4 h-4 animate-spin", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] })), !loading && leftIcon && (jsx("span", { className: cn("flex items-center mr-2", iconSizes[size]), children: leftIcon })), children, !loading && rightIcon && (jsx("span", { className: cn("flex items-center ml-2", iconSizes[size]), children: rightIcon }))] }));
107
108
  });
108
109
  Button.displayName = "Button";
109
110
 
@@ -1255,6 +1256,50 @@ function useController(props) {
1255
1256
  }), [field, formState, fieldState]);
1256
1257
  }
1257
1258
 
1259
+ /**
1260
+ * Component based on `useController` hook to work with controlled component.
1261
+ *
1262
+ * @remarks
1263
+ * [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)
1264
+ *
1265
+ * @param props - the path name to the form field value, and validation rules.
1266
+ *
1267
+ * @returns provide field handler functions, field and form state.
1268
+ *
1269
+ * @example
1270
+ * ```tsx
1271
+ * function App() {
1272
+ * const { control } = useForm<FormValues>({
1273
+ * defaultValues: {
1274
+ * test: ""
1275
+ * }
1276
+ * });
1277
+ *
1278
+ * return (
1279
+ * <form>
1280
+ * <Controller
1281
+ * control={control}
1282
+ * name="test"
1283
+ * render={({ field: { onChange, onBlur, value, ref }, formState, fieldState }) => (
1284
+ * <>
1285
+ * <input
1286
+ * onChange={onChange} // send value to hook form
1287
+ * onBlur={onBlur} // notify when input is touched
1288
+ * value={value} // return updated value
1289
+ * ref={ref} // set ref for focus management
1290
+ * />
1291
+ * <p>{formState.isSubmitted ? "submitted" : ""}</p>
1292
+ * <p>{fieldState.isTouched ? "touched" : ""}</p>
1293
+ * </>
1294
+ * )}
1295
+ * />
1296
+ * </form>
1297
+ * );
1298
+ * }
1299
+ * ```
1300
+ */
1301
+ const Controller = (props) => props.render(useController(props));
1302
+
1258
1303
  function useSelectField({ name, control, options: _options, rules, defaultValue, }) {
1259
1304
  const { field, fieldState: { error, invalid }, } = useController({
1260
1305
  name,
@@ -1588,7 +1633,90 @@ const Modal = ({ isOpen, onClose, title, children, variant = "info", primaryActi
1588
1633
  onClose();
1589
1634
  }
1590
1635
  };
1591
- return (jsxs("div", { className: "flex fixed inset-0 z-50 justify-center items-center", children: [jsx("div", { className: "absolute inset-0 backdrop-blur-sm bg-black/50", onClick: handleOverlayClick }), 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: [jsxs("div", { className: cn("flex items-center justify-between p-6 border-b", variantStyles.borderColor), children: [jsxs("div", { className: "flex items-center space-x-3", children: [jsx("div", { className: cn("p-2 rounded-full", variantStyles.iconBg), children: variantStyles.icon }), jsx("h2", { id: "modal-title", className: cn("text-lg font-semibold", variantStyles.titleColor), children: title })] }), jsx("button", { onClick: onClose, className: "p-1 text-gray-400 transition-colors hover:text-gray-600", "aria-label": "Close modal", children: jsx(X, { className: "w-5 h-5" }) })] }), jsx("div", { className: cn("p-6", contentClassName), children: jsx("div", { className: "leading-relaxed text-gray-700", children: error && variant === "error" ? (jsxs("div", { className: "space-y-3", children: [jsx("p", { children: error.message }), error.fields && error.fields.length > 0 && (jsxs("div", { children: [jsx("p", { className: "font-semibold text-gray-900", children: "Erreurs de champ:" }), jsx("ul", { className: "mt-2 space-y-1", children: error.fields.map((field, index) => (jsxs("li", { className: "text-ews-error", children: ["\u2022 ", field.path, ": ", field.message] }, index))) })] }))] })) : (children) }) }), (primaryAction || secondaryAction) && (jsxs("div", { className: "flex justify-end items-center p-6 pt-0 space-x-3", children: [secondaryAction && (jsx(Button, { variant: "ghost", onClick: onSecondaryAction || onClose, disabled: isLoading, children: secondaryAction })), primaryAction && (jsx(Button, { variant: variant === "error" ? "error" : "primary", onClick: onPrimaryAction, loading: isLoading, children: primaryAction }))] }))] })] }));
1636
+ return (jsxs("div", { className: "flex fixed inset-0 z-50 justify-center items-center", children: [jsx("div", { className: "absolute inset-0 backdrop-blur-sm bg-black/50", onClick: handleOverlayClick }), 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: [jsxs("div", { className: cn("flex items-center justify-between p-6 border-b", variantStyles.borderColor), children: [jsxs("div", { className: "flex items-center space-x-3", children: [jsx("div", { className: cn("p-2 rounded-full", variantStyles.iconBg), children: variantStyles.icon }), jsx("h2", { id: "modal-title", className: cn("text-lg font-semibold", variantStyles.titleColor), children: title })] }), jsx("button", { onClick: onClose, className: "p-1 text-gray-400 transition-colors hover:text-gray-600", "aria-label": "Close modal", children: jsx(X, { className: "w-5 h-5" }) })] }), jsx("div", { className: cn("p-6", contentClassName), children: jsx("div", { className: "leading-relaxed text-gray-700", children: error && variant === "error" ? (jsxs("div", { className: "space-y-3", children: [jsx("p", { children: error.message }), error.fields && error.fields.length > 0 && (jsxs("div", { children: [jsx("p", { className: "font-semibold text-gray-900", children: "Erreurs de champ:" }), jsx("ul", { className: "mt-2 space-y-1", children: error.fields.map((field, index) => (jsxs("li", { className: "text-ews-error", children: ["\u2022 ", field.path, ": ", field.message] }, index))) })] }))] })) : (children) }) }), (primaryAction || secondaryAction) && (jsxs("div", { className: "flex justify-end items-center p-6 pt-0 space-x-3", children: [secondaryAction && (jsx(Button, { variant: "outline", onClick: onSecondaryAction || onClose, disabled: isLoading, children: secondaryAction })), primaryAction && (jsx(Button, { variant: variant === "error" ? "error" : "ews-primary", onClick: onPrimaryAction, loading: isLoading, children: primaryAction }))] }))] })] }));
1637
+ };
1638
+
1639
+ const DropdownMultiSelect = ({ options, name, control, placeholder = "Select options", searchPlaceholder = "Search...", onChange, value: controlledValue, defaultValue, onValidate, disabled = false, error, label, className, }) => {
1640
+ const [isOpen, setIsOpen] = useState(false);
1641
+ const [searchTerm, setSearchTerm] = useState("");
1642
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
1643
+ const dropdownRef = useRef(null);
1644
+ const handleToggle = () => {
1645
+ if (!disabled) {
1646
+ setIsOpen(!isOpen);
1647
+ }
1648
+ };
1649
+ const handleClickOutside = (event) => {
1650
+ if (dropdownRef.current &&
1651
+ !dropdownRef.current.contains(event.target)) {
1652
+ setIsOpen(false);
1653
+ setSearchTerm("");
1654
+ }
1655
+ };
1656
+ useEffect(() => {
1657
+ document.addEventListener("mousedown", handleClickOutside);
1658
+ return () => {
1659
+ document.removeEventListener("mousedown", handleClickOutside);
1660
+ };
1661
+ }, []);
1662
+ const removeAccents = (str) => str
1663
+ .normalize("NFD")
1664
+ .replace(/[\u0300-\u036f]/g, "")
1665
+ .replace(/ç/g, "c")
1666
+ .replace(/é|è|ê|ë/g, "e")
1667
+ .replace(/à|á|â|ã|ä/g, "a")
1668
+ .replace(/î|ï/g, "i")
1669
+ .replace(/ô|ö/g, "o")
1670
+ .replace(/ù|ú|û|ü/g, "u");
1671
+ const getDisplayValue = (value) => {
1672
+ if (typeof value === "string" || typeof value === "number") {
1673
+ return String(value);
1674
+ }
1675
+ return options.find((opt) => opt.value === value)?.label || "";
1676
+ };
1677
+ const filteredOptions = options.filter((option) => removeAccents(option.label.toLowerCase()).includes(removeAccents(searchTerm.toLowerCase())));
1678
+ const renderDropdown = ({ value = [], onChange: fieldOnChange, }) => (jsxs("div", { className: cn("relative", className), ref: dropdownRef, children: [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: [jsx("span", { className: cn("truncate", !value?.length && "text-ews-gray-500"), children: value?.length > 0
1679
+ ? value.map((v) => getDisplayValue(v)).join(", ")
1680
+ : placeholder }), jsx("span", { className: cn("ml-2 w-4 h-4 transition-transform transform", isOpen ? "rotate-180" : "rotate-0"), children: jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) }) })] }), isOpen && (jsxs("div", { className: "absolute z-50 w-full bg-white rounded-b-md border border-t-0 shadow-lg border-ews-gray-300", children: [jsx("div", { className: "p-2 border-b border-ews-gray-200", children: 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" }) }), jsx("div", { className: "overflow-y-auto max-h-48", children: filteredOptions.length > 0 ? (filteredOptions.map((option) => (jsxs("div", { className: "flex items-center p-2 cursor-pointer hover:bg-ews-gray-100", onClick: () => {
1681
+ const currentValue = value ?? [];
1682
+ const isSelected = currentValue.some((item) => JSON.stringify(item) === JSON.stringify(option.value));
1683
+ const newValue = isSelected
1684
+ ? currentValue.filter((item) => JSON.stringify(item) !==
1685
+ JSON.stringify(option.value))
1686
+ : [...currentValue, option.value];
1687
+ if (onValidate && !onValidate(newValue)) {
1688
+ return;
1689
+ }
1690
+ fieldOnChange(newValue);
1691
+ onChange?.(newValue);
1692
+ }, children: [jsx("input", { type: "checkbox", checked: (value ?? []).some((item) => JSON.stringify(item) === JSON.stringify(option.value)), onChange: (e) => {
1693
+ e.stopPropagation();
1694
+ const currentValue = value ?? [];
1695
+ const isSelected = currentValue.some((item) => JSON.stringify(item) === JSON.stringify(option.value));
1696
+ const newValue = isSelected
1697
+ ? currentValue.filter((item) => JSON.stringify(item) !==
1698
+ JSON.stringify(option.value))
1699
+ : [...currentValue, option.value];
1700
+ if (onValidate && !onValidate(newValue)) {
1701
+ return;
1702
+ }
1703
+ fieldOnChange(newValue);
1704
+ onChange?.(newValue);
1705
+ }, onClick: (e) => e.stopPropagation(), className: "mr-3 w-4 h-4 rounded border-ews-gray-300 text-ews-primary focus:ring-ews-primary" }), jsx("label", { className: "text-sm cursor-pointer text-ews-gray-700", children: option.label })] }, getDisplayValue(option.value))))) : (jsx("div", { className: "p-2 text-sm text-ews-gray-500", children: "No options found" })) })] }))] }));
1706
+ // Render controlled version with react-hook-form
1707
+ if (control) {
1708
+ return (jsx(Controller, { name: name, control: control, render: ({ field: { value, onChange } }) => (jsxs("div", { children: [label && (jsx("label", { className: "block mb-1 text-sm font-medium text-ews-gray-700", children: label })), renderDropdown({ value, onChange }), error && jsx("p", { className: "mt-1 text-sm text-ews-error", children: error })] })) }));
1709
+ }
1710
+ // Render uncontrolled version
1711
+ return (jsxs("div", { children: [label && (jsx("label", { className: "block mb-1 text-sm font-medium text-ews-gray-700", children: label })), renderDropdown({
1712
+ value: controlledValue ?? uncontrolledValue,
1713
+ onChange: (newValue) => {
1714
+ if (controlledValue === undefined) {
1715
+ setUncontrolledValue(newValue);
1716
+ }
1717
+ onChange?.(newValue);
1718
+ },
1719
+ }), error && jsx("p", { className: "mt-1 text-sm text-ews-error", children: error })] }));
1592
1720
  };
1593
1721
 
1594
1722
  const Logo = ({ size = "md", showTagline: _showTagline = true, iconOnly = false, variant = "normal", className, onClick, }) => {
@@ -1786,5 +1914,5 @@ const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesCh
1786
1914
  : "border-ews-gray-300"), children: isSelected && (jsx("svg", { className: "w-3 h-3 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) })) }), jsxs("div", { className: "flex flex-col", children: [jsx("span", { className: cn("font-medium", isSelected ? "text-ews-primary" : "text-gray-900"), children: specialty.label }), jsx("span", { className: cn("text-sm", isSelected ? "text-ews-primary/70" : "text-gray-500"), children: specialty.code })] })] })) }), showSelectedCount && selectedSpecialties.length > 0 && (jsxs("div", { className: "flex items-center justify-between text-sm text-gray-600", children: [jsxs("span", { children: [selectedSpecialties.length, " specialty", selectedSpecialties.length !== 1 ? "ies" : "", " selected"] }), maxSelections && (jsxs("span", { className: "text-gray-400", children: [selectedSpecialties.length, "/", maxSelections] }))] }))] }));
1787
1915
  };
1788
1916
 
1789
- export { ArrowRight, Button, Check, DoctorIcon, Icon, Input, Logo, Modal, MultiSearchAutocomplete, PatientIcon, Search, SearchAutocomplete, Select, SpecialtySearchAutocomplete, ThemeDebugger, ThemeProvider, ThemeToggle, UserIcon, cn, debounce, formatCurrency, formatDate, formatNumeric, generateId, isValidPhoneNumber, useDebounce, useDebouncedCallback, useSelectField, useTheme };
1917
+ export { ArrowRight, Button, Check, DoctorIcon, DropdownMultiSelect, Icon, Input, Logo, Modal, MultiSearchAutocomplete, PatientIcon, Search, SearchAutocomplete, Select, SpecialtySearchAutocomplete, ThemeDebugger, ThemeProvider, ThemeToggle, UserIcon, cn, debounce, formatCurrency, formatDate, formatNumeric, generateId, isValidPhoneNumber, useDebounce, useDebouncedCallback, useSelectField, useTheme };
1790
1918
  //# sourceMappingURL=index.esm.js.map