nexus-shared 1.1.17 → 1.1.19

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 (58) hide show
  1. package/package.json +1 -1
  2. package/src/api-services/system-service.tsx +1 -2
  3. package/src/client.ts +3 -2
  4. package/src/components/documents/button.tsx +5 -1
  5. package/src/components/inputs/input-box.tsx +4 -2
  6. package/src/components/inputs/input-form.tsx +126 -14
  7. package/src/components/layouts/global-layout.tsx +4 -2
  8. package/src/components/layouts/layout-helpers.tsx +23 -23
  9. package/src/helpers/browser-helpers.tsx +41 -1
  10. package/src/helpers/input-helpers.tsx +34 -4
  11. package/src/helpers/utility-helpers.tsx +1 -0
  12. package/src/helpers/validation-helpers.tsx +302 -0
  13. package/src/index.ts +3 -18
  14. package/src/interface.ts +4 -1
  15. package/src/interfaces/button-interfaces.tsx +2 -2
  16. package/src/interfaces/exception-interfaces.tsx +99 -0
  17. package/src/interfaces/http-interfaces.tsx +120 -3
  18. package/src/interfaces/input-interfaces.tsx +61 -11
  19. package/src/interfaces/message-interfaces.tsx +32 -0
  20. package/src/interfaces/permission-interfaces.tsx +4 -4
  21. package/src/interfaces/storage-interfaces.tsx +1 -0
  22. package/src/interfaces/system-interfaces.tsx +2 -2
  23. package/src/interfaces/type-interfaces.tsx +8 -2
  24. package/src/interfaces/user-interfaces.tsx +17 -0
  25. package/src/nexus.environments.tsx +27 -17
  26. package/src/proxy-api/proxy-backend.tsx +60 -0
  27. package/src/proxy-api/proxy-constants.tsx +28 -0
  28. package/src/proxy-api/proxy-helpers.tsx +91 -0
  29. package/src/proxy-api-client.tsx +1 -0
  30. package/src/proxy-api-server.tsx +2 -0
  31. package/src/services/http-client-services.tsx +75 -0
  32. package/src/services/http-services.tsx +158 -0
  33. package/src/services/loader-service.tsx +5 -4
  34. package/src/services/localstorage-service.tsx +59 -0
  35. package/src/services/message-services.tsx +383 -0
  36. package/src/sso-config/auth-token-validation.ts +20 -0
  37. package/src/sso-config/callback-route.tsx +34 -0
  38. package/src/sso-config/cookie-encryption.ts +51 -0
  39. package/src/sso-config/cookie-helpers.tsx +77 -0
  40. package/src/sso-config/forgot-password-route.tsx +36 -0
  41. package/src/sso-config/oauth-callback-state.ts +58 -0
  42. package/src/sso-config/pkce-helpers.ts +16 -0
  43. package/src/sso-config/provider-complete-route.tsx +15 -0
  44. package/src/sso-config/redirect-context-actions.tsx +46 -0
  45. package/src/sso-config/redirect-context-helpers.tsx +144 -0
  46. package/src/sso-config/redirect-context-persist.tsx +18 -0
  47. package/src/sso-config/redirect-context-route.tsx +45 -0
  48. package/src/sso-config/refresh-route.tsx +22 -0
  49. package/src/sso-config/sign-in-route.tsx +69 -0
  50. package/src/sso-config/sign-out-route.tsx +53 -0
  51. package/src/sso-config/sign-up-route.tsx +46 -0
  52. package/src/sso-config/sso-backend-flow.ts +45 -0
  53. package/src/sso-config/sso-complete-route.tsx +46 -0
  54. package/src/sso-config/sso-config.ts +57 -0
  55. package/src/sso-config/sso-interfaces.tsx +68 -0
  56. package/src/sso-config/sso-response-helpers.tsx +20 -0
  57. package/src/sso-server.tsx +21 -1
  58. package/src/styles/nexus.dialog.css +3 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Server-side HTTP utilities for Route Handlers, Server Components, and Server Actions.
3
+ * For browser/client components use `http-client-services` (`Post`, `Get`, …) via `nexus-shared/client`.
4
+ */
5
+ import { Delay } from "../helpers/utility-helpers";
6
+ import { EActions, EBody, EModules, GetMethod, IErrorRespose, ISuccessResponse, UN } from "../interface";
7
+ import { EErrorCodes, EExceptionCategories } from "../interfaces/exception-interfaces";
8
+ import { APIS, NEXUS_CONFIG, NEXUS_INFO } from "../nexus.environments";
9
+
10
+ const MAX_RETRIES = NEXUS_INFO.IS_PRODUCTION ? 2 : 1;
11
+ const RETRY_DELAY_MS = NEXUS_INFO.IS_PRODUCTION ? 50 : 1;
12
+
13
+ export const BuildApiHeader = (formdata?: any, httpMethod?: EActions | string, bodyType: EBody | UN = EBody.JSON, token?: string | UN, revalidate?: number | UN, browserId?: string | UN): RequestInit & { next?: { revalidate: number } } => {
14
+ httpMethod = httpMethod ?? "GET";
15
+ bodyType = bodyType ?? EBody.JSON;
16
+
17
+ const headers = new Headers();
18
+ if (token && token.length > 50) headers.append("authorization", `${token}`);
19
+ if (browserId) headers.append("bid", browserId);
20
+ headers.append("Content-Type", bodyType);
21
+
22
+ const options: RequestInit & { next?: { revalidate: number } } = {
23
+ method: typeof httpMethod === "string" ? httpMethod : GetMethod(httpMethod),
24
+ headers,
25
+ };
26
+
27
+ if (formdata) options.body = bodyType === EBody.JSON ? JSON.stringify(formdata) : formdata;
28
+
29
+ if (!revalidate || revalidate <= 0) {
30
+ // No caching
31
+ options.cache = "no-store";
32
+ } else {
33
+ // Use Next.js ISR caching
34
+ options.next = { revalidate: revalidate ?? 0 };
35
+ }
36
+
37
+ return options;
38
+ };
39
+ const shouldRetry = (error: any) => error.name === "AbortError" || error.code === "ECONNREFUSED" || error.message.includes("fetch") || error.message.includes("network");
40
+ const safeFetch = async (fetchFunc: () => Promise<Response>): Promise<ISuccessResponse | IErrorRespose> => {
41
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
42
+ try {
43
+ const response = await fetchFunc();
44
+ if (!response.ok) return await handleResponse(response);
45
+ return await response.json();
46
+ } catch (error: any) {
47
+ if (attempt < MAX_RETRIES - 1 && shouldRetry(error)) {
48
+ await Delay(RETRY_DELAY_MS * (attempt + 1));
49
+ continue;
50
+ }
51
+ return handleNetworkError(error);
52
+ }
53
+ }
54
+ return handleError({ status: 500, message: "Maximum retry attempts exceeded." });
55
+ };
56
+ const fetchWithTimeout = async (url: string, options?: RequestInit, timeout = NEXUS_CONFIG.DEFAULT_HTTP_REQUEST_TIMEOUT): Promise<Response> => {
57
+ const controller = new AbortController();
58
+ const id = setTimeout(() => controller.abort(), timeout);
59
+ try {
60
+ const response = await fetch(url, { ...options, signal: controller.signal });
61
+ return response;
62
+ } catch (error) {
63
+ throw error;
64
+ } finally {
65
+ clearTimeout(id);
66
+ }
67
+ };
68
+ export const HttpGet = async (apiUrl: string, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
69
+ return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(undefined, EActions.Get, EBody.JSON, token, revalidate, browserId)));
70
+ };
71
+ export const HttpPost = async (apiUrl: string, body?: any | UN, bodyType?: EBody | UN, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
72
+ return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(body, EActions.Post, bodyType, token, revalidate, browserId)));
73
+ };
74
+
75
+ export const HttpPut = async (apiUrl: string, body?: any | UN, bodyType?: EBody | UN, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
76
+ return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(body, EActions.Put, bodyType, token, revalidate, browserId)));
77
+ };
78
+
79
+ export const HttpDelete = async (apiUrl: string, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
80
+ return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(undefined, EActions.Delete, undefined, token, revalidate, browserId)));
81
+ };
82
+
83
+ export const HttpFetch = async (apiUrl: string, method: EActions | string, body?: any, bodyType?: EBody, token?: string | UN, revalidate?: number, browserId?: string | UN): Promise<Response | Error> => {
84
+ try {
85
+ const response = await fetch(apiUrl, BuildApiHeader(body, method, bodyType, token, revalidate, browserId));
86
+ return response;
87
+ } catch (error: Error | any) {
88
+ return error;
89
+ }
90
+ };
91
+
92
+ export const GetRootApi = (nexusModule: EModules) => {
93
+ switch (nexusModule) {
94
+ case EModules.None:
95
+ return "/api";
96
+ case EModules.Authentication:
97
+ return APIS.AUTHENTICATION;
98
+ case EModules.Configuration:
99
+ return APIS.CONFIGURATION;
100
+ case EModules.Enterprise:
101
+ return APIS.ENTERPRISE;
102
+ case EModules.Drive:
103
+ return APIS.DRIVE;
104
+ case EModules.Message:
105
+ return APIS.MESSAGE;
106
+ case EModules.Accounting:
107
+ return APIS.ACCOUNTING;
108
+ case EModules.Report:
109
+ return APIS.REPORT;
110
+ default:
111
+ throw new Error("Invalid Nexus Module");
112
+ }
113
+ };
114
+ const handleResponse = async (response: Response): Promise<IErrorRespose> => {
115
+ const status = response.status;
116
+ try {
117
+ const errorData = await response.json();
118
+ return handleError({ ...errorData, status });
119
+ } catch {
120
+ let message = "Unexpected server error.";
121
+ switch (status) {
122
+ case 401:
123
+ case 403:
124
+ message = "You do not have permission to access this resource.";
125
+ break;
126
+ case 404:
127
+ case 410:
128
+ message = "Resource not found.";
129
+ break;
130
+ case 405:
131
+ message = "Method not allowed.";
132
+ break;
133
+ }
134
+ return handleError({ status, message });
135
+ }
136
+ };
137
+ export const handleError = (error: any): IErrorRespose => {
138
+ return {
139
+ isSuccess: false,
140
+ code: error?.code,
141
+ message: error?.message || "An unknown error occurred.",
142
+ messageCode: error?.messageCode || "GENERIC_ERROR",
143
+ exceptionCategory: error?.exceptionCategory || EExceptionCategories.Application,
144
+ errorCode: error?.errorCode || EErrorCodes.Unknown,
145
+ inputName: error?.inputName || undefined,
146
+ exception: error?.exception || undefined,
147
+ result: error?.result || undefined,
148
+ };
149
+ };
150
+ const handleNetworkError = (error: any): IErrorRespose => {
151
+ console.error("\n\nNetwork error encountered:", error);
152
+ return handleError({
153
+ status: 503,
154
+ message: "Unable to connect to the server. Please check your network or backend status.",
155
+ key: "NETWORK_ERROR",
156
+ exception: error,
157
+ });
158
+ };
@@ -74,7 +74,7 @@ const _updateLoaderAttributes = (loaderElement: HTMLElement, type: EAnimationCla
74
74
  loaderElement.setAttribute(LOADER_DATA_SIZE_ATTRIBUTE, size);
75
75
  loaderElement.setAttribute(LOADER_DATA_DISABLED_INPUTS_ATTRIBUTE, disabledInputs ? "true" : "false");
76
76
  };
77
- const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size: ESizes, showLogo: boolean, disabledInputs: boolean, background: EBackgrounds): HTMLElement | null => {
77
+ const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size: ESizes, showLogo: boolean, disabledInputs: boolean, background: EBackgrounds, defaultClass?: string | UN): HTMLElement | null => {
78
78
  const loaderElement = _findExistingLoader(parent);
79
79
  if (loaderElement && !loaderElement.classList.contains(DISPOSED_LOADER_CLASS)) {
80
80
  const count = parseInt(loaderElement.getAttribute(LOADER_DATA_COUNT_ATTRIBUTE) ?? "0") + 1;
@@ -86,6 +86,7 @@ const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size
86
86
  const newLoaderElement = document.createElement("div");
87
87
  newLoaderElement.id = LOADER_IDENTITY;
88
88
  newLoaderElement.classList.add("nexus-container", "o0", EAnimationClasses.FADE_IN, background);
89
+ if (defaultClass) newLoaderElement.classList.add(defaultClass);
89
90
  _updateLoaderAttributes(newLoaderElement, type, size, disabledInputs, 1);
90
91
 
91
92
  const loader = document.createElement("div");
@@ -97,7 +98,7 @@ const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size
97
98
  loader.innerText = "Loading...";
98
99
  loader.classList.add("tx52");
99
100
  loader.style.fontSize = "1.5rem";
100
- } else if (showLogo) {
101
+ } else if (showLogo && type !== EAnimationClasses.SLIDE_BAR && type !== EAnimationClasses.BOUNCE_BAR) {
101
102
  _updateLoaderProperties(newLoaderElement, type, size);
102
103
 
103
104
  const logoElement = document.createElement("img");
@@ -123,7 +124,7 @@ const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size
123
124
  return newLoaderElement;
124
125
  };
125
126
 
126
- export const ShowLoader = async (parent?: String | HTMLElement | UN, type?: EAnimationClasses | UN, disabledInputs?: boolean | UN, size?: ESizes | UN, delay?: number | UN, showLogo?: boolean | UN, background?: EBackgrounds | UN): Promise<HTMLElement | null> => {
127
+ export const ShowLoader = async (parent?: String | HTMLElement | UN, type?: EAnimationClasses | UN, disabledInputs?: boolean | UN, size?: ESizes | UN, delay?: number | UN, showLogo?: boolean | UN, background?: EBackgrounds | UN, defaultClass?: string | UN): Promise<HTMLElement | null> => {
127
128
  if (!IsBrowser()) return Promise.resolve(null);
128
129
  type = type ?? DEFAULT_LOADER_TYPE;
129
130
  size = size ?? DEFAULT_LOADER_SIZE;
@@ -133,7 +134,7 @@ export const ShowLoader = async (parent?: String | HTMLElement | UN, type?: EAni
133
134
  const timeout = setTimeout(() => {
134
135
  clearTimeout(timeout);
135
136
  const parentElement = _findParentElement(parent);
136
- const loaderElement = _createLoaderElement(parentElement, type, size, showLogo ?? true, disabledInputs ?? false, background);
137
+ const loaderElement = _createLoaderElement(parentElement, type, size, showLogo ?? true, disabledInputs ?? false, background, defaultClass);
137
138
  resolve(loaderElement);
138
139
  }, delay ?? DEFAULT_LOADER_DELAY);
139
140
  });
@@ -1,30 +1,67 @@
1
1
  import { CreateUpdatePreference } from "../api-services/preference-service";
2
2
  import { IsBrowser } from "../helpers/browser-helpers";
3
3
  import { Debounce } from "../helpers/utility-helpers";
4
+ import { EFlexPositions } from "../interface";
4
5
  import { LOCAL_STORAGE_KEYS } from "../interfaces/storage-interfaces";
5
6
  import { EPreferenceTargets } from "../interfaces/system-interfaces";
6
7
  import { IThemeInformation } from "../interfaces/theme-interfaces";
7
8
  import { O } from "../nexus-client";
8
9
 
10
+ /**
11
+ * Create a debounce function to update the preference in the server
12
+ * This is used to update the preference in the server
13
+ * @param CreateUpdatePreference The function to create the update preference
14
+ * @param debounceTime The debounce time in milliseconds
15
+ * @returns The debounce function
16
+ */
9
17
  const _createUpdatePreferenceDebounce = Debounce(CreateUpdatePreference, 1000);
10
18
 
19
+ /**
20
+ * Sync the value of the local storage to the server
21
+ * This is used to sync the value of the local storage to the server
22
+ * @param key The key of the local storage
23
+ * @param value The value to set
24
+ * @param isSyncWithServer Whether to sync with the server
25
+ * @param target The target of the local storage
26
+ */
11
27
  const _syncStorageToServer = async (key: string, value: string | null, isSyncWithServer?: boolean, target?: EPreferenceTargets) => {
12
28
  if (!IsBrowser() || !isSyncWithServer || !target) return;
13
29
 
14
30
  await _createUpdatePreferenceDebounce(target, key, value);
15
31
  };
16
32
 
33
+ /**
34
+ * Set the value of the local storage
35
+ * This is used to set the value of the local storage
36
+ * @param key The key of the local storage
37
+ * @param value The value to set
38
+ * @param isSyncWithServer Whether to sync with the server
39
+ * @param target The target of the local storage
40
+ */
17
41
  export const SetLocalStorage = (key: string, value: string, isSyncWithServer?: boolean, target?: EPreferenceTargets) => {
18
42
  if (!IsBrowser()) return;
19
43
 
20
44
  _syncStorageToServer(key, value, isSyncWithServer, target);
21
45
  localStorage.setItem(key, value);
22
46
  };
47
+ /**
48
+ * Remove the value of the local storage
49
+ * This is used to remove the value of the local storage
50
+ * @param key The key of the local storage
51
+ * @param isSyncWithServer Whether to sync with the server
52
+ * @param target The target of the local storage
53
+ */
23
54
  export const RemoveLocalStorage = (key: string, isSyncWithServer?: boolean, target?: EPreferenceTargets) => {
24
55
  _syncStorageToServer(key, null, isSyncWithServer, target);
25
56
  localStorage.removeItem(key);
26
57
  };
27
58
 
59
+ /**
60
+ * Get the value of the local storage
61
+ * This is used to get the value of the local storage
62
+ * @param key The key of the local storage
63
+ * @returns The value of the local storage
64
+ */
28
65
  export function GetLocalStorage(key: string): string | null {
29
66
  if (!IsBrowser()) return null;
30
67
  const data = localStorage.getItem(key);
@@ -32,6 +69,11 @@ export function GetLocalStorage(key: string): string | null {
32
69
  return data;
33
70
  }
34
71
 
72
+ /**
73
+ * Set the current theme in the local storage
74
+ * This is used to determine the current theme in the application
75
+ * @param themeCode The code of the theme to set
76
+ */
35
77
  export const SetCurrentTheme = (themeCode: string) => SetLocalStorage(LOCAL_STORAGE_KEYS.ACTIVE_THEME, themeCode, true, EPreferenceTargets.User);
36
78
  export const GetCurrentTheme = (): IThemeInformation => {
37
79
  const themeInformations = O.systemThemes;
@@ -41,3 +83,20 @@ export const GetCurrentTheme = (): IThemeInformation => {
41
83
  return themeInformations.find((theme) => theme.systemName === activeTheme) || themeInformations[0];
42
84
  };
43
85
  export const GetCurrentThemeName = (): string => GetCurrentTheme().systemName;
86
+
87
+ /**
88
+ * Get the position of the form buttons from the local storage
89
+ * This is used to determine the position of the form buttons in the form
90
+ * @returns The position of the form buttons
91
+ */
92
+ export const GetFormButtonPosition = (): EFlexPositions => {
93
+ const position = GetLocalStorage(LOCAL_STORAGE_KEYS.FORM_BUTTON_POSITION);
94
+ if (!position) return EFlexPositions.Left;
95
+ return position as EFlexPositions;
96
+ };
97
+ /**
98
+ * Set the position of the form buttons in the local storage
99
+ * This is used to determine the position of the form buttons in the form
100
+ * @param position The position of the form buttons
101
+ */
102
+ export const SetFormButtonPosition = (position: EFlexPositions) => SetLocalStorage(LOCAL_STORAGE_KEYS.FORM_BUTTON_POSITION, position, true, EPreferenceTargets.User);
@@ -0,0 +1,383 @@
1
+ import { DEFAULT_ANIMATION_DURATION, DEFAULT_ICON_ELEMENT_NAME, EAnimationClasses, EBackgrounds, ECallbackTypes, EIcons, EIconTypes, EMessageBoxButtons, EMessageIcons, EMessageResult, EMessageTypes, EOpacity, ESizes, PopupMessageCallback, UN } from "nexus-shared";
2
+ import { CreateIconBox, IsBrowser, PxToRem, RemoveElement } from "nexus-shared/client";
3
+
4
+ const MESSAGE_IDENTITY = "nexus-message";
5
+ const MESSAGE_GAP_REM = 0.8;
6
+ const MESSAGE_TOP_REM = 1;
7
+ const PROGRESS_BAR_HEIGHT_REM = 0.2;
8
+
9
+ /** Newest first — index 0 is the top message. */
10
+ const activeMessages: HTMLElement[] = [];
11
+ const messageProgressCleanups = new WeakMap<HTMLElement, () => void>();
12
+ let reflowScheduled = false;
13
+
14
+ const stopMessageProgress = (messageElement: HTMLElement) => {
15
+ messageProgressCleanups.get(messageElement)?.();
16
+ messageProgressCleanups.delete(messageElement);
17
+ };
18
+
19
+ const startMessageProgress = (messageElement: HTMLElement, progressFillElement: HTMLElement, durationMilliseconds: number, onComplete: () => void) => {
20
+ let elapsedMilliseconds = 0;
21
+ let lastFrameTimestamp: number | null = null;
22
+ let animationFrameId = 0;
23
+
24
+ const cancelAnimation = () => {
25
+ if (animationFrameId) cancelAnimationFrame(animationFrameId);
26
+ animationFrameId = 0;
27
+ };
28
+
29
+ const finishProgress = () => {
30
+ cancelAnimation();
31
+ messageProgressCleanups.delete(messageElement);
32
+ onComplete();
33
+ };
34
+
35
+ const updateProgress = (frameTimestamp: number) => {
36
+ animationFrameId = 0;
37
+ if (lastFrameTimestamp !== null) {
38
+ elapsedMilliseconds += frameTimestamp - lastFrameTimestamp;
39
+ progressFillElement.style.width = `${Math.min((elapsedMilliseconds / durationMilliseconds) * 100, 100)}%`;
40
+ if (elapsedMilliseconds >= durationMilliseconds) return finishProgress();
41
+ }
42
+ lastFrameTimestamp = frameTimestamp;
43
+ animationFrameId = requestAnimationFrame(updateProgress);
44
+ };
45
+
46
+ const onMouseEnter = () => {
47
+ lastFrameTimestamp = null;
48
+ cancelAnimation();
49
+ };
50
+ const onMouseLeave = () => {
51
+ if (!animationFrameId) animationFrameId = requestAnimationFrame(updateProgress);
52
+ };
53
+
54
+ messageElement.addEventListener("mouseenter", onMouseEnter);
55
+ messageElement.addEventListener("mouseleave", onMouseLeave);
56
+ messageProgressCleanups.set(messageElement, () => {
57
+ cancelAnimation();
58
+ messageElement.removeEventListener("mouseenter", onMouseEnter);
59
+ messageElement.removeEventListener("mouseleave", onMouseLeave);
60
+ });
61
+ animationFrameId = requestAnimationFrame(updateProgress);
62
+ };
63
+
64
+ const scheduleReflow = () => {
65
+ if (reflowScheduled) return;
66
+ reflowScheduled = true;
67
+ requestAnimationFrame(() => {
68
+ reflowScheduled = false;
69
+ let offset = MESSAGE_TOP_REM;
70
+ for (const el of activeMessages) {
71
+ el.style.top = `${offset}rem`;
72
+ offset += PxToRem(el.offsetHeight) + MESSAGE_GAP_REM;
73
+ }
74
+ });
75
+ };
76
+
77
+ const dismissMessage = (el: HTMLElement, callback?: PopupMessageCallback) => {
78
+ const index = activeMessages.indexOf(el);
79
+ if (index === -1) return;
80
+
81
+ stopMessageProgress(el);
82
+ activeMessages.splice(index, 1);
83
+ el.style.right = "-100%";
84
+ el.style.top = "100%";
85
+ RemoveElement(el, DEFAULT_ANIMATION_DURATION, EAnimationClasses.FADE_OUT);
86
+ scheduleReflow();
87
+ callback?.(ECallbackTypes.Destroy);
88
+ };
89
+
90
+ const _showMessage = (title: string, message: string, messageType: EMessageTypes, delay?: number | UN, duration?: number | UN, icon?: EMessageIcons | UN, callback?: PopupMessageCallback) => {
91
+ if (!IsBrowser()) return;
92
+
93
+ delay = delay ?? 0;
94
+ duration = duration ?? 3000;
95
+
96
+ const createAndShow = () => {
97
+ /** Root message element */
98
+ const messageRootElement = document.createElement("div");
99
+ const messageBodyElement = document.createElement("div");
100
+ const messageHandleElement = document.createElement("div");
101
+ const messageContentElement = document.createElement("div");
102
+ const messageIconElement = document.createElement("div");
103
+ const progressTrackElement = document.createElement("div");
104
+ const progressFillElement = document.createElement("div");
105
+
106
+ document.body.appendChild(messageRootElement);
107
+ messageRootElement.appendChild(messageBodyElement);
108
+ messageBodyElement.appendChild(messageHandleElement);
109
+ messageBodyElement.appendChild(messageIconElement);
110
+ messageBodyElement.appendChild(messageContentElement);
111
+ messageRootElement.appendChild(progressTrackElement);
112
+ progressTrackElement.appendChild(progressFillElement);
113
+
114
+ messageRootElement.className = `${MESSAGE_IDENTITY} pf bs3 r bg1 anim df ddc oh`;
115
+ messageBodyElement.className = "df ac w100";
116
+ messageHandleElement.className = messageType;
117
+ messageIconElement.className = `ib ml4 sm rr ${messageType}`;
118
+ messageContentElement.className = `p4 pr2 fg`;
119
+ progressTrackElement.className = "w100";
120
+ progressFillElement.className = messageType;
121
+
122
+ messageRootElement.style.right = "-100%";
123
+ messageRootElement.style.transitionDuration = `${DEFAULT_ANIMATION_DURATION * 1.5}ms`;
124
+
125
+ messageHandleElement.style.width = "0.3rem";
126
+ messageHandleElement.style.alignSelf = "stretch";
127
+ messageHandleElement.style.flexShrink = "0";
128
+
129
+ progressTrackElement.style.height = `${PROGRESS_BAR_HEIGHT_REM}rem`;
130
+ progressTrackElement.style.flexShrink = "0";
131
+ progressFillElement.style.height = "100%";
132
+ progressFillElement.style.width = "0%";
133
+
134
+ messageIconElement.innerHTML = `<${DEFAULT_ICON_ELEMENT_NAME} class="icon" name="${icon}" />`;
135
+
136
+ const _TM = setTimeout(() => {
137
+ clearTimeout(_TM);
138
+ const WINDOW_WIDTH = window.innerWidth;
139
+ if (WINDOW_WIDTH > 1000) {
140
+ messageRootElement.style.right = "1rem";
141
+ messageRootElement.style.maxWidth = "35rem";
142
+ messageRootElement.style.minWidth = "16rem";
143
+ } else {
144
+ messageRootElement.style.maxWidth = "calc(100vw - 1rem)";
145
+ messageRootElement.style.minWidth = "15rem";
146
+ messageRootElement.style.right = "0.5rem";
147
+ }
148
+ }, 0);
149
+
150
+ const titleElement = document.createElement("p");
151
+ const messageElement = document.createElement("p");
152
+ titleElement.className = "mb1 fw600 w100 df ac";
153
+ messageElement.className = "o08 fs-09 pr3";
154
+ titleElement.innerHTML = `<p class="w100 fg">${title}</p>`;
155
+ messageElement.innerHTML = message;
156
+ messageContentElement.appendChild(titleElement);
157
+ messageContentElement.appendChild(messageElement);
158
+
159
+ const dismiss = () => dismissMessage(messageRootElement, callback);
160
+ CreateIconBox(EIcons.CLOSE, titleElement, null, null, EIconTypes.FILLED, EBackgrounds.Background1, ESizes.VeryTiny, ESizes.Tiny, null, null, null, null, EOpacity.O05, "Clear Message", null, null, null, dismiss);
161
+
162
+ activeMessages.unshift(messageRootElement);
163
+ scheduleReflow();
164
+ requestAnimationFrame(scheduleReflow);
165
+ callback?.(ECallbackTypes.Initialize);
166
+
167
+ if (duration > 0) startMessageProgress(messageRootElement, progressFillElement, duration, dismiss);
168
+ };
169
+
170
+ if (typeof delay === "number" && delay > 0) {
171
+ const _TM = window.setTimeout(() => {
172
+ clearTimeout(_TM);
173
+ createAndShow();
174
+ }, delay);
175
+ } else createAndShow();
176
+ };
177
+
178
+ export const SuccessMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
179
+ return _showMessage(title, message, EMessageTypes.Success, delay, duration, icon ?? EMessageIcons.Success, callback);
180
+ };
181
+ export const ErrorMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
182
+ return _showMessage(title, message, EMessageTypes.Error, delay, duration, icon ?? EMessageIcons.Error, callback);
183
+ };
184
+ export const WarningMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
185
+ return _showMessage(title, message, EMessageTypes.Warning, delay, duration, icon ?? EMessageIcons.Warning, callback);
186
+ };
187
+ export const InformationMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
188
+ return _showMessage(title, message, EMessageTypes.Information, delay, duration, icon ?? EMessageIcons.Information, callback);
189
+ };
190
+ export const QuestionMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
191
+ return _showMessage(title, message, EMessageTypes.Information, delay, duration, icon ?? EMessageIcons.Question, callback);
192
+ };
193
+
194
+ type MessageBoxButton = {
195
+ result: EMessageResult;
196
+ label: string;
197
+ themeBackgroundClass?: EBackgrounds | string;
198
+ };
199
+
200
+ const normalizeMessageResult = (value: string): EMessageResult => {
201
+ const v = (value ?? "").trim().toLowerCase();
202
+ if (v === "ok") return EMessageResult.OK;
203
+ if (v === "yes") return EMessageResult.Yes;
204
+ if (v === "no") return EMessageResult.No;
205
+ if (v === "cancel") return EMessageResult.Cancel;
206
+ return EMessageResult.Undefined;
207
+ };
208
+
209
+ const getMessageBoxButtons = (messageButtons: EMessageBoxButtons, themeBackgroundClass: EMessageTypes, overrideYesButtonText?: string | null): MessageBoxButton[] => {
210
+ const parts = (messageButtons ?? EMessageBoxButtons.OK).split("_").filter(Boolean);
211
+ const results = parts.map(normalizeMessageResult).filter((r) => r !== EMessageResult.Undefined);
212
+ const resolved = results.length ? results : [EMessageResult.OK];
213
+
214
+ return resolved.map((result) => {
215
+ if (result === EMessageResult.Yes) return { result, label: overrideYesButtonText ?? "Yes", themeBackgroundClass };
216
+ if (result === EMessageResult.OK) return { result, label: "OK", themeBackgroundClass: themeBackgroundClass };
217
+ if (result === EMessageResult.No) return { result, label: "No", themeBackgroundClass: EBackgrounds.AlwaysDarkBackgroundColor };
218
+ if (result === EMessageResult.Cancel) return { result, label: "Cancel", themeBackgroundClass: EBackgrounds.AlwaysDarkBackgroundColor };
219
+ return { result, label: String(result) };
220
+ });
221
+ };
222
+
223
+ const getCloseResult = (buttons: MessageBoxButton[]): EMessageResult => {
224
+ if (buttons.some((b) => b.result === EMessageResult.Cancel)) return EMessageResult.Cancel;
225
+ if (buttons.some((b) => b.result === EMessageResult.No)) return EMessageResult.No;
226
+ if (buttons.some((b) => b.result === EMessageResult.OK)) return EMessageResult.OK;
227
+ return EMessageResult.Undefined;
228
+ };
229
+
230
+ /**
231
+ * Windows-like modal message box (DOM-based).
232
+ * - Keyboard: Tab / Shift+Tab / ArrowLeft / ArrowRight to move focus
233
+ * - Enter: activates focused button
234
+ * - Escape: closes with Cancel/No/OK (in that order if present)
235
+ */
236
+ export const ShowDialogMessageBox = async (messageButtons: EMessageBoxButtons, title: string, message: string, iconName: EMessageIcons, themeBackgroundClass?: EMessageTypes, overrideYesButtonText?: string | null, confirmMessage?: string | null, allowOverlayClick?: boolean): Promise<EMessageResult | null> => {
237
+ if (!IsBrowser()) return null;
238
+
239
+ return new Promise((resolve) => {
240
+ themeBackgroundClass = themeBackgroundClass ?? EMessageTypes.Information;
241
+ const bodyElement = document.body as HTMLBodyElement;
242
+ const overlayElement = document.createElement("div") as HTMLDivElement;
243
+ const dialogElement = document.createElement("div") as HTMLDivElement;
244
+
245
+ const iconBoxElement = document.createElement("div") as HTMLDivElement;
246
+ const headerElement = document.createElement("div") as HTMLDivElement;
247
+ const headerTextElement = document.createElement("div") as HTMLDivElement;
248
+ const titleElement = document.createElement("h4") as HTMLHeadingElement;
249
+ const messageElement = document.createElement("p") as HTMLParagraphElement;
250
+ const confirmElement = document.createElement("p") as HTMLParagraphElement;
251
+ const buttonRowElement = document.createElement("div") as HTMLDivElement;
252
+
253
+ const buttons = getMessageBoxButtons(messageButtons, themeBackgroundClass, overrideYesButtonText);
254
+ const actionButtons: HTMLButtonElement[] = [];
255
+ let currentFocusIndex = 0;
256
+ let isClosed = false;
257
+
258
+ overlayElement.className = `pf df ac jc ${EBackgrounds.PopupBackgroundColor} ${EAnimationClasses.FADE_IN}`;
259
+ overlayElement.style.zIndex = "100000";
260
+ overlayElement.style.inset = "0";
261
+
262
+ dialogElement.className = `bg1 r bs3 anim df ddc`;
263
+ dialogElement.style.maxWidth = "36rem";
264
+ dialogElement.style.width = "calc(100vw - 2rem)";
265
+ dialogElement.style.boxSizing = "border-box";
266
+
267
+ headerElement.className = "df w100";
268
+ headerElement.style.gap = "1rem";
269
+ headerElement.style.padding = "1.25rem";
270
+
271
+ iconBoxElement.className = `df ac jc rr bb ${themeBackgroundClass}`;
272
+ iconBoxElement.style.minWidth = "4rem";
273
+ iconBoxElement.style.maxWidth = "4rem";
274
+ iconBoxElement.style.minHeight = "4rem";
275
+ iconBoxElement.style.maxHeight = "4rem";
276
+
277
+ const iconElement = document.createElement(DEFAULT_ICON_ELEMENT_NAME) as HTMLElement & { name?: string };
278
+ iconElement.name = `${iconName}${EIconTypes.OUTLINED}`;
279
+ iconElement.className = `icon ${ESizes.Large} o08`;
280
+ iconElement.style.color = "white";
281
+ iconBoxElement.appendChild(iconElement);
282
+
283
+ headerTextElement.className = "fg";
284
+
285
+ titleElement.className = "fw600";
286
+ titleElement.style.margin = "0";
287
+ titleElement.innerText = title ?? "";
288
+
289
+ messageElement.className = "o08";
290
+ messageElement.style.margin = "0.5rem 0 0 0";
291
+ messageElement.innerHTML = message ?? "";
292
+
293
+ confirmElement.className = "o08";
294
+ confirmElement.style.margin = "0.75rem 0 0 0";
295
+ confirmElement.innerHTML = confirmMessage ?? "";
296
+ if (!confirmMessage) confirmElement.style.display = "none";
297
+
298
+ buttonRowElement.className = "df w100";
299
+ buttonRowElement.style.gap = "0.75rem";
300
+ buttonRowElement.style.justifyContent = "flex-end";
301
+ buttonRowElement.style.padding = "0 1.25rem 1.25rem 1.25rem";
302
+
303
+ const setFocus = (index: number) => {
304
+ currentFocusIndex = Math.min(Math.max(index, 0), actionButtons.length - 1);
305
+ actionButtons[currentFocusIndex]?.focus();
306
+ };
307
+
308
+ const closeDialog = (result: EMessageResult) => {
309
+ if (isClosed) return;
310
+ isClosed = true;
311
+
312
+ document.removeEventListener("keydown", onKeyDown);
313
+ overlayElement.removeEventListener("click", onOverlayClick);
314
+
315
+ overlayElement.classList.remove(EAnimationClasses.FADE_IN);
316
+ overlayElement.classList.add(EAnimationClasses.FADE_OUT);
317
+ dialogElement.classList.add(EAnimationClasses.PULSE_OUT);
318
+
319
+ RemoveElement(overlayElement, DEFAULT_ANIMATION_DURATION, EAnimationClasses.FADE_OUT).finally(() => resolve(result));
320
+ };
321
+
322
+ const onOverlayClick = (event: MouseEvent) => {
323
+ if (event.target === overlayElement) closeDialog(getCloseResult(buttons));
324
+ };
325
+
326
+ const onKeyDown = (event: KeyboardEvent) => {
327
+ const key = event.key;
328
+ const isShiftTab = key === "Tab" && event.shiftKey;
329
+ const isTab = key === "Tab" && !event.shiftKey;
330
+
331
+ if (key === "ArrowLeft" || isShiftTab) {
332
+ event.preventDefault();
333
+ setFocus((currentFocusIndex - 1 + actionButtons.length) % actionButtons.length);
334
+ } else if (key === "ArrowRight" || isTab) {
335
+ event.preventDefault();
336
+ setFocus((currentFocusIndex + 1) % actionButtons.length);
337
+ } else if (key === "Enter") {
338
+ event.preventDefault();
339
+ const result = buttons[currentFocusIndex]?.result ?? EMessageResult.Undefined;
340
+ closeDialog(result);
341
+ } else if (key === "Escape") {
342
+ event.preventDefault();
343
+ closeDialog(getCloseResult(buttons));
344
+ }
345
+ };
346
+
347
+ const createButton = (button: MessageBoxButton) => {
348
+ const buttonElement = document.createElement("button") as HTMLButtonElement;
349
+ buttonElement.type = "button";
350
+ buttonElement.className = `sn r h p2 pr4 pl4 ${button.themeBackgroundClass}`;
351
+ buttonElement.style.minWidth = "5.5rem";
352
+ buttonElement.innerText = button.label;
353
+
354
+ const onClick = () => closeDialog(button.result);
355
+ buttonElement.addEventListener("click", onClick, { once: true });
356
+ return buttonElement;
357
+ };
358
+
359
+ for (const button of buttons) {
360
+ const buttonElement = createButton(button);
361
+ buttonRowElement.appendChild(buttonElement);
362
+ actionButtons.push(buttonElement);
363
+ }
364
+
365
+ headerTextElement.appendChild(titleElement);
366
+ headerTextElement.appendChild(messageElement);
367
+ headerTextElement.appendChild(confirmElement);
368
+
369
+ headerElement.appendChild(iconBoxElement);
370
+ headerElement.appendChild(headerTextElement);
371
+
372
+ dialogElement.appendChild(headerElement);
373
+ dialogElement.appendChild(buttonRowElement);
374
+
375
+ overlayElement.appendChild(dialogElement);
376
+ bodyElement.insertBefore(overlayElement, bodyElement.firstChild);
377
+
378
+ if (allowOverlayClick === false) overlayElement.addEventListener("click", onOverlayClick);
379
+ document.addEventListener("keydown", onKeyDown);
380
+
381
+ setTimeout(() => setFocus(0), 0);
382
+ });
383
+ };