@jmruthers/pace-core 0.5.136 → 0.5.137

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 (289) hide show
  1. package/dist/{DataTable-CYOHOX3O.js → DataTable-6M4L6BI2.js} +10 -9
  2. package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
  3. package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
  4. package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
  5. package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
  6. package/dist/chunk-4C7EXCAR.js.map +1 -0
  7. package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
  8. package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
  9. package/dist/chunk-7QCC6MCP.js.map +1 -0
  10. package/dist/{chunk-FHWWBIHA.js → chunk-BCIBECNB.js} +5 -5
  11. package/dist/chunk-BJPBT3CU.js +21 -0
  12. package/dist/chunk-BJPBT3CU.js.map +1 -0
  13. package/dist/{chunk-L6PGMCMD.js → chunk-BLCXZEYF.js} +3 -3
  14. package/dist/{chunk-HJGGOMQ6.js → chunk-HAWZXGR2.js} +147 -103
  15. package/dist/chunk-HAWZXGR2.js.map +1 -0
  16. package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
  17. package/dist/chunk-JISYG63F.js +70 -0
  18. package/dist/chunk-JISYG63F.js.map +1 -0
  19. package/dist/{chunk-NOHEVYVX.js → chunk-KYRHUBIU.js} +417 -319
  20. package/dist/chunk-KYRHUBIU.js.map +1 -0
  21. package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
  22. package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
  23. package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
  24. package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
  25. package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
  26. package/dist/components.d.ts +1 -1
  27. package/dist/components.js +12 -11
  28. package/dist/components.js.map +1 -1
  29. package/dist/hooks.js +8 -9
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +15 -14
  33. package/dist/index.js.map +1 -1
  34. package/dist/providers.js +3 -4
  35. package/dist/rbac/index.js +8 -9
  36. package/dist/schema-DTDZQe2u.d.ts +28 -0
  37. package/dist/types.d.ts +152 -3
  38. package/dist/types.js +51 -16
  39. package/dist/types.js.map +1 -1
  40. package/dist/utils.d.ts +89 -4
  41. package/dist/utils.js +214 -96
  42. package/dist/utils.js.map +1 -1
  43. package/dist/validation.d.ts +1 -343
  44. package/dist/validation.js +3 -100
  45. package/docs/api/classes/ColumnFactory.md +1 -1
  46. package/docs/api/classes/ErrorBoundary.md +1 -1
  47. package/docs/api/classes/InvalidScopeError.md +1 -1
  48. package/docs/api/classes/MissingUserContextError.md +1 -1
  49. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  50. package/docs/api/classes/PermissionDeniedError.md +1 -1
  51. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  52. package/docs/api/classes/RBACAuditManager.md +1 -1
  53. package/docs/api/classes/RBACCache.md +1 -1
  54. package/docs/api/classes/RBACEngine.md +1 -1
  55. package/docs/api/classes/RBACError.md +1 -1
  56. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  57. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  58. package/docs/api/classes/StorageUtils.md +1 -1
  59. package/docs/api/enums/FileCategory.md +1 -1
  60. package/docs/api/interfaces/AggregateConfig.md +1 -1
  61. package/docs/api/interfaces/BadgeProps.md +27 -0
  62. package/docs/api/interfaces/ButtonProps.md +1 -1
  63. package/docs/api/interfaces/CardProps.md +1 -1
  64. package/docs/api/interfaces/ColorPalette.md +1 -1
  65. package/docs/api/interfaces/ColorShade.md +1 -1
  66. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  67. package/docs/api/interfaces/DataRecord.md +1 -1
  68. package/docs/api/interfaces/DataTableAction.md +1 -1
  69. package/docs/api/interfaces/DataTableColumn.md +1 -1
  70. package/docs/api/interfaces/DataTableProps.md +1 -1
  71. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  72. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  73. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  74. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  75. package/docs/api/interfaces/EventLogoProps.md +1 -1
  76. package/docs/api/interfaces/ExportColumn.md +1 -1
  77. package/docs/api/interfaces/ExportOptions.md +1 -1
  78. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  79. package/docs/api/interfaces/FileMetadata.md +1 -1
  80. package/docs/api/interfaces/FileReference.md +1 -1
  81. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  82. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  83. package/docs/api/interfaces/FileUploadProps.md +1 -1
  84. package/docs/api/interfaces/FooterProps.md +1 -1
  85. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  86. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  87. package/docs/api/interfaces/InputProps.md +1 -1
  88. package/docs/api/interfaces/LabelProps.md +1 -1
  89. package/docs/api/interfaces/LoginFormProps.md +1 -1
  90. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  91. package/docs/api/interfaces/NavigationContextType.md +1 -1
  92. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  93. package/docs/api/interfaces/NavigationItem.md +1 -1
  94. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  95. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  96. package/docs/api/interfaces/Organisation.md +1 -1
  97. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  98. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  99. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  100. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  101. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  102. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  103. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  104. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  105. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  106. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  107. package/docs/api/interfaces/PaletteData.md +1 -1
  108. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  109. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  110. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  112. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  113. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  116. package/docs/api/interfaces/RBACConfig.md +1 -1
  117. package/docs/api/interfaces/RBACLogger.md +1 -1
  118. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  119. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  120. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  121. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  122. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  123. package/docs/api/interfaces/RouteConfig.md +1 -1
  124. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  125. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  126. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +79 -10
  154. package/docs/architecture/README.md +0 -1
  155. package/docs/styles/README.md +0 -2
  156. package/examples/RBAC/CompleteRBACExample.tsx +324 -0
  157. package/examples/RBAC/EventBasedApp.tsx +239 -0
  158. package/examples/RBAC/PermissionExample.tsx +151 -0
  159. package/examples/RBAC/index.ts +13 -0
  160. package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
  161. package/examples/public-pages/PublicEventPage.tsx +274 -0
  162. package/examples/public-pages/PublicPageApp.tsx +308 -0
  163. package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
  164. package/examples/public-pages/index.ts +14 -0
  165. package/package.json +1 -10
  166. package/src/__tests__/TEST_STANDARD.md +92 -0
  167. package/src/components/Badge/Badge.test.tsx +314 -0
  168. package/src/components/Badge/Badge.tsx +304 -0
  169. package/src/components/Badge/index.ts +3 -0
  170. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
  171. package/src/components/DataTable/__tests__/styles.test.ts +1 -1
  172. package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
  173. package/src/components/DataTable/components/DataTableBody.tsx +461 -0
  174. package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
  175. package/src/components/DataTable/components/FilterRow.tsx +9 -3
  176. package/src/components/DataTable/components/PaginationControls.tsx +1 -0
  177. package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
  178. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
  179. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
  180. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
  181. package/src/components/DataTable/core/ActionManager.ts +235 -0
  182. package/src/components/DataTable/core/ColumnManager.ts +205 -0
  183. package/src/components/DataTable/core/DataManager.ts +188 -0
  184. package/src/components/DataTable/core/DataTableContext.tsx +181 -0
  185. package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
  186. package/src/components/DataTable/core/PluginRegistry.ts +229 -0
  187. package/src/components/DataTable/core/StateManager.ts +311 -0
  188. package/src/components/DataTable/core/interfaces.ts +338 -0
  189. package/src/components/DataTable/styles.ts +27 -6
  190. package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
  191. package/src/components/DataTable/utils/columnUtils.ts +40 -0
  192. package/src/components/DataTable/utils/debugTools.ts +609 -0
  193. package/src/components/DataTable/utils/index.ts +1 -0
  194. package/src/components/Dialog/README.md +804 -0
  195. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
  196. package/src/components/Dialog/utils/safeHtml.ts +185 -0
  197. package/src/components/Footer/Footer.test.tsx +1 -1
  198. package/src/components/Form/Form.test.tsx +1 -1
  199. package/src/components/Form/FormErrorSummary.tsx +113 -0
  200. package/src/components/Form/FormFieldset.tsx +127 -0
  201. package/src/components/Form/FormLiveRegion.tsx +198 -0
  202. package/src/components/LoginForm/LoginForm.test.tsx +1 -1
  203. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
  204. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  205. package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
  206. package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
  207. package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
  208. package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
  209. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  210. package/src/components/Select/Select.test.tsx +1 -1
  211. package/src/components/Select/Select.tsx +20 -8
  212. package/src/components/Table/__tests__/Table.test.tsx +1 -1
  213. package/src/components/index.ts +3 -0
  214. package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
  215. package/src/index.ts +4 -0
  216. package/src/styles/core.css +3 -0
  217. package/src/utils/appConfig.ts +47 -0
  218. package/src/utils/appIdResolver.test.ts +499 -0
  219. package/src/utils/appIdResolver.ts +130 -0
  220. package/src/utils/appNameResolver.simple.test.ts +212 -0
  221. package/src/utils/appNameResolver.test.ts +121 -0
  222. package/src/utils/appNameResolver.ts +191 -0
  223. package/src/utils/audit.ts +127 -0
  224. package/src/utils/auth-utils.ts +96 -0
  225. package/src/utils/bundleAnalysis.ts +129 -0
  226. package/src/utils/cn.ts +7 -0
  227. package/src/utils/debugLogger.ts +67 -0
  228. package/src/utils/deviceFingerprint.ts +215 -0
  229. package/src/utils/dynamicUtils.ts +105 -0
  230. package/src/utils/file-reference.test.ts +788 -0
  231. package/src/utils/file-reference.ts +519 -0
  232. package/src/utils/formatDate.test.ts +237 -0
  233. package/src/utils/formatting.ts +133 -0
  234. package/src/utils/index.ts +7 -0
  235. package/src/utils/lazyLoad.tsx +44 -0
  236. package/src/utils/logger.ts +179 -0
  237. package/src/utils/organisationContext.test.ts +322 -0
  238. package/src/utils/organisationContext.ts +153 -0
  239. package/src/utils/performanceBenchmark.ts +64 -0
  240. package/src/utils/performanceBudgets.ts +110 -0
  241. package/src/utils/permissionTypes.ts +37 -0
  242. package/src/utils/permissionUtils.test.ts +393 -0
  243. package/src/utils/permissionUtils.ts +34 -0
  244. package/src/utils/sanitization.ts +264 -0
  245. package/src/utils/schemaUtils.ts +37 -0
  246. package/src/utils/secureDataAccess.test.ts +711 -0
  247. package/src/utils/secureDataAccess.ts +377 -0
  248. package/src/utils/secureErrors.ts +79 -0
  249. package/src/utils/secureStorage.ts +244 -0
  250. package/src/utils/security.ts +156 -0
  251. package/src/utils/securityMonitor.ts +45 -0
  252. package/src/utils/sessionTracking.ts +126 -0
  253. package/src/utils/validation.ts +111 -0
  254. package/src/utils/validationUtils.ts +120 -0
  255. package/src/validation/index.ts +2 -2
  256. package/dist/chunk-444EZN6N.js.map +0 -1
  257. package/dist/chunk-APIBCTL2.js +0 -670
  258. package/dist/chunk-APIBCTL2.js.map +0 -1
  259. package/dist/chunk-HJGGOMQ6.js.map +0 -1
  260. package/dist/chunk-K2WWTH7O.js +0 -94
  261. package/dist/chunk-K2WWTH7O.js.map +0 -1
  262. package/dist/chunk-LMC26NLJ.js +0 -84
  263. package/dist/chunk-LMC26NLJ.js.map +0 -1
  264. package/dist/chunk-NOHEVYVX.js.map +0 -1
  265. package/dist/chunk-TVYPTYOY.js.map +0 -1
  266. package/dist/validation-8npbysjg.d.ts +0 -177
  267. /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-6M4L6BI2.js.map} +0 -0
  268. /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
  269. /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
  270. /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
  271. /package/dist/{chunk-FHWWBIHA.js.map → chunk-BCIBECNB.js.map} +0 -0
  272. /package/dist/{chunk-L6PGMCMD.js.map → chunk-BLCXZEYF.js.map} +0 -0
  273. /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
  274. /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
  275. /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
  276. /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
  277. /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
  278. /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
  279. /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
  280. /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
  281. /package/examples/{components → components 2}/DataTable/index.ts +0 -0
  282. /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
  283. /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
  284. /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
  285. /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
  286. /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
  287. /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
  288. /package/examples/{components → components 2}/Dialog/index.ts +0 -0
  289. /package/examples/{components → components 2}/index.ts +0 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @file formatDate Utility Tests
3
+ * @description Comprehensive tests for the formatDate utility function
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
+ import { formatDate } from './formatting';
8
+
9
+ describe('formatDate Utility', () => {
10
+ let originalLocale: string;
11
+ let originalTimezone: string;
12
+
13
+ beforeEach(() => {
14
+ // Store original values
15
+ originalLocale = Intl.DateTimeFormat().resolvedOptions().locale;
16
+ originalTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
17
+ });
18
+
19
+ describe('Date Object Input', () => {
20
+ it('formats a Date object correctly', () => {
21
+ const date = new Date('2024-01-15T10:30:00.000Z');
22
+ const result = formatDate(date);
23
+
24
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
25
+ });
26
+
27
+ it('formats different Date objects', () => {
28
+ const dates = [
29
+ new Date('2023-12-25T00:00:00.000Z'),
30
+ new Date('2024-06-15T12:00:00.000Z'),
31
+ new Date('2024-12-31T23:59:59.999Z')
32
+ ];
33
+
34
+ dates.forEach(date => {
35
+ const result = formatDate(date);
36
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
37
+ });
38
+ });
39
+ });
40
+
41
+ describe('String Input', () => {
42
+ it('formats ISO date strings correctly', () => {
43
+ const result = formatDate('2024-01-15T10:30:00.000Z');
44
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
45
+ });
46
+
47
+ it('formats date-only strings correctly', () => {
48
+ const result = formatDate('2024-01-15');
49
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
50
+ });
51
+
52
+ it('formats various date string formats', () => {
53
+ const dateStrings = [
54
+ '2024-01-15',
55
+ '2024-01-15T10:30:00Z',
56
+ '2024-01-15T10:30:00.000Z',
57
+ '2024-01-15T10:30:00+00:00'
58
+ ];
59
+
60
+ dateStrings.forEach(dateString => {
61
+ const result = formatDate(dateString);
62
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
63
+ });
64
+ });
65
+ });
66
+
67
+ describe('Number Input', () => {
68
+ it('formats timestamp numbers correctly', () => {
69
+ const timestamp = new Date('2024-01-15T10:30:00.000Z').getTime();
70
+ const result = formatDate(timestamp);
71
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
72
+ });
73
+
74
+ it('formats Unix timestamps correctly', () => {
75
+ const unixTimestamp = Math.floor(new Date('2024-01-15T10:30:00.000Z').getTime() / 1000);
76
+ const result = formatDate(unixTimestamp * 1000); // Convert back to milliseconds
77
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
78
+ });
79
+ });
80
+
81
+ describe('Edge Cases', () => {
82
+ it('handles invalid date strings gracefully', () => {
83
+ const invalidDates = [
84
+ 'invalid-date',
85
+ 'not-a-date',
86
+ '2024-13-45', // Invalid month and day
87
+ '2024-02-30' // Invalid day for February
88
+ ];
89
+
90
+ invalidDates.forEach(invalidDate => {
91
+ const result = formatDate(invalidDate);
92
+ // Should return a string even for invalid dates (may be "Invalid Date")
93
+ expect(typeof result).toBe('string');
94
+ expect(result.length).toBeGreaterThan(0);
95
+ });
96
+ });
97
+
98
+ it('handles empty string input', () => {
99
+ const result = formatDate('');
100
+ expect(typeof result).toBe('string');
101
+ });
102
+
103
+ it('handles null and undefined input', () => {
104
+ // @ts-expect-error - Testing runtime behavior with invalid types
105
+ expect(() => formatDate(null)).toThrow();
106
+ // @ts-expect-error - Testing runtime behavior with invalid types
107
+ expect(() => formatDate(undefined)).toThrow();
108
+ });
109
+
110
+ it('handles very large numbers', () => {
111
+ const largeNumber = Number.MAX_SAFE_INTEGER;
112
+ const result = formatDate(largeNumber);
113
+ expect(typeof result).toBe('string');
114
+ });
115
+
116
+ it('handles negative numbers', () => {
117
+ const negativeNumber = -1000000;
118
+ const result = formatDate(negativeNumber);
119
+ expect(typeof result).toBe('string');
120
+ });
121
+
122
+ it('handles zero', () => {
123
+ const result = formatDate(0);
124
+ expect(typeof result).toBe('string');
125
+ });
126
+ });
127
+
128
+ describe('Format Consistency', () => {
129
+ it('returns consistent format for same date', () => {
130
+ const date = new Date('2024-01-15T10:30:00.000Z');
131
+ const result1 = formatDate(date);
132
+ const result2 = formatDate(date);
133
+
134
+ expect(result1).toBe(result2);
135
+ });
136
+
137
+ it('returns consistent format across different input types for same date', () => {
138
+ const date = new Date('2024-01-15T10:30:00.000Z');
139
+ const dateString = '2024-01-15T10:30:00.000Z';
140
+ const timestamp = date.getTime();
141
+
142
+ const result1 = formatDate(date);
143
+ const result2 = formatDate(dateString);
144
+ const result3 = formatDate(timestamp);
145
+
146
+ expect(result1).toBe(result2);
147
+ expect(result2).toBe(result3);
148
+ });
149
+ });
150
+
151
+ describe('Locale Handling', () => {
152
+ it('uses default locale when no locale is specified', () => {
153
+ const date = new Date('2024-01-15T10:30:00.000Z');
154
+ const result = formatDate(date);
155
+
156
+ // Should use the default locale format
157
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
158
+ });
159
+
160
+ it('handles different locales correctly', () => {
161
+ const date = new Date('2024-01-15T10:30:00.000Z');
162
+
163
+ const result = formatDate(date);
164
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
165
+ });
166
+ });
167
+
168
+ describe('Time Zone Handling', () => {
169
+ it('handles different time zones correctly', () => {
170
+ const date = new Date('2024-01-15T10:30:00.000Z');
171
+ const result = formatDate(date);
172
+
173
+ // Should format consistently regardless of timezone
174
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
175
+ });
176
+ });
177
+
178
+ describe('Performance', () => {
179
+ it('handles large number of calls efficiently', () => {
180
+ const date = new Date('2024-01-15T10:30:00.000Z');
181
+ const startTime = performance.now();
182
+
183
+ // Call formatDate many times
184
+ for (let i = 0; i < 1000; i++) {
185
+ formatDate(date);
186
+ }
187
+
188
+ const endTime = performance.now();
189
+ const duration = endTime - startTime;
190
+
191
+ // Should complete in reasonable time (less than 200ms for 1000 calls)
192
+ expect(duration).toBeLessThan(200);
193
+ });
194
+ });
195
+
196
+ describe('Type Safety', () => {
197
+ it('accepts valid Date types', () => {
198
+ const date = new Date();
199
+ expect(() => formatDate(date)).not.toThrow();
200
+ });
201
+
202
+ it('accepts valid string types', () => {
203
+ const dateString = '2024-01-15';
204
+ expect(() => formatDate(dateString)).not.toThrow();
205
+ });
206
+
207
+ it('accepts valid number types', () => {
208
+ const timestamp = Date.now();
209
+ expect(() => formatDate(timestamp)).not.toThrow();
210
+ });
211
+ });
212
+
213
+ describe('Return Value', () => {
214
+ it('returns a string', () => {
215
+ const date = new Date('2024-01-15T10:30:00.000Z');
216
+ const result = formatDate(date);
217
+
218
+ expect(typeof result).toBe('string');
219
+ });
220
+
221
+ it('returns non-empty string for valid dates', () => {
222
+ const date = new Date('2024-01-15T10:30:00.000Z');
223
+ const result = formatDate(date);
224
+
225
+ expect(result).toBeTruthy();
226
+ expect(result.length).toBeGreaterThan(0);
227
+ });
228
+
229
+ it('returns expected format pattern', () => {
230
+ const date = new Date('2024-01-15T10:30:00.000Z');
231
+ const result = formatDate(date);
232
+
233
+ // Should match pattern: "MMM DD, YYYY"
234
+ expect(result).toMatch(/^\d{1,2} [A-Za-z]{3} \d{4}$/);
235
+ });
236
+ });
237
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Utility functions for formatting data in the application
3
+ */
4
+
5
+ /**
6
+ * Format a date as a readable string in "dd mmm yyyy" format (e.g., "15 Jun 2024")
7
+ */
8
+ export function formatDate(date: Date | string | number): string {
9
+ const dateObj = typeof date === 'string' || typeof date === 'number'
10
+ ? new Date(date)
11
+ : date;
12
+
13
+ // Use 'en-GB' locale to ensure "dd mmm yyyy" format (e.g., "15 Jun 2024")
14
+ return dateObj.toLocaleDateString('en-GB', {
15
+ year: 'numeric',
16
+ month: 'short',
17
+ day: 'numeric'
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Format a number as a currency
23
+ */
24
+ export function formatCurrency(value: number, currencyCode = 'USD', locale = 'en-US'): string {
25
+ return new Intl.NumberFormat(locale, {
26
+ style: 'currency',
27
+ currency: currencyCode,
28
+ }).format(value);
29
+ }
30
+
31
+ /**
32
+ * Format a number with custom options
33
+ */
34
+ export function formatNumber(
35
+ value: number,
36
+ options: Intl.NumberFormatOptions = {},
37
+ locale = 'en-US'
38
+ ): string {
39
+ return new Intl.NumberFormat(locale, options).format(value);
40
+ }
41
+
42
+ /**
43
+ * Format a number as a percentage.
44
+ *
45
+ * The third parameter can be either:
46
+ * - A number for fixed decimal places (backward compatible): `formatPercent(0.81, 'en-US', 2)`
47
+ * - An options object with:
48
+ * - `decimals`: Fixed number of decimal places (default: 1)
49
+ * - `preserveDecimals`: Auto-detect and preserve decimal places from the input value
50
+ * - `maxDecimals`: Maximum decimal places when preserving (default: 10)
51
+ *
52
+ * @param value - The percentage value as a decimal (e.g., 0.81 for 0.81%)
53
+ * @param locale - The locale string (default: 'en-US')
54
+ * @param decimalsOrOptions - Either a number for fixed decimals, or an options object with:
55
+ * - `decimals` - Fixed number of decimal places (default: 1)
56
+ * - `preserveDecimals` - Auto-detect and preserve decimal places from the input value
57
+ * - `maxDecimals` - Maximum decimal places when preserving (default: 10)
58
+ * @returns Formatted percentage string (e.g., "0.81%", "81%")
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * // Fixed decimals (default behavior)
63
+ * formatPercent(0.5) // '0.5%'
64
+ * formatPercent(0.81, 'en-US', 1) // '0.8%' (loses precision)
65
+ *
66
+ * // Preserve decimal places dynamically
67
+ * formatPercent(0.81, 'en-US', { preserveDecimals: true }) // '0.81%'
68
+ * formatPercent(0.8123, 'en-US', { preserveDecimals: true, maxDecimals: 2 }) // '0.81%'
69
+ * ```
70
+ */
71
+ export function formatPercent(
72
+ value: number,
73
+ locale: string = 'en-US',
74
+ decimalsOrOptions?: number | {
75
+ decimals?: number;
76
+ preserveDecimals?: boolean;
77
+ maxDecimals?: number;
78
+ }
79
+ ): string {
80
+ let decimals: number;
81
+
82
+ // Backward compatibility: if decimalsOrOptions is a number, use it directly
83
+ if (typeof decimalsOrOptions === 'number') {
84
+ decimals = decimalsOrOptions;
85
+ } else if (decimalsOrOptions && typeof decimalsOrOptions === 'object') {
86
+ // New options object: check if we should preserve decimals
87
+ if (decimalsOrOptions.preserveDecimals) {
88
+ const valueStr = value.toString();
89
+ const decimalIndex = valueStr.indexOf('.');
90
+
91
+ if (decimalIndex !== -1) {
92
+ const detectedDecimals = valueStr.length - decimalIndex - 1;
93
+ const maxDecimals = decimalsOrOptions.maxDecimals ?? 10;
94
+ decimals = Math.min(detectedDecimals, maxDecimals);
95
+ } else {
96
+ decimals = 0;
97
+ }
98
+ } else {
99
+ decimals = decimalsOrOptions.decimals ?? 1;
100
+ }
101
+ } else {
102
+ decimals = 1;
103
+ }
104
+
105
+ return new Intl.NumberFormat(locale, {
106
+ style: 'percent',
107
+ minimumFractionDigits: decimals,
108
+ maximumFractionDigits: decimals,
109
+ }).format(value / 100);
110
+ }
111
+
112
+ /**
113
+ * Format a large number with abbreviations (K, M, B)
114
+ */
115
+ export function formatCompactNumber(value: number, locale = 'en-US'): string {
116
+ return new Intl.NumberFormat(locale, {
117
+ notation: 'compact',
118
+ compactDisplay: 'short'
119
+ }).format(value);
120
+ }
121
+
122
+ /**
123
+ * Format a file size in bytes to a human-readable string
124
+ */
125
+ export function formatFileSize(bytes: number): string {
126
+ if (bytes === 0) return '0 Bytes';
127
+
128
+ const k = 1024;
129
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
130
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
131
+
132
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
133
+ }
@@ -18,6 +18,13 @@ export * from './app/appIdResolver';
18
18
 
19
19
  // Validation and sanitization
20
20
  export * from './validation';
21
+ // Explicitly re-export commonly used validation utilities to ensure they're available
22
+ // Import from source modules to avoid re-export issues
23
+ export { validateUserInput, usernameSchema } from './validation/validationUtils';
24
+ export { sanitizeUserInput, sanitizeFormData } from './validation/sanitization';
25
+ export { emailSchema, nameSchema, phoneSchema, urlSchema } from './validation/common';
26
+ export { passwordSchema } from './validation/passwordSchema';
27
+ export { pickSchema, combineSchemas } from './validation/schema';
21
28
 
22
29
  // Security utilities
23
30
  export {
@@ -0,0 +1,44 @@
1
+ import React, { Suspense, ComponentType, lazy } from 'react';
2
+ import { LoadingSpinner } from '../components/LoadingSpinner/LoadingSpinner';
3
+
4
+ interface LazyLoadOptions {
5
+ fallback?: React.ReactNode;
6
+ errorBoundary?: ComponentType<{ children: React.ReactNode }>;
7
+ }
8
+
9
+ /**
10
+ * Create a lazy-loaded component with error boundary and loading fallback
11
+ */
12
+ export function createLazyComponent<T extends ComponentType<any>>(
13
+ importFn: () => Promise<{ default: T }>,
14
+ componentName: string,
15
+ options: LazyLoadOptions = {}
16
+ ): T {
17
+ const LazyComponent = lazy(importFn);
18
+
19
+ const WrappedComponent = (props: any) => {
20
+ const content = (
21
+ <Suspense fallback={options.fallback || <LoadingSpinner />}>
22
+ <LazyComponent {...props} />
23
+ </Suspense>
24
+ );
25
+
26
+ if (options.errorBoundary) {
27
+ const ErrorBoundary = options.errorBoundary;
28
+ return <ErrorBoundary>{content}</ErrorBoundary>;
29
+ }
30
+
31
+ return content;
32
+ };
33
+
34
+ WrappedComponent.displayName = `Lazy${componentName}`;
35
+ return WrappedComponent as T;
36
+ }
37
+
38
+ /**
39
+ * Lazy-loaded DataTable component
40
+ */
41
+ export const LazyDataTable = createLazyComponent(
42
+ () => import('../components/DataTable').then(module => ({ default: module.DataTable })),
43
+ 'DataTable'
44
+ );
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @file Production-Safe Logger Utility
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Logger
5
+ * @since 0.4.76
6
+ */
7
+
8
+ /**
9
+ * Logger levels for different types of messages
10
+ */
11
+ export enum LogLevel {
12
+ DEBUG = 0,
13
+ INFO = 1,
14
+ WARN = 2,
15
+ ERROR = 3,
16
+ }
17
+
18
+ /**
19
+ * Logger configuration interface
20
+ */
21
+ export interface LoggerConfig {
22
+ /** Minimum log level to output */
23
+ level: LogLevel;
24
+ /** Whether to include timestamps */
25
+ includeTimestamp: boolean;
26
+ /** Whether to include component names */
27
+ includeComponent: boolean;
28
+ /** Custom prefix for all log messages */
29
+ prefix?: string;
30
+ }
31
+
32
+ /**
33
+ * Production-safe logger that no-ops in production builds
34
+ * Only outputs logs in development mode to prevent production console spam
35
+ */
36
+ export class Logger {
37
+ private static config: LoggerConfig = {
38
+ level: LogLevel.DEBUG,
39
+ includeTimestamp: false,
40
+ includeComponent: true,
41
+ };
42
+
43
+ /**
44
+ * Check if we're in development mode
45
+ */
46
+ private static get isDevelopment(): boolean {
47
+ return import.meta.env.MODE === 'development';
48
+ }
49
+
50
+ /**
51
+ * Configure the logger
52
+ */
53
+ static configure(config: Partial<LoggerConfig>): void {
54
+ this.config = { ...this.config, ...config };
55
+ }
56
+
57
+ /**
58
+ * Format a log message with optional timestamp and component
59
+ */
60
+ private static formatMessage(
61
+ level: LogLevel,
62
+ component: string,
63
+ message: string
64
+ ): string {
65
+ const parts: string[] = [];
66
+
67
+ if (this.config.prefix) {
68
+ parts.push(`[${this.config.prefix}]`);
69
+ }
70
+
71
+ if (this.config.includeTimestamp) {
72
+ parts.push(`[${new Date().toISOString()}]`);
73
+ }
74
+
75
+ parts.push(`[${LogLevel[level]}]`);
76
+
77
+ if (this.config.includeComponent && component) {
78
+ parts.push(`[${component}]`);
79
+ }
80
+
81
+ parts.push(message);
82
+
83
+ return parts.join(' ');
84
+ }
85
+
86
+ /**
87
+ * Check if a log level should be output
88
+ */
89
+ private static shouldLog(level: LogLevel): boolean {
90
+ return this.isDevelopment && level >= this.config.level;
91
+ }
92
+
93
+ /**
94
+ * Log debug information (development only)
95
+ */
96
+ static debug(component: string, message: string, ...args: any[]): void {
97
+ if (this.shouldLog(LogLevel.DEBUG)) {
98
+ try {
99
+ console.debug(this.formatMessage(LogLevel.DEBUG, component, message), ...args);
100
+ } catch (e) {
101
+ // Gracefully handle console method errors
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Log info information (development only)
108
+ */
109
+ static info(component: string, message: string, ...args: any[]): void {
110
+ if (this.shouldLog(LogLevel.INFO)) {
111
+ try {
112
+ console.info(this.formatMessage(LogLevel.INFO, component, message), ...args);
113
+ } catch (e) {
114
+ // Gracefully handle console method errors
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Log warning information (development only)
121
+ */
122
+ static warn(component: string, message: string, ...args: any[]): void {
123
+ if (this.shouldLog(LogLevel.WARN)) {
124
+ try {
125
+ console.warn(this.formatMessage(LogLevel.WARN, component, message), ...args);
126
+ } catch (e) {
127
+ // Gracefully handle console method errors
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Log error information (development only)
134
+ */
135
+ static error(component: string, message: string, ...args: any[]): void {
136
+ if (this.shouldLog(LogLevel.ERROR)) {
137
+ try {
138
+ console.error(this.formatMessage(LogLevel.ERROR, component, message), ...args);
139
+ } catch (e) {
140
+ // Gracefully handle console method errors
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Create a scoped logger for a specific component
147
+ */
148
+ static createScopedLogger(component: string) {
149
+ return {
150
+ debug: (message: string, ...args: any[]) => this.debug(component, message, ...args),
151
+ info: (message: string, ...args: any[]) => this.info(component, message, ...args),
152
+ warn: (message: string, ...args: any[]) => this.warn(component, message, ...args),
153
+ error: (message: string, ...args: any[]) => this.error(component, message, ...args),
154
+ };
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Default logger instance for convenience
160
+ */
161
+ export const logger = {
162
+ debug: (component: string, message: string, ...args: any[]) =>
163
+ Logger.debug(component, message, ...args),
164
+ info: (component: string, message: string, ...args: any[]) =>
165
+ Logger.info(component, message, ...args),
166
+ warn: (component: string, message: string, ...args: any[]) =>
167
+ Logger.warn(component, message, ...args),
168
+ error: (component: string, message: string, ...args: any[]) =>
169
+ Logger.error(component, message, ...args),
170
+ createScopedLogger: (component: string) => Logger.createScopedLogger(component),
171
+ configure: (config: Partial<LoggerConfig>) => Logger.configure(config),
172
+ };
173
+
174
+ /**
175
+ * Convenience function to create a scoped logger
176
+ */
177
+ export function createLogger(component: string) {
178
+ return Logger.createScopedLogger(component);
179
+ }