@ews-admin/global-design-system 1.1.26 → 1.5.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
@@ -60,6 +60,76 @@ function createEnvConfig(env, overrides) {
60
60
  };
61
61
  }
62
62
 
63
+ /**
64
+ * Available country codes supported by the platform
65
+ */
66
+ const COUNTRY_CODES = [
67
+ { code: "+221", country: "SN" }, // Senegal
68
+ { code: "+235", country: "TD" }, // Chad
69
+ ];
70
+ /**
71
+ * Determines the default country code based on phone number prefix or country ISO code.
72
+ * Falls back to "+221" (Senegal) when no match is found.
73
+ */
74
+ function getDefaultCountryCode(phoneNumber, country) {
75
+ if (phoneNumber) {
76
+ const match = COUNTRY_CODES.find(({ code }) => phoneNumber.startsWith(code));
77
+ if (match)
78
+ return match.code;
79
+ }
80
+ if (country) {
81
+ const match = COUNTRY_CODES.find(({ country: c }) => c.toUpperCase() === country.toUpperCase());
82
+ if (match)
83
+ return match.code;
84
+ }
85
+ return "+221";
86
+ }
87
+ /**
88
+ * Extracts the country code prefix from a phone number and returns both
89
+ * the detected country code and the local number without the prefix.
90
+ *
91
+ * @example
92
+ * extractCountryCodeFromPhoneNumber("+22177123456") // { countryCode: "+221", cleanedPhoneNumber: "77123456" }
93
+ * extractCountryCodeFromPhoneNumber("77123456") // { countryCode: "+221", cleanedPhoneNumber: "77123456" }
94
+ */
95
+ function extractCountryCodeFromPhoneNumber(phoneNumber, country) {
96
+ const matched = COUNTRY_CODES.find(({ code }) => phoneNumber.startsWith(code));
97
+ if (matched) {
98
+ const cleaned = phoneNumber
99
+ .replace(new RegExp(`^\\${matched.code}\\s*`), "")
100
+ .trim();
101
+ return { countryCode: matched.code, cleanedPhoneNumber: cleaned };
102
+ }
103
+ return {
104
+ countryCode: getDefaultCountryCode(phoneNumber, country),
105
+ cleanedPhoneNumber: phoneNumber.trim(),
106
+ };
107
+ }
108
+ /**
109
+ * Formats a phone number by prepending the country code if not already present.
110
+ * If the number already starts with a known country code it is returned as-is.
111
+ * Leading zeros, spaces and dashes are stripped from the local part before formatting.
112
+ *
113
+ * @example
114
+ * formatPhoneNumberWithCountryCode("77123456", "+221") // "+22177123456"
115
+ * formatPhoneNumberWithCountryCode("+22177123456", "+221") // "+22177123456" (unchanged)
116
+ * formatPhoneNumberWithCountryCode("077123456", "+221") // "+22177123456" (leading 0 stripped)
117
+ */
118
+ function formatPhoneNumberWithCountryCode(phoneNumber, countryCode = "+221") {
119
+ const trimmed = phoneNumber?.trim() || "";
120
+ if (!trimmed)
121
+ return "";
122
+ // Already has a known country code — return as-is
123
+ if (COUNTRY_CODES.some(({ code }) => trimmed.startsWith(code))) {
124
+ return trimmed;
125
+ }
126
+ // Strip leading zeros, spaces, dashes and + signs from the local part
127
+ const cleaned = trimmed.replace(/^[\s+0-]*/, "").trim();
128
+ if (!cleaned)
129
+ return "";
130
+ return `${countryCode}${cleaned}`;
131
+ }
132
+
63
133
  /**
64
134
  * Default currency for price formatting
65
135
  */
@@ -32851,29 +32921,23 @@ var isWeb = typeof window !== 'undefined' &&
32851
32921
  typeof document !== 'undefined';
32852
32922
 
32853
32923
  function cloneObject(data) {
32854
- let copy;
32855
- const isArray = Array.isArray(data);
32856
- const isFileListInstance = typeof FileList !== 'undefined' ? data instanceof FileList : false;
32857
32924
  if (data instanceof Date) {
32858
- copy = new Date(data);
32925
+ return new Date(data);
32859
32926
  }
32860
- else if (!(isWeb && (data instanceof Blob || isFileListInstance)) &&
32861
- (isArray || isObject(data))) {
32862
- copy = isArray ? [] : Object.create(Object.getPrototypeOf(data));
32863
- if (!isArray && !isPlainObject(data)) {
32864
- copy = data;
32865
- }
32866
- else {
32867
- for (const key in data) {
32868
- if (data.hasOwnProperty(key)) {
32869
- copy[key] = cloneObject(data[key]);
32870
- }
32871
- }
32872
- }
32927
+ const isFileListInstance = typeof FileList !== 'undefined' && data instanceof FileList;
32928
+ if (isWeb && (data instanceof Blob || isFileListInstance)) {
32929
+ return data;
32873
32930
  }
32874
- else {
32931
+ const isArray = Array.isArray(data);
32932
+ if (!isArray && !(isObject(data) && isPlainObject(data))) {
32875
32933
  return data;
32876
32934
  }
32935
+ const copy = isArray ? [] : Object.create(Object.getPrototypeOf(data));
32936
+ for (const key in data) {
32937
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
32938
+ copy[key] = cloneObject(data[key]);
32939
+ }
32940
+ }
32877
32941
  return copy;
32878
32942
  }
32879
32943
 
@@ -32899,6 +32963,8 @@ var get = (object, path, defaultValue) => {
32899
32963
 
32900
32964
  var isBoolean = (value) => typeof value === 'boolean';
32901
32965
 
32966
+ var isFunction = (value) => typeof value === 'function';
32967
+
32902
32968
  var set = (object, path, value) => {
32903
32969
  let index = -1;
32904
32970
  const tempPath = isKey(path) ? [path] : stringToPath(path);
@@ -32926,45 +32992,21 @@ var set = (object, path, value) => {
32926
32992
 
32927
32993
  const EVENTS = {
32928
32994
  BLUR: 'blur',
32929
- CHANGE: 'change',
32930
- };
32995
+ CHANGE: 'change'};
32931
32996
  const VALIDATION_MODE = {
32932
32997
  all: 'all',
32933
32998
  };
32934
32999
 
32935
- const HookFormContext = React.createContext(null);
32936
- HookFormContext.displayName = 'HookFormContext';
32937
33000
  /**
32938
- * This custom hook allows you to access the form context. useFormContext is intended to be used in deeply nested structures, where it would become inconvenient to pass the context as a prop. To be used with {@link FormProvider}.
32939
- *
32940
- * @remarks
32941
- * [API](https://react-hook-form.com/docs/useformcontext) • [Demo](https://codesandbox.io/s/react-hook-form-v7-form-context-ytudi)
32942
- *
32943
- * @returns return all useForm methods
32944
- *
32945
- * @example
32946
- * ```tsx
32947
- * function App() {
32948
- * const methods = useForm();
32949
- * const onSubmit = data => console.log(data);
32950
- *
32951
- * return (
32952
- * <FormProvider {...methods} >
32953
- * <form onSubmit={methods.handleSubmit(onSubmit)}>
32954
- * <NestedInput />
32955
- * <input type="submit" />
32956
- * </form>
32957
- * </FormProvider>
32958
- * );
32959
- * }
32960
- *
32961
- * function NestedInput() {
32962
- * const { register } = useFormContext(); // retrieve all hook methods
32963
- * return <input {...register("test")} />;
32964
- * }
32965
- * ```
33001
+ * Separate context for `control` to prevent unnecessary rerenders.
33002
+ * Internal hooks that only need control use this instead of full form context.
33003
+ */
33004
+ const HookFormControlContext = React.createContext(null);
33005
+ HookFormControlContext.displayName = 'HookFormControlContext';
33006
+ /**
33007
+ * @internal Internal hook to access only control from context.
32966
33008
  */
32967
- const useFormContext = () => React.useContext(HookFormContext);
33009
+ const useFormControlContext = () => React.useContext(HookFormControlContext);
32968
33010
 
32969
33011
  var getProxyFormState = (formState, control, localProxyFormState, isRoot = true) => {
32970
33012
  const result = {
@@ -33018,8 +33060,8 @@ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayou
33018
33060
  * ```
33019
33061
  */
33020
33062
  function useFormState(props) {
33021
- const methods = useFormContext();
33022
- const { control = methods.control, disabled, name, exact } = props || {};
33063
+ const formControl = useFormControlContext();
33064
+ const { control = formControl, disabled, name, exact } = props || {};
33023
33065
  const [formState, updateFormState] = React.useState(control._formState);
33024
33066
  const _localProxyFormState = React.useRef({
33025
33067
  isDirty: false,
@@ -33065,10 +33107,10 @@ var isPrimitive = (value) => isNullOrUndefined(value) || !isObjectType(value);
33065
33107
 
33066
33108
  function deepEqual(object1, object2, _internal_visited = new WeakSet()) {
33067
33109
  if (isPrimitive(object1) || isPrimitive(object2)) {
33068
- return object1 === object2;
33110
+ return Object.is(object1, object2);
33069
33111
  }
33070
33112
  if (isDateObject(object1) && isDateObject(object2)) {
33071
- return object1.getTime() === object2.getTime();
33113
+ return Object.is(object1.getTime(), object2.getTime());
33072
33114
  }
33073
33115
  const keys1 = Object.keys(object1);
33074
33116
  const keys2 = Object.keys(object2);
@@ -33091,7 +33133,7 @@ function deepEqual(object1, object2, _internal_visited = new WeakSet()) {
33091
33133
  (isObject(val1) && isObject(val2)) ||
33092
33134
  (Array.isArray(val1) && Array.isArray(val2))
33093
33135
  ? !deepEqual(val1, val2, _internal_visited)
33094
- : val1 !== val2) {
33136
+ : !Object.is(val1, val2)) {
33095
33137
  return false;
33096
33138
  }
33097
33139
  }
@@ -33116,8 +33158,8 @@ function deepEqual(object1, object2, _internal_visited = new WeakSet()) {
33116
33158
  * ```
33117
33159
  */
33118
33160
  function useWatch(props) {
33119
- const methods = useFormContext();
33120
- const { control = methods.control, name, defaultValue, disabled, exact, compute, } = props || {};
33161
+ const formControl = useFormControlContext();
33162
+ const { control = formControl, name, defaultValue, disabled, exact, compute, } = props || {};
33121
33163
  const _defaultValue = React.useRef(defaultValue);
33122
33164
  const _compute = React.useRef(compute);
33123
33165
  const _computeFormValues = React.useRef(undefined);
@@ -33210,20 +33252,20 @@ function useWatch(props) {
33210
33252
  * ```
33211
33253
  */
33212
33254
  function useController(props) {
33213
- const methods = useFormContext();
33214
- const { name, disabled, control = methods.control, shouldUnregister, defaultValue, } = props;
33255
+ const formControl = useFormControlContext();
33256
+ const { name, disabled, control = formControl, shouldUnregister, defaultValue, exact = true, } = props;
33215
33257
  const isArrayField = isNameInFieldArray(control._names.array, name);
33216
33258
  const defaultValueMemo = React.useMemo(() => get(control._formValues, name, get(control._defaultValues, name, defaultValue)), [control, name, defaultValue]);
33217
33259
  const value = useWatch({
33218
33260
  control,
33219
33261
  name,
33220
33262
  defaultValue: defaultValueMemo,
33221
- exact: true,
33263
+ exact,
33222
33264
  });
33223
33265
  const formState = useFormState({
33224
33266
  control,
33225
33267
  name,
33226
- exact: true,
33268
+ exact,
33227
33269
  });
33228
33270
  const _props = React.useRef(props);
33229
33271
  const _previousNameRef = React.useRef(undefined);
@@ -33271,12 +33313,12 @@ function useController(props) {
33271
33313
  }), [name, control._formValues]);
33272
33314
  const ref = React.useCallback((elm) => {
33273
33315
  const field = get(control._fields, name);
33274
- if (field && elm) {
33316
+ if (field && field._f && elm) {
33275
33317
  field._f.ref = {
33276
- focus: () => elm.focus && elm.focus(),
33277
- select: () => elm.select && elm.select(),
33278
- setCustomValidity: (message) => elm.setCustomValidity(message),
33279
- reportValidity: () => elm.reportValidity(),
33318
+ focus: () => isFunction(elm.focus) && elm.focus(),
33319
+ select: () => isFunction(elm.select) && elm.select(),
33320
+ setCustomValidity: (message) => isFunction(elm.setCustomValidity) && elm.setCustomValidity(message),
33321
+ reportValidity: () => isFunction(elm.reportValidity) && elm.reportValidity(),
33280
33322
  };
33281
33323
  }
33282
33324
  }, [control._fields, name]);
@@ -33383,6 +33425,9 @@ function useController(props) {
33383
33425
  */
33384
33426
  const Controller = (props) => props.render(useController(props));
33385
33427
 
33428
+ const HookFormContext = React.createContext(null);
33429
+ HookFormContext.displayName = 'HookFormContext';
33430
+
33386
33431
  function useSelectField({ name, control, options: _options, rules, defaultValue, }) {
33387
33432
  const { field, fieldState: { error, invalid }, } = useController({
33388
33433
  name,
@@ -33872,6 +33917,91 @@ const DropdownMultiSelect = ({ options, name, control, placeholder = "Select opt
33872
33917
  }), error && jsxRuntime.jsx("p", { className: "mt-1 text-sm text-ews-error", children: error })] }));
33873
33918
  };
33874
33919
 
33920
+ const DEFAULT_COUNTRY_CODE = "+221";
33921
+ /**
33922
+ * Phone number input with integrated country code selector.
33923
+ *
33924
+ * The component stores the **full** phone number (e.g. "+22177123456") as its
33925
+ * form value so that the country code prefix is always preserved on save.
33926
+ * The text field displays only the local part ("77123456") for a clean UX.
33927
+ *
33928
+ * Works with React Hook Form via `Controller` — no extra setup needed.
33929
+ */
33930
+ const PhoneInput = React.forwardRef(({ countryCode, onCountryCodeChange, onChange, value, ...props }, ref) => {
33931
+ // internalCode tracks user's explicit dropdown selection when no countryCode
33932
+ // prop is provided and value doesn't carry a recognised prefix.
33933
+ const [internalCode, setInternalCode] = React.useState(DEFAULT_COUNTRY_CODE);
33934
+ const nativeRef = React.useRef(null);
33935
+ // Derive the country code directly from value on every render — no state
33936
+ // sync needed. Falls back to internalCode when value is empty or unrecognised.
33937
+ const codeFromValue = value
33938
+ ? COUNTRY_CODES.find(({ code }) => value.startsWith(code))?.code ?? null
33939
+ : null;
33940
+ const activeCode = countryCode ?? codeFromValue ?? internalCode;
33941
+ const handleCodeChange = (newCode) => {
33942
+ // Update internal or external country code state.
33943
+ if (onCountryCodeChange) {
33944
+ onCountryCodeChange(newCode);
33945
+ }
33946
+ else {
33947
+ setInternalCode(newCode);
33948
+ }
33949
+ // Re-emit the current phone value with the new country code so that
33950
+ // RHF (Controller) and any other onChange listeners stay in sync even
33951
+ // when the user changes the dropdown without retyping the number.
33952
+ const el = nativeRef.current;
33953
+ if (el && onChange) {
33954
+ const numeric = formatNumeric(el.value);
33955
+ const fullValue = numeric ? `${newCode}${numeric}` : "";
33956
+ // Pass the value string directly — RHF's Controller field.onChange
33957
+ // accepts raw values, avoiding unreliable fake-event parsing.
33958
+ onChange(fullValue);
33959
+ }
33960
+ };
33961
+ const toLocalPart = (v) => extractCountryCodeFromPhoneNumber(v || "").cleanedPhoneNumber;
33962
+ // When value prop changes (Controller / direct value prop usage),
33963
+ // update the DOM input directly so the display stays in sync.
33964
+ React.useEffect(() => {
33965
+ const el = nativeRef.current;
33966
+ if (el && value !== undefined) {
33967
+ el.value = toLocalPart(value);
33968
+ }
33969
+ }, [value]);
33970
+ const handleChange = (e) => {
33971
+ // Enforce digits-only in the displayed field.
33972
+ const numeric = formatNumeric(e.target.value);
33973
+ if (e.target.value !== numeric) {
33974
+ e.target.value = numeric;
33975
+ }
33976
+ // Always emit the full phone number (country code + local part) to the form.
33977
+ const fullValue = numeric ? `${activeCode}${numeric}` : "";
33978
+ const syntheticEvent = {
33979
+ ...e,
33980
+ target: { ...e.target, value: fullValue, name: e.target.name },
33981
+ };
33982
+ onChange?.(syntheticEvent);
33983
+ };
33984
+ // Merge the forwarded ref (used by RHF register) with our internal ref.
33985
+ const composedRef = React.useCallback((node) => {
33986
+ nativeRef.current = node;
33987
+ if (typeof ref === "function")
33988
+ ref(node);
33989
+ else if (ref)
33990
+ ref.current = node;
33991
+ }, [ref]);
33992
+ // Initial display: strip country code so only the local part is shown.
33993
+ const initialDisplay = value !== undefined ? toLocalPart(value) : undefined;
33994
+ return (jsxRuntime.jsx(Input, { ref: composedRef, type: "tel",
33995
+ // Use defaultValue (uncontrolled) so React never resets what the user types.
33996
+ // Updates from the value prop are applied imperatively via the useEffect above.
33997
+ defaultValue: initialDisplay, countryCodeSelect: {
33998
+ options: COUNTRY_CODES,
33999
+ value: activeCode,
34000
+ onChange: handleCodeChange,
34001
+ }, onChange: handleChange, ...props }));
34002
+ });
34003
+ PhoneInput.displayName = "PhoneInput";
34004
+
33875
34005
  const Logo = ({ size = "md", showTagline: _showTagline = true, iconOnly = false, variant = "normal", className, onClick, customSrc, alt = "MEDECINE 360 Logo", clickable = false, }) => {
33876
34006
  const sizes = {
33877
34007
  sm: "h-8",
@@ -34661,6 +34791,7 @@ exports.BusFront = BusFront;
34661
34791
  exports.BusFrontIcon = BusFront;
34662
34792
  exports.BusIcon = Bus;
34663
34793
  exports.Button = Button;
34794
+ exports.COUNTRY_CODES = COUNTRY_CODES;
34664
34795
  exports.Cable = Cable;
34665
34796
  exports.CableCar = CableCar;
34666
34797
  exports.CableCarIcon = CableCar;
@@ -38486,6 +38617,7 @@ exports.PhoneForwardedIcon = PhoneForwarded;
38486
38617
  exports.PhoneIcon = Phone;
38487
38618
  exports.PhoneIncoming = PhoneIncoming;
38488
38619
  exports.PhoneIncomingIcon = PhoneIncoming;
38620
+ exports.PhoneInput = PhoneInput;
38489
38621
  exports.PhoneMissed = PhoneMissed;
38490
38622
  exports.PhoneMissedIcon = PhoneMissed;
38491
38623
  exports.PhoneOff = PhoneOff;
@@ -39679,10 +39811,13 @@ exports.cn = cn;
39679
39811
  exports.createEnvConfig = createEnvConfig;
39680
39812
  exports.createLucideIcon = createLucideIcon;
39681
39813
  exports.debounce = debounce;
39814
+ exports.extractCountryCodeFromPhoneNumber = extractCountryCodeFromPhoneNumber;
39682
39815
  exports.formatCurrency = formatCurrency;
39683
39816
  exports.formatDate = formatDate;
39684
39817
  exports.formatNumeric = formatNumeric;
39818
+ exports.formatPhoneNumberWithCountryCode = formatPhoneNumberWithCountryCode;
39685
39819
  exports.generateId = generateId;
39820
+ exports.getDefaultCountryCode = getDefaultCountryCode;
39686
39821
  exports.icons = index;
39687
39822
  exports.isValidPhoneNumber = isValidPhoneNumber;
39688
39823
  exports.useDebounce = useDebounce;