@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.
@@ -1,5 +1,7 @@
1
1
  import { type ClassValue } from "clsx";
2
2
  export { createEnvConfig } from "./env-config";
3
+ export { COUNTRY_CODES, extractCountryCodeFromPhoneNumber, formatPhoneNumberWithCountryCode, getDefaultCountryCode, } from "./phone";
4
+ export type { CountryCodeOption, CountryCodeSelectProps } from "./phone";
3
5
  export type { EnvConfig, EnvConfigOverrides, Environment, } from "./env-config";
4
6
  /**
5
7
  * Default currency for price formatting
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAChE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,oBAAY,SAAS;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,WAAW,QAAQ;IACnB,WAAW,QAAQ;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,OAAO,YAAY;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,EAUlC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,iCAAiC,EACjC,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AACzE,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAChE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,oBAAY,SAAS;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,WAAW,QAAQ;IACnB,WAAW,QAAQ;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,OAAO,YAAY;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,EAUlC,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Country code option for phone number selection
3
+ */
4
+ export interface CountryCodeOption {
5
+ code: string;
6
+ country: string;
7
+ }
8
+ /**
9
+ * Props for country code select component
10
+ */
11
+ export interface CountryCodeSelectProps {
12
+ options: CountryCodeOption[];
13
+ value: string;
14
+ onChange: (code: string) => void;
15
+ }
16
+ /**
17
+ * Available country codes supported by the platform
18
+ */
19
+ export declare const COUNTRY_CODES: CountryCodeOption[];
20
+ /**
21
+ * Determines the default country code based on phone number prefix or country ISO code.
22
+ * Falls back to "+221" (Senegal) when no match is found.
23
+ */
24
+ export declare function getDefaultCountryCode(phoneNumber?: string, country?: string): string;
25
+ /**
26
+ * Extracts the country code prefix from a phone number and returns both
27
+ * the detected country code and the local number without the prefix.
28
+ *
29
+ * @example
30
+ * extractCountryCodeFromPhoneNumber("+22177123456") // { countryCode: "+221", cleanedPhoneNumber: "77123456" }
31
+ * extractCountryCodeFromPhoneNumber("77123456") // { countryCode: "+221", cleanedPhoneNumber: "77123456" }
32
+ */
33
+ export declare function extractCountryCodeFromPhoneNumber(phoneNumber: string, country?: string): {
34
+ countryCode: string;
35
+ cleanedPhoneNumber: string;
36
+ };
37
+ /**
38
+ * Formats a phone number by prepending the country code if not already present.
39
+ * If the number already starts with a known country code it is returned as-is.
40
+ * Leading zeros, spaces and dashes are stripped from the local part before formatting.
41
+ *
42
+ * @example
43
+ * formatPhoneNumberWithCountryCode("77123456", "+221") // "+22177123456"
44
+ * formatPhoneNumberWithCountryCode("+22177123456", "+221") // "+22177123456" (unchanged)
45
+ * formatPhoneNumberWithCountryCode("077123456", "+221") // "+22177123456" (leading 0 stripped)
46
+ */
47
+ export declare function formatPhoneNumberWithCountryCode(phoneNumber: string, countryCode?: string): string;
48
+ //# sourceMappingURL=phone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phone.d.ts","sourceRoot":"","sources":["../../src/utils/phone.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,iBAAiB,EAG5C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CAgBR;AAED;;;;;;;GAOG;AACH,wBAAgB,iCAAiC,CAC/C,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,MAAM,GACf;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,CAgBrD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,MAAM,EACnB,WAAW,GAAE,MAAe,GAC3B,MAAM,CAgBR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ews-admin/global-design-system",
3
- "version": "1.1.26",
3
+ "version": "1.5.0",
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",
@@ -0,0 +1,127 @@
1
+ import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
2
+ import {
3
+ COUNTRY_CODES,
4
+ extractCountryCodeFromPhoneNumber,
5
+ } from "../../utils/phone";
6
+ import { formatNumeric } from "../../utils";
7
+ import { Input } from "../Input/Input";
8
+ import type { InputProps } from "../Input/Input";
9
+
10
+ const DEFAULT_COUNTRY_CODE = "+221";
11
+
12
+ export interface PhoneInputProps
13
+ extends Omit<InputProps, "countryCodeSelect" | "leftIcon" | "type"> {
14
+ /** Currently selected country code (controlled). Defaults to +221 if omitted. */
15
+ countryCode?: string;
16
+ /** Called when the user changes the country code dropdown. */
17
+ onCountryCodeChange?: (code: string) => void;
18
+ }
19
+
20
+ /**
21
+ * Phone number input with integrated country code selector.
22
+ *
23
+ * The component stores the **full** phone number (e.g. "+22177123456") as its
24
+ * form value so that the country code prefix is always preserved on save.
25
+ * The text field displays only the local part ("77123456") for a clean UX.
26
+ *
27
+ * Works with React Hook Form via `Controller` — no extra setup needed.
28
+ */
29
+ export const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
30
+ ({ countryCode, onCountryCodeChange, onChange, value, ...props }, ref) => {
31
+ // internalCode tracks user's explicit dropdown selection when no countryCode
32
+ // prop is provided and value doesn't carry a recognised prefix.
33
+ const [internalCode, setInternalCode] = useState(DEFAULT_COUNTRY_CODE);
34
+ const nativeRef = useRef<HTMLInputElement>(null);
35
+
36
+ // Derive the country code directly from value on every render — no state
37
+ // sync needed. Falls back to internalCode when value is empty or unrecognised.
38
+ const codeFromValue = (value as string)
39
+ ? COUNTRY_CODES.find(({ code }) =>
40
+ (value as string).startsWith(code)
41
+ )?.code ?? null
42
+ : null;
43
+
44
+ const activeCode = countryCode ?? codeFromValue ?? internalCode;
45
+
46
+ const handleCodeChange = (newCode: string) => {
47
+ // Update internal or external country code state.
48
+ if (onCountryCodeChange) {
49
+ onCountryCodeChange(newCode);
50
+ } else {
51
+ setInternalCode(newCode);
52
+ }
53
+ // Re-emit the current phone value with the new country code so that
54
+ // RHF (Controller) and any other onChange listeners stay in sync even
55
+ // when the user changes the dropdown without retyping the number.
56
+ const el = nativeRef.current;
57
+ if (el && onChange) {
58
+ const numeric = formatNumeric(el.value);
59
+ const fullValue = numeric ? `${newCode}${numeric}` : "";
60
+ // Pass the value string directly — RHF's Controller field.onChange
61
+ // accepts raw values, avoiding unreliable fake-event parsing.
62
+ (onChange as unknown as (value: string) => void)(fullValue);
63
+ }
64
+ };
65
+
66
+ const toLocalPart = (v: unknown) =>
67
+ extractCountryCodeFromPhoneNumber((v as string) || "").cleanedPhoneNumber;
68
+
69
+ // When value prop changes (Controller / direct value prop usage),
70
+ // update the DOM input directly so the display stays in sync.
71
+ useEffect(() => {
72
+ const el = nativeRef.current;
73
+ if (el && value !== undefined) {
74
+ el.value = toLocalPart(value);
75
+ }
76
+ }, [value]);
77
+
78
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
79
+ // Enforce digits-only in the displayed field.
80
+ const numeric = formatNumeric(e.target.value);
81
+ if (e.target.value !== numeric) {
82
+ e.target.value = numeric;
83
+ }
84
+ // Always emit the full phone number (country code + local part) to the form.
85
+ const fullValue = numeric ? `${activeCode}${numeric}` : "";
86
+ const syntheticEvent = {
87
+ ...e,
88
+ target: { ...e.target, value: fullValue, name: e.target.name },
89
+ } as React.ChangeEvent<HTMLInputElement>;
90
+ onChange?.(syntheticEvent);
91
+ };
92
+
93
+ // Merge the forwarded ref (used by RHF register) with our internal ref.
94
+ const composedRef = useCallback(
95
+ (node: HTMLInputElement | null) => {
96
+ (nativeRef as MutableRefObject<HTMLInputElement | null>).current = node;
97
+ if (typeof ref === "function") ref(node);
98
+ else if (ref)
99
+ (ref as MutableRefObject<HTMLInputElement | null>).current = node;
100
+ },
101
+ [ref]
102
+ );
103
+
104
+ // Initial display: strip country code so only the local part is shown.
105
+ const initialDisplay =
106
+ value !== undefined ? toLocalPart(value) : undefined;
107
+
108
+ return (
109
+ <Input
110
+ ref={composedRef}
111
+ type="tel"
112
+ // Use defaultValue (uncontrolled) so React never resets what the user types.
113
+ // Updates from the value prop are applied imperatively via the useEffect above.
114
+ defaultValue={initialDisplay}
115
+ countryCodeSelect={{
116
+ options: COUNTRY_CODES,
117
+ value: activeCode,
118
+ onChange: handleCodeChange,
119
+ }}
120
+ onChange={handleChange}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+ );
126
+
127
+ PhoneInput.displayName = "PhoneInput";
@@ -0,0 +1,2 @@
1
+ export { PhoneInput } from "./PhoneInput";
2
+ export type { PhoneInputProps } from "./PhoneInput";
package/src/index.ts CHANGED
@@ -22,6 +22,9 @@ export type { ProfileImageUploadProps } from "./components/ProfileImageUpload";
22
22
  export { DropdownMultiSelect } from "./components/DropdownMultiSelect";
23
23
  export type { DropdownMultiSelectProps } from "./components/DropdownMultiSelect";
24
24
 
25
+ export { PhoneInput } from "./components/PhoneInput";
26
+ export type { PhoneInputProps } from "./components/PhoneInput";
27
+
25
28
  export { Logo } from "./components/Logo";
26
29
  export type { LogoProps } from "./components/Logo";
27
30
 
@@ -44,15 +47,25 @@ export {
44
47
  BLOOD_TYPES,
45
48
  BloodType,
46
49
  cn,
50
+ COUNTRY_CODES,
47
51
  createEnvConfig,
48
52
  debounce,
53
+ extractCountryCodeFromPhoneNumber,
49
54
  formatCurrency,
50
55
  formatDate,
51
56
  formatNumeric,
57
+ formatPhoneNumberWithCountryCode,
52
58
  generateId,
59
+ getDefaultCountryCode,
53
60
  isValidPhoneNumber,
54
61
  } from "./utils";
55
- export type { EnvConfig, EnvConfigOverrides, Environment } from "./utils";
62
+ export type {
63
+ CountryCodeOption,
64
+ CountryCodeSelectProps,
65
+ EnvConfig,
66
+ EnvConfigOverrides,
67
+ Environment,
68
+ } from "./utils";
56
69
 
57
70
  // Hooks
58
71
  export { useDebounce, useDebouncedCallback, useSelectField } from "./hooks";
@@ -1,6 +1,13 @@
1
1
  import { clsx, type ClassValue } from "clsx";
2
2
 
3
3
  export { createEnvConfig } from "./env-config";
4
+ export {
5
+ COUNTRY_CODES,
6
+ extractCountryCodeFromPhoneNumber,
7
+ formatPhoneNumberWithCountryCode,
8
+ getDefaultCountryCode,
9
+ } from "./phone";
10
+ export type { CountryCodeOption, CountryCodeSelectProps } from "./phone";
4
11
  export type {
5
12
  EnvConfig,
6
13
  EnvConfigOverrides,
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Country code option for phone number selection
3
+ */
4
+ export interface CountryCodeOption {
5
+ code: string;
6
+ country: string;
7
+ }
8
+
9
+ /**
10
+ * Props for country code select component
11
+ */
12
+ export interface CountryCodeSelectProps {
13
+ options: CountryCodeOption[];
14
+ value: string;
15
+ onChange: (code: string) => void;
16
+ }
17
+
18
+ /**
19
+ * Available country codes supported by the platform
20
+ */
21
+ export const COUNTRY_CODES: CountryCodeOption[] = [
22
+ { code: "+221", country: "SN" }, // Senegal
23
+ { code: "+235", country: "TD" }, // Chad
24
+ ];
25
+
26
+ /**
27
+ * Determines the default country code based on phone number prefix or country ISO code.
28
+ * Falls back to "+221" (Senegal) when no match is found.
29
+ */
30
+ export function getDefaultCountryCode(
31
+ phoneNumber?: string,
32
+ country?: string
33
+ ): string {
34
+ if (phoneNumber) {
35
+ const match = COUNTRY_CODES.find(({ code }) =>
36
+ phoneNumber.startsWith(code)
37
+ );
38
+ if (match) return match.code;
39
+ }
40
+
41
+ if (country) {
42
+ const match = COUNTRY_CODES.find(
43
+ ({ country: c }) => c.toUpperCase() === country.toUpperCase()
44
+ );
45
+ if (match) return match.code;
46
+ }
47
+
48
+ return "+221";
49
+ }
50
+
51
+ /**
52
+ * Extracts the country code prefix from a phone number and returns both
53
+ * the detected country code and the local number without the prefix.
54
+ *
55
+ * @example
56
+ * extractCountryCodeFromPhoneNumber("+22177123456") // { countryCode: "+221", cleanedPhoneNumber: "77123456" }
57
+ * extractCountryCodeFromPhoneNumber("77123456") // { countryCode: "+221", cleanedPhoneNumber: "77123456" }
58
+ */
59
+ export function extractCountryCodeFromPhoneNumber(
60
+ phoneNumber: string,
61
+ country?: string
62
+ ): { countryCode: string; cleanedPhoneNumber: string } {
63
+ const matched = COUNTRY_CODES.find(({ code }) =>
64
+ phoneNumber.startsWith(code)
65
+ );
66
+
67
+ if (matched) {
68
+ const cleaned = phoneNumber
69
+ .replace(new RegExp(`^\\${matched.code}\\s*`), "")
70
+ .trim();
71
+ return { countryCode: matched.code, cleanedPhoneNumber: cleaned };
72
+ }
73
+
74
+ return {
75
+ countryCode: getDefaultCountryCode(phoneNumber, country),
76
+ cleanedPhoneNumber: phoneNumber.trim(),
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Formats a phone number by prepending the country code if not already present.
82
+ * If the number already starts with a known country code it is returned as-is.
83
+ * Leading zeros, spaces and dashes are stripped from the local part before formatting.
84
+ *
85
+ * @example
86
+ * formatPhoneNumberWithCountryCode("77123456", "+221") // "+22177123456"
87
+ * formatPhoneNumberWithCountryCode("+22177123456", "+221") // "+22177123456" (unchanged)
88
+ * formatPhoneNumberWithCountryCode("077123456", "+221") // "+22177123456" (leading 0 stripped)
89
+ */
90
+ export function formatPhoneNumberWithCountryCode(
91
+ phoneNumber: string,
92
+ countryCode: string = "+221"
93
+ ): string {
94
+ const trimmed = phoneNumber?.trim() || "";
95
+
96
+ if (!trimmed) return "";
97
+
98
+ // Already has a known country code — return as-is
99
+ if (COUNTRY_CODES.some(({ code }) => trimmed.startsWith(code))) {
100
+ return trimmed;
101
+ }
102
+
103
+ // Strip leading zeros, spaces, dashes and + signs from the local part
104
+ const cleaned = trimmed.replace(/^[\s+0-]*/, "").trim();
105
+
106
+ if (!cleaned) return "";
107
+
108
+ return `${countryCode}${cleaned}`;
109
+ }