@ews-admin/global-design-system 1.27.0 → 1.29.0

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.js CHANGED
@@ -58,6 +58,29 @@ function buildUrls(environment) {
58
58
  adminUrl: `https://${prefix}admin.${domain}`,
59
59
  };
60
60
  }
61
+ /**
62
+ * Derives the environment from the host the app is served from, for builds
63
+ * that did not set VITE_ENV. Hosts actually served (Traefik rules in
64
+ * infrastructure/medecine360-hetzner/deployment/docker-stack.*.yml):
65
+ * - local: localhost, 127.0.0.1, *.medecine360local.com (local.app., api., ...)
66
+ * - dev: dev.{app,login,dashboard,admin,api}.medecine360.com
67
+ * - qa: qa.{app,login,dashboard,admin,api}.medecine360.com
68
+ * - prod: medecine360.com, {login,dashboard,admin,api}.medecine360.com
69
+ * Any other host (or no window, e.g. SSR or tests) falls back to local.
70
+ */
71
+ function environmentFromHost() {
72
+ const hostname = typeof window !== "undefined" ? window.location?.hostname?.toLowerCase() : undefined;
73
+ if (!hostname)
74
+ return "local";
75
+ if (hostname === "medecine360.com" || hostname.endsWith(".medecine360.com")) {
76
+ if (hostname.startsWith("dev."))
77
+ return "dev";
78
+ if (hostname.startsWith("qa."))
79
+ return "qa";
80
+ return "prod";
81
+ }
82
+ return "local";
83
+ }
61
84
  /**
62
85
  * Creates an environment configuration with all derived URLs.
63
86
  *
@@ -67,11 +90,20 @@ function buildUrls(environment) {
67
90
  * For local development, sensible defaults are provided but can be overridden
68
91
  * (e.g. to point BFF URL at a mock server).
69
92
  *
93
+ * When env is empty (build without VITE_ENV), the environment is derived from
94
+ * the current host instead of always assuming local, so a deployed build never
95
+ * links to http://local.*.medecine360local.com. A console warning names the
96
+ * derived environment so the misconfigured build stays visible.
97
+ *
70
98
  * @param env - The environment name (VITE_ENV value)
71
99
  * @param overrides - Optional URL overrides (e.g. for local mock servers)
72
100
  */
73
101
  function createEnvConfig(env, overrides) {
74
- const environment = (env || "local");
102
+ let environment = env;
103
+ if (!environment) {
104
+ environment = environmentFromHost();
105
+ console.warn(`[env-config] VITE_ENV is not set; using "${environment}" derived from the current host.`);
106
+ }
75
107
  const derived = buildUrls(environment);
76
108
  return {
77
109
  env: environment,
@@ -32786,7 +32818,7 @@ const Input = React.forwardRef(({ className, variant = "default", size = "md", l
32786
32818
  });
32787
32819
  Input.displayName = "Input";
32788
32820
 
32789
- const Select = React.forwardRef(({ options = [], value, onChange, placeholder = "Select an option...", label, helperText, error, isError = false, size = "md", disabled = false, required = false, searchable = false, multiple: _multiple = false, selectClassName, containerClassName, dropdownClassName, maxHeight = 200, clearable = false, renderOption, renderValue, renderTrigger, ...props }, ref) => {
32821
+ const Select = React.forwardRef(({ options = [], value, onChange, placeholder = "Select an option...", label, helperText, error, isError = false, size = "md", disabled = false, required = false, searchable = false, multiple: _multiple = false, selectClassName, containerClassName, dropdownClassName, maxHeight = 200, clearable = false, renderOption, renderValue, renderTrigger, searchPlaceholder = "Search options...", noOptionsFoundText = "No options found", noOptionsAvailableText = "No options available", ...props }, ref) => {
32790
32822
  const generatedId = React.useId();
32791
32823
  const selectId = `select-${generatedId}`;
32792
32824
  const hasError = isError || !!error;
@@ -32995,7 +33027,7 @@ const Select = React.forwardRef(({ options = [], value, onChange, placeholder =
32995
33027
  // Padding for icons
32996
33028
  "pr-10", selectClassName), ...props, children: jsxRuntime.jsx("div", { className: "flex justify-between items-center", children: renderTrigger ? (renderTrigger()) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: selectedOption ? (renderValue ? (renderValue(selectedOption)) : (jsxRuntime.jsx("span", { className: "truncate", children: selectedOption.label }))) : (jsxRuntime.jsx("span", { className: "text-ews-gray-500", children: placeholder })) }), jsxRuntime.jsxs("div", { className: "flex items-center ml-2 space-x-1", children: [clearable && selectedOption && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: "p-1 rounded hover:bg-ews-gray-100", children: jsxRuntime.jsx(X, { className: cn(iconSizeClasses[size], "text-ews-gray-400") }) })), jsxRuntime.jsx(ChevronDown, { className: cn(iconSizeClasses[size], hasError ? "text-ews-error" : "text-ews-gray-400", disabled && "text-ews-gray-300", isOpen && "rotate-180 transition-transform") })] })] })) }) }), isOpen && (jsxRuntime.jsxs("div", { ref: dropdownRef, role: "listbox", className: cn("absolute z-50 w-full bg-white rounded-md border shadow-lg border-ews-gray-300", "focus:outline-none", dropdownPosition === "top"
32997
33029
  ? "bottom-full mb-1"
32998
- : "top-full mt-1", dropdownClassName), style: { maxHeight: `${maxHeight}px` }, children: [searchable && (jsxRuntime.jsx("div", { className: "p-2 border-b border-ews-gray-200", children: jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(Search, { className: "absolute left-3 top-1/2 w-4 h-4 transform -translate-y-1/2 text-ews-gray-400" }), jsxRuntime.jsx(Input, { ref: inputRef, type: "text", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), placeholder: "Search options...", leftIcon: jsxRuntime.jsx(Search, { className: "w-4 h-4 text-ews-gray-400" }) })] }) })), jsxRuntime.jsx("div", { className: "overflow-auto py-1", style: { maxHeight: `${maxHeight - (searchable ? 60 : 0)}px` }, children: filteredOptions.length === 0 ? (jsxRuntime.jsx("div", { className: "px-3 py-2 text-sm text-ews-gray-500", children: searchTerm ? "No options found" : "No options available" })) : (filteredOptions.map((option, index) => {
33030
+ : "top-full mt-1", dropdownClassName), style: { maxHeight: `${maxHeight}px` }, children: [searchable && (jsxRuntime.jsx("div", { className: "p-2 border-b border-ews-gray-200", children: jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(Search, { className: "absolute left-3 top-1/2 w-4 h-4 transform -translate-y-1/2 text-ews-gray-400" }), jsxRuntime.jsx(Input, { ref: inputRef, type: "text", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), placeholder: searchPlaceholder, leftIcon: jsxRuntime.jsx(Search, { className: "w-4 h-4 text-ews-gray-400" }) })] }) })), jsxRuntime.jsx("div", { className: "overflow-auto py-1", style: { maxHeight: `${maxHeight - (searchable ? 60 : 0)}px` }, children: filteredOptions.length === 0 ? (jsxRuntime.jsx("div", { className: "px-3 py-2 text-sm text-ews-gray-500", children: searchTerm ? noOptionsFoundText : noOptionsAvailableText })) : (filteredOptions.map((option, index) => {
32999
33031
  const isSelected = option.value === value;
33000
33032
  const isFocused = index === focusedIndex;
33001
33033
  return (jsxRuntime.jsx("div", { ref: (el) => (optionRefs.current[index] = el), role: "option", "aria-selected": isSelected, onClick: () => handleSelect(option), className: cn("px-3 py-2 text-sm cursor-pointer transition-colors", isSelected && "bg-ews-primary text-white", !isSelected && isFocused && "bg-ews-gray-100", !isSelected && !isFocused && "hover:bg-ews-gray-50", option.disabled && "opacity-50 cursor-not-allowed"), children: renderOption ? (renderOption(option, isSelected)) : (jsxRuntime.jsx("span", { className: "truncate", children: option.label })) }, `${String(option.value)}-${index}`));
@@ -33072,7 +33104,7 @@ function useSelectField({ name, control, options: _options, rules, defaultValue,
33072
33104
  };
33073
33105
  }
33074
33106
 
33075
- function SearchAutocomplete({ onSelect, selectedId, searchFunction, getEntityById, getDisplayValue, getSecondaryText, placeholder, icon = Search, disabled = false, minSearchLength = 2, debounceTime = 300, error, }) {
33107
+ function SearchAutocomplete({ onSelect, selectedId, searchFunction, getEntityById, getDisplayValue, getSecondaryText, placeholder, icon = Search, disabled = false, minSearchLength = 2, debounceTime = 300, error, getMinCharactersHint = (min) => `(min. ${min} characters)`, }) {
33076
33108
  const [searchTerm, setSearchTerm] = React.useState("");
33077
33109
  const [entities, setEntities] = React.useState([]);
33078
33110
  const [isLoading, setIsLoading] = React.useState(false);
@@ -33185,7 +33217,11 @@ function SearchAutocomplete({ onSelect, selectedId, searchFunction, getEntityByI
33185
33217
  setSearchTerm("");
33186
33218
  onSelect("");
33187
33219
  };
33188
- return (jsxRuntime.jsxs("div", { className: "relative", ref: dropdownRef, children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(Input, { type: "text", placeholder: `${placeholder} (min. ${minSearchLength} characters)`, value: selectedEntity ? "" : searchTerm, onChange: handleSearchChange, onFocus: () => searchTerm.length >= minSearchLength &&
33220
+ const minCharactersHint = getMinCharactersHint(minSearchLength);
33221
+ const inputPlaceholder = minCharactersHint
33222
+ ? `${placeholder} ${minCharactersHint}`
33223
+ : placeholder;
33224
+ return (jsxRuntime.jsxs("div", { className: "relative", ref: dropdownRef, children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(Input, { type: "text", placeholder: inputPlaceholder, value: selectedEntity ? "" : searchTerm, onChange: handleSearchChange, onFocus: () => searchTerm.length >= minSearchLength &&
33189
33225
  isUserSearch.current &&
33190
33226
  setShowDropdown(true), disabled: disabled, autoComplete: "off", name: "search-autocomplete", leftIcon: React.createElement(icon, { size: 16 }) }), selectedEntity && (jsxRuntime.jsx("button", { type: "button", className: "flex absolute inset-y-0 right-0 items-center pr-3", onClick: handleClearSelection, disabled: disabled, children: jsxRuntime.jsx(X, { className: "w-5 h-5 text-gray-400 hover:text-gray-500" }) }))] }), selectedEntity && (jsxRuntime.jsx("div", { className: "p-4 mt-3 rounded-lg border bg-neutral-50 border-neutral-200", children: jsxRuntime.jsxs("div", { className: "flex justify-between items-center", children: [jsxRuntime.jsxs("div", { className: "flex items-center", children: [selectedEntity.photoUrl ? (jsxRuntime.jsx("img", { src: selectedEntity.photoUrl, alt: getDisplayValue(selectedEntity), className: "object-cover mr-3 w-10 h-10 rounded-full" })) : (React.createElement(icon, {
33191
33227
  className: "w-10 h-10 text-neutral-400 mr-3",
@@ -35256,7 +35292,7 @@ const PaymentDueBanner = ({ title, subtitle, warning, ctaLabel, onCtaClick, onDi
35256
35292
  */
35257
35293
  const SecureLoader = ({ label, description }) => (jsxRuntime.jsx("div", { "data-testid": "secure-loader", className: "flex min-h-screen items-center justify-center bg-gray-100", children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center rounded-lg bg-white p-6 shadow-lg", children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(Lock, { className: "h-16 w-16 text-gray-500" }), jsxRuntime.jsx("div", { className: "absolute bottom-2 right-1 h-6 w-6 animate-spin rounded-full border-4 border-gray-300 border-t-green-500" })] }), jsxRuntime.jsx("p", { className: "mt-4 text-lg text-gray-600", children: label }), description && (jsxRuntime.jsx("p", { className: "mt-2 text-sm text-gray-500", children: description }))] }) }));
35258
35294
 
35259
- const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesChange, specialties: availableSpecialties, placeholder = "Search and select medical specialties...", className = "", disabled = false, maxSelections, showSelectedCount = true, title = "Medical Specialties", label = "Select Specialties", getSelectedCountText, }) => {
35295
+ const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesChange, specialties: availableSpecialties, placeholder = "Search and select medical specialties...", className = "", disabled = false, maxSelections, showSelectedCount = true, title = "Medical Specialties", label = "Select Specialties", getSelectedCountText, removeSpecialtyLabel = "Remove specialty", }) => {
35260
35296
  const [filteredSpecialties, setFilteredSpecialties] = React.useState([]);
35261
35297
  const [isLoading, setIsLoading] = React.useState(false);
35262
35298
  // Filter specialties based on search term
@@ -35287,10 +35323,15 @@ const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesCh
35287
35323
  }
35288
35324
  onSpecialtiesChange(items);
35289
35325
  }, [onSpecialtiesChange, maxSelections]);
35326
+ // MED-1499: a selected specialty must be removable from its chip, not only
35327
+ // by toggling it off again in the dropdown list.
35328
+ const handleRemoveSpecialty = React.useCallback((specialtyToRemove) => {
35329
+ onSpecialtiesChange(selectedSpecialties.filter((specialty) => specialty.id !== specialtyToRemove.id));
35330
+ }, [onSpecialtiesChange, selectedSpecialties]);
35290
35331
  const getEntityById = React.useCallback(async (id) => {
35291
35332
  return availableSpecialties.find((specialty) => specialty.id === id);
35292
35333
  }, [availableSpecialties]);
35293
- return (jsxRuntime.jsxs("div", { className: cn("space-y-3", className), children: [jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx("div", { className: "flex justify-center items-center w-8 h-8 rounded-lg bg-secondary-100", children: jsxRuntime.jsx(Stethoscope, { className: "w-4 h-4 text-secondary-600" }) }), jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title || "Medical Specialties" })] }), jsxRuntime.jsx("div", { children: jsxRuntime.jsx("label", { className: "block mb-2 text-sm font-medium text-gray-700", children: label || "Select Specialties" }) }), jsxRuntime.jsx(MultiSearchAutocomplete, { items: filteredSpecialties, selectedItems: selectedSpecialties, onSelectionChange: handleSelectionChange, onSearch: fetchSpecialties, getEntityById: getEntityById, getPrimaryText: (specialty) => specialty.label, getSecondaryText: (specialty) => specialty.code, placeholder: placeholder, disabled: disabled, loading: isLoading, multiple: true, keepOpenOnSelect: true, className: "w-full", renderSelectedItem: (specialty) => (jsxRuntime.jsx("span", { className: "inline-flex items-center px-3 py-1 text-sm font-medium rounded-full border bg-ews-primary/10 text-ews-primary border-ews-primary/20", children: specialty.label })), renderListItem: (specialty, isSelected) => (jsxRuntime.jsxs("div", { className: "flex items-center space-x-3", children: [jsxRuntime.jsx("div", { className: cn("flex justify-center items-center w-5 h-5 rounded border-2", isSelected
35334
+ return (jsxRuntime.jsxs("div", { className: cn("space-y-3", className), children: [jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx("div", { className: "flex justify-center items-center w-8 h-8 rounded-lg bg-secondary-100", children: jsxRuntime.jsx(Stethoscope, { className: "w-4 h-4 text-secondary-600" }) }), jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title || "Medical Specialties" })] }), jsxRuntime.jsx("div", { children: jsxRuntime.jsx("label", { className: "block mb-2 text-sm font-medium text-gray-700", children: label || "Select Specialties" }) }), jsxRuntime.jsx(MultiSearchAutocomplete, { items: filteredSpecialties, selectedItems: selectedSpecialties, onSelectionChange: handleSelectionChange, onSearch: fetchSpecialties, getEntityById: getEntityById, getPrimaryText: (specialty) => specialty.label, getSecondaryText: (specialty) => specialty.code, placeholder: placeholder, disabled: disabled, loading: isLoading, multiple: true, keepOpenOnSelect: true, className: "w-full", renderSelectedItem: (specialty) => (jsxRuntime.jsxs("span", { className: "inline-flex items-center px-3 py-1 text-sm font-medium rounded-full border bg-ews-primary/10 text-ews-primary border-ews-primary/20", children: [specialty.label, jsxRuntime.jsx("button", { type: "button", onClick: () => handleRemoveSpecialty(specialty), className: "ml-2 text-ews-primary/60 hover:text-ews-primary", disabled: disabled, title: removeSpecialtyLabel, "aria-label": removeSpecialtyLabel, children: jsxRuntime.jsx(X, { className: "w-3 h-3" }) })] })), renderListItem: (specialty, isSelected) => (jsxRuntime.jsxs("div", { className: "flex items-center space-x-3", children: [jsxRuntime.jsx("div", { className: cn("flex justify-center items-center w-5 h-5 rounded border-2", isSelected
35294
35335
  ? "bg-ews-primary border-ews-primary"
35295
35336
  : "border-ews-gray-300"), children: isSelected && (jsxRuntime.jsx("svg", { className: "w-3 h-3 text-white", fill: "currentColor", viewBox: "0 0 20 20", children: jsxRuntime.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" }) })) }), jsxRuntime.jsx("span", { className: cn("font-medium", isSelected ? "text-ews-primary" : "text-gray-900"), children: specialty.label })] })) }), showSelectedCount && selectedSpecialties.length > 0 && (jsxRuntime.jsxs("div", { className: "flex justify-between items-center text-sm text-gray-600", children: [jsxRuntime.jsx("span", { children: getSelectedCountText
35296
35337
  ? getSelectedCountText(selectedSpecialties.length)