@jmruthers/pace-core 0.5.183 → 0.5.185

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 (307) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +60 -1
  3. package/core-usage-manifest.json +312 -0
  4. package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
  5. package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
  6. package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-BABf6JCh.d.ts} +21 -10
  7. package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
  8. package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
  9. package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
  10. package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
  11. package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
  12. package/dist/{chunk-CSOFYHAG.js → chunk-AISXLWGZ.js} +374 -60
  13. package/dist/chunk-AISXLWGZ.js.map +1 -0
  14. package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
  15. package/dist/chunk-FXFJRTKI.js.map +1 -0
  16. package/dist/{chunk-QETLRQI6.js → chunk-HC67NW5K.js} +380 -360
  17. package/dist/chunk-HC67NW5K.js.map +1 -0
  18. package/dist/chunk-HESYZWZW.js +388 -0
  19. package/dist/chunk-HESYZWZW.js.map +1 -0
  20. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  21. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  22. package/dist/{chunk-UHNYIBXL.js → chunk-IXSNYUCT.js} +1 -1
  23. package/dist/chunk-IXSNYUCT.js.map +1 -0
  24. package/dist/{chunk-MI7HBHN3.js → chunk-MX3EIJGQ.js} +4 -3
  25. package/dist/{chunk-MI7HBHN3.js.map → chunk-MX3EIJGQ.js.map} +1 -1
  26. package/dist/{chunk-PWAHJW4G.js → chunk-OKI34GZD.js} +86 -33
  27. package/dist/chunk-OKI34GZD.js.map +1 -0
  28. package/dist/{chunk-W22JP75J.js → chunk-STTZQK2I.js} +3 -3
  29. package/dist/chunk-THRPYOFK.js +215 -0
  30. package/dist/chunk-THRPYOFK.js.map +1 -0
  31. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  32. package/dist/chunk-U6WNSFX5.js.map +1 -0
  33. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  34. package/dist/components.d.ts +182 -6
  35. package/dist/components.js +157 -11
  36. package/dist/components.js.map +1 -1
  37. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  38. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-BjR39ktt.d.ts} +7 -1
  39. package/dist/hooks.d.ts +7 -14
  40. package/dist/hooks.js +10 -22
  41. package/dist/hooks.js.map +1 -1
  42. package/dist/index.d.ts +11 -11
  43. package/dist/index.js +79 -16
  44. package/dist/index.js.map +1 -1
  45. package/dist/providers.d.ts +1 -1
  46. package/dist/providers.js +3 -1
  47. package/dist/rbac/index.d.ts +205 -14
  48. package/dist/rbac/index.js +28 -6
  49. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  50. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  51. package/dist/types.d.ts +1 -1
  52. package/dist/types.js +1 -1
  53. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-CvnC3d-e.d.ts} +113 -2
  54. package/dist/utils.d.ts +109 -151
  55. package/dist/utils.js +128 -138
  56. package/dist/utils.js.map +1 -1
  57. package/docs/api/README.md +60 -1
  58. package/docs/api/classes/ColumnFactory.md +1 -1
  59. package/docs/api/classes/ErrorBoundary.md +1 -1
  60. package/docs/api/classes/InvalidScopeError.md +1 -1
  61. package/docs/api/classes/Logger.md +178 -0
  62. package/docs/api/classes/MissingUserContextError.md +1 -1
  63. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  64. package/docs/api/classes/PermissionDeniedError.md +1 -1
  65. package/docs/api/classes/RBACAuditManager.md +2 -2
  66. package/docs/api/classes/RBACCache.md +1 -1
  67. package/docs/api/classes/RBACEngine.md +2 -2
  68. package/docs/api/classes/RBACError.md +1 -1
  69. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  70. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  71. package/docs/api/classes/StorageUtils.md +1 -1
  72. package/docs/api/enums/FileCategory.md +1 -1
  73. package/docs/api/enums/LogLevel.md +54 -0
  74. package/docs/api/enums/RBACErrorCode.md +1 -1
  75. package/docs/api/enums/RPCFunction.md +1 -1
  76. package/docs/api/interfaces/AggregateConfig.md +1 -1
  77. package/docs/api/interfaces/BadgeProps.md +1 -1
  78. package/docs/api/interfaces/ButtonProps.md +1 -1
  79. package/docs/api/interfaces/CalendarProps.md +18 -2
  80. package/docs/api/interfaces/CardProps.md +1 -1
  81. package/docs/api/interfaces/ColorPalette.md +1 -1
  82. package/docs/api/interfaces/ColorShade.md +1 -1
  83. package/docs/api/interfaces/ComplianceResult.md +30 -0
  84. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  85. package/docs/api/interfaces/DataRecord.md +1 -1
  86. package/docs/api/interfaces/DataTableAction.md +1 -1
  87. package/docs/api/interfaces/DataTableColumn.md +1 -1
  88. package/docs/api/interfaces/DataTableProps.md +1 -1
  89. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  90. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  91. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  92. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  93. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  94. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  95. package/docs/api/interfaces/ExportColumn.md +1 -1
  96. package/docs/api/interfaces/ExportOptions.md +1 -1
  97. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  98. package/docs/api/interfaces/FileMetadata.md +1 -1
  99. package/docs/api/interfaces/FileReference.md +1 -1
  100. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  101. package/docs/api/interfaces/FileUploadOptions.md +24 -8
  102. package/docs/api/interfaces/FileUploadProps.md +24 -13
  103. package/docs/api/interfaces/FooterProps.md +1 -1
  104. package/docs/api/interfaces/FormFieldProps.md +1 -1
  105. package/docs/api/interfaces/FormProps.md +1 -1
  106. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  107. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  108. package/docs/api/interfaces/InputProps.md +1 -1
  109. package/docs/api/interfaces/LabelProps.md +1 -1
  110. package/docs/api/interfaces/LoggerConfig.md +62 -0
  111. package/docs/api/interfaces/LoginFormProps.md +1 -1
  112. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  113. package/docs/api/interfaces/NavigationContextType.md +1 -1
  114. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  115. package/docs/api/interfaces/NavigationItem.md +1 -1
  116. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  117. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  118. package/docs/api/interfaces/Organisation.md +1 -1
  119. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  120. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  121. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  122. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  123. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  124. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  125. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  126. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  127. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  128. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  129. package/docs/api/interfaces/PaletteData.md +1 -1
  130. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  131. package/docs/api/interfaces/ProgressProps.md +1 -1
  132. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  134. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  135. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  136. package/docs/api/interfaces/QuickFix.md +52 -0
  137. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  138. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  139. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  140. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  141. package/docs/api/interfaces/RBACConfig.md +4 -4
  142. package/docs/api/interfaces/RBACContext.md +1 -1
  143. package/docs/api/interfaces/RBACLogger.md +1 -1
  144. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  149. package/docs/api/interfaces/RBACResult.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  151. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  157. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  159. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  160. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  161. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  162. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  163. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  164. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  165. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  166. package/docs/api/interfaces/RouteConfig.md +1 -1
  167. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  168. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  169. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  170. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  171. package/docs/api/interfaces/SetupIssue.md +41 -0
  172. package/docs/api/interfaces/StorageConfig.md +1 -1
  173. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  174. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  175. package/docs/api/interfaces/StorageListOptions.md +1 -1
  176. package/docs/api/interfaces/StorageListResult.md +1 -1
  177. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  178. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  179. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  180. package/docs/api/interfaces/StyleImport.md +1 -1
  181. package/docs/api/interfaces/SwitchProps.md +1 -1
  182. package/docs/api/interfaces/TabsContentProps.md +1 -1
  183. package/docs/api/interfaces/TabsListProps.md +1 -1
  184. package/docs/api/interfaces/TabsProps.md +1 -1
  185. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  186. package/docs/api/interfaces/TextareaProps.md +1 -1
  187. package/docs/api/interfaces/ToastActionElement.md +1 -1
  188. package/docs/api/interfaces/ToastProps.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  190. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  192. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  193. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  194. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  196. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  198. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  200. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  202. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  203. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  204. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  205. package/docs/api/interfaces/UserEventAccess.md +1 -1
  206. package/docs/api/interfaces/UserMenuProps.md +1 -1
  207. package/docs/api/interfaces/UserProfile.md +1 -1
  208. package/docs/api/modules.md +738 -42
  209. package/docs/api-reference/hooks.md +111 -0
  210. package/docs/api-reference/rpc-functions.md +1 -1
  211. package/docs/api-reference/utilities.md +184 -0
  212. package/docs/getting-started/installation-guide.md +75 -16
  213. package/docs/getting-started/quick-start.md +61 -11
  214. package/docs/implementation-guides/authentication.md +88 -12
  215. package/docs/implementation-guides/file-reference-system.md +2 -1
  216. package/docs/implementation-guides/file-upload-storage.md +21 -0
  217. package/docs/rbac/README.md +1 -0
  218. package/docs/rbac/compliance/compliance-guide.md +544 -0
  219. package/docs/rbac/getting-started.md +158 -33
  220. package/docs/standards/pace-core-compliance.md +432 -0
  221. package/eslint-config-pace-core.cjs +93 -0
  222. package/package.json +15 -3
  223. package/scripts/analyze-bundle.js +232 -0
  224. package/scripts/build-css.js +56 -0
  225. package/scripts/build-docs-incremental.js +1015 -0
  226. package/scripts/check-pace-core-compliance.cjs +2353 -0
  227. package/scripts/generate-docs.js +157 -0
  228. package/scripts/setup-build-cache.js +73 -0
  229. package/scripts/utils/command-runner.js +131 -0
  230. package/scripts/utils/env.js +33 -0
  231. package/scripts/utils/index.js +10 -0
  232. package/scripts/utils/logger.js +88 -0
  233. package/scripts/utils/path-helpers.js +37 -0
  234. package/scripts/validate-formats.js +133 -0
  235. package/scripts/validate-master.js +155 -0
  236. package/scripts/validate-pre-publish.js +140 -0
  237. package/scripts/validate-theme.js +142 -0
  238. package/src/components/Calendar/Calendar.tsx +8 -1
  239. package/src/components/Card/Card.tsx +47 -8
  240. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  241. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  242. package/src/components/DatePickerWithTimezone/README.md +135 -0
  243. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  244. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  245. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  246. package/src/components/DateTimeField/README.md +148 -0
  247. package/src/components/DateTimeField/index.ts +10 -0
  248. package/src/components/FileUpload/FileUpload.tsx +3 -0
  249. package/src/components/Header/Header.test.tsx +47 -18
  250. package/src/components/Header/Header.tsx +24 -6
  251. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  252. package/src/components/PaceAppLayout/README.md +9 -0
  253. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  254. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  255. package/src/components/ProtectedRoute/ProtectedRoute.tsx +12 -4
  256. package/src/components/index.ts +8 -0
  257. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  258. package/src/eslint-rules/pace-core-compliance.js +640 -0
  259. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  260. package/src/hooks/index.ts +2 -0
  261. package/src/hooks/useFileReference.test.ts +1 -0
  262. package/src/hooks/useFormDialog.ts +147 -0
  263. package/src/index.ts +27 -0
  264. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  265. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  266. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  267. package/src/rbac/compliance/database-validator.ts +165 -0
  268. package/src/rbac/compliance/index.ts +38 -0
  269. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  270. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  271. package/src/rbac/compliance/setup-validator.ts +131 -0
  272. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  273. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  274. package/src/rbac/docs/event-based-apps.md +285 -0
  275. package/src/rbac/errors.ts +11 -0
  276. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  277. package/src/rbac/index.ts +30 -0
  278. package/src/services/OrganisationService.ts +4 -0
  279. package/src/types/file-reference.ts +6 -0
  280. package/src/utils/__tests__/timezone.test.ts +345 -0
  281. package/src/utils/file-reference/__tests__/file-reference.test.ts +2 -0
  282. package/src/utils/file-reference/index.ts +1 -0
  283. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  284. package/src/utils/formatting/formatting.ts +179 -0
  285. package/src/utils/index.ts +27 -1
  286. package/src/utils/location/index.ts +16 -0
  287. package/src/utils/location/location.test.ts +286 -0
  288. package/src/utils/location/location.ts +175 -0
  289. package/src/utils/timezone/index.ts +17 -0
  290. package/src/utils/timezone/timezone.test.ts +349 -0
  291. package/src/utils/timezone/timezone.ts +281 -0
  292. package/dist/chunk-CSOFYHAG.js.map +0 -1
  293. package/dist/chunk-FUEYYMX5.js.map +0 -1
  294. package/dist/chunk-HKIT6O7W.js +0 -198
  295. package/dist/chunk-HKIT6O7W.js.map +0 -1
  296. package/dist/chunk-KUEN3HFB.js +0 -94
  297. package/dist/chunk-KUEN3HFB.js.map +0 -1
  298. package/dist/chunk-M7W4CP3M.js.map +0 -1
  299. package/dist/chunk-PWAHJW4G.js.map +0 -1
  300. package/dist/chunk-QETLRQI6.js.map +0 -1
  301. package/dist/chunk-UHNYIBXL.js.map +0 -1
  302. package/dist/formatting-5wETwiGF.d.ts +0 -162
  303. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  304. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  305. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  306. /package/dist/{chunk-W22JP75J.js.map → chunk-STTZQK2I.js.map} +0 -0
  307. /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @file Timezone Utilities Exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Timezone
5
+ * @since 0.1.0
6
+ */
7
+
8
+ export {
9
+ formatInTimeZone,
10
+ getTimezoneAbbreviation,
11
+ formatTimeInTimeZone,
12
+ getUserTimeZone,
13
+ toZonedTime,
14
+ fromZonedTime,
15
+ roundToNearestMinutes,
16
+ getTimeZoneDifference
17
+ } from './timezone';
@@ -0,0 +1,349 @@
1
+ /**
2
+ * @file Timezone Utilities Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Timezone/__tests__
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive tests for timezone utility functions.
8
+ * Tests cover all major functionality, edge cases, and error handling.
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import {
13
+ formatInTimeZone,
14
+ getTimezoneAbbreviation,
15
+ formatTimeInTimeZone,
16
+ getUserTimeZone,
17
+ toZonedTime,
18
+ fromZonedTime,
19
+ roundToNearestMinutes,
20
+ getTimeZoneDifference
21
+ } from './timezone';
22
+
23
+ describe('Timezone Utilities', () => {
24
+ describe('formatInTimeZone', () => {
25
+ it('formats a Date object in a specific timezone', () => {
26
+ const date = new Date('2024-01-15T10:00:00Z');
27
+ const result = formatInTimeZone(date, 'America/New_York', 'MMM dd, yyyy HH:mm');
28
+ expect(result).toMatch(/Jan 15, 2024/);
29
+ expect(result).toMatch(/\d{2}:\d{2}/);
30
+ });
31
+
32
+ it('formats an ISO string in a specific timezone', () => {
33
+ const result = formatInTimeZone('2024-01-15T10:00:00Z', 'America/New_York', 'MMM dd, yyyy');
34
+ expect(result).toMatch(/Jan 15, 2024/);
35
+ });
36
+
37
+ it('formats a timestamp in a specific timezone', () => {
38
+ const timestamp = new Date('2024-01-15T10:00:00Z').getTime();
39
+ const result = formatInTimeZone(timestamp, 'America/New_York', 'yyyy-MM-dd');
40
+ expect(result).toBe('2024-01-15');
41
+ });
42
+
43
+ it('returns "Invalid date" for invalid date input', () => {
44
+ const result = formatInTimeZone(new Date('invalid'), 'America/New_York', 'MMM dd, yyyy');
45
+ expect(result).toBe('Invalid date');
46
+ });
47
+
48
+ it('returns "Invalid date" for invalid timezone', () => {
49
+ const date = new Date('2024-01-15T10:00:00Z');
50
+ const result = formatInTimeZone(date, '', 'MMM dd, yyyy');
51
+ expect(result).toBe('Invalid date');
52
+ });
53
+
54
+ it('returns "Invalid date" for non-string timezone', () => {
55
+ const date = new Date('2024-01-15T10:00:00Z');
56
+ // @ts-expect-error - Testing invalid input
57
+ const result = formatInTimeZone(date, null, 'MMM dd, yyyy');
58
+ expect(result).toBe('Invalid date');
59
+ });
60
+
61
+ it('handles different timezones correctly', () => {
62
+ const date = new Date('2024-01-15T10:00:00Z');
63
+ const ny = formatInTimeZone(date, 'America/New_York', 'HH:mm');
64
+ const la = formatInTimeZone(date, 'America/Los_Angeles', 'HH:mm');
65
+ expect(ny).not.toBe(la);
66
+ });
67
+ });
68
+
69
+ describe('getTimezoneAbbreviation', () => {
70
+ it('returns timezone abbreviation for valid date and timezone', () => {
71
+ const date = new Date('2024-01-15T10:00:00Z');
72
+ const result = getTimezoneAbbreviation(date, 'America/New_York');
73
+ expect(typeof result).toBe('string');
74
+ expect(result.length).toBeGreaterThan(0);
75
+ });
76
+
77
+ it('returns timezone name as fallback when abbreviation unavailable', () => {
78
+ const date = new Date('2024-01-15T10:00:00Z');
79
+ const result = getTimezoneAbbreviation(date, 'America/New_York');
80
+ expect(result).toBeTruthy();
81
+ });
82
+
83
+ it('returns timezone string for invalid date', () => {
84
+ const result = getTimezoneAbbreviation(new Date('invalid'), 'America/New_York');
85
+ expect(result).toBe('America/New_York');
86
+ });
87
+
88
+ it('returns "UTC" for empty timezone', () => {
89
+ const date = new Date('2024-01-15T10:00:00Z');
90
+ const result = getTimezoneAbbreviation(date, '');
91
+ expect(result).toBe('UTC');
92
+ });
93
+
94
+ it('handles invalid timezone gracefully', () => {
95
+ const date = new Date('2024-01-15T10:00:00Z');
96
+ const result = getTimezoneAbbreviation(date, 'Invalid/Timezone');
97
+ expect(typeof result).toBe('string');
98
+ });
99
+ });
100
+
101
+ describe('formatTimeInTimeZone', () => {
102
+ it('formats time only in specific timezone', () => {
103
+ const date = new Date('2024-01-15T10:00:00Z');
104
+ const result = formatTimeInTimeZone(date, 'America/New_York');
105
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
106
+ });
107
+
108
+ it('formats ISO string time in timezone', () => {
109
+ const result = formatTimeInTimeZone('2024-01-15T10:00:00Z', 'America/New_York');
110
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
111
+ });
112
+
113
+ it('returns "Invalid date" for invalid input', () => {
114
+ const result = formatTimeInTimeZone(new Date('invalid'), 'America/New_York');
115
+ expect(result).toBe('Invalid date');
116
+ });
117
+ });
118
+
119
+ describe('getUserTimeZone', () => {
120
+ it('returns IANA timezone string', () => {
121
+ const result = getUserTimeZone();
122
+ expect(typeof result).toBe('string');
123
+ expect(result.length).toBeGreaterThan(0);
124
+ });
125
+
126
+ it('returns "UTC" as fallback when Intl is unavailable', () => {
127
+ const originalIntl = global.Intl;
128
+ // @ts-expect-error - Testing fallback behavior
129
+ global.Intl = undefined;
130
+ const result = getUserTimeZone();
131
+ expect(result).toBe('UTC');
132
+ global.Intl = originalIntl;
133
+ });
134
+
135
+ it('returns "UTC" on error', () => {
136
+ const originalDateTimeFormat = Intl.DateTimeFormat;
137
+ // @ts-expect-error - Testing error handling
138
+ Intl.DateTimeFormat = () => {
139
+ throw new Error('Test error');
140
+ };
141
+ const result = getUserTimeZone();
142
+ expect(result).toBe('UTC');
143
+ Intl.DateTimeFormat = originalDateTimeFormat;
144
+ });
145
+ });
146
+
147
+ describe('toZonedTime', () => {
148
+ it('converts UTC date to timezone local time', () => {
149
+ const utcDate = new Date('2024-01-15T10:00:00Z');
150
+ const result = toZonedTime(utcDate, 'America/New_York');
151
+ expect(result).toBeInstanceOf(Date);
152
+ expect(result.getTime()).not.toBe(utcDate.getTime());
153
+ });
154
+
155
+ it('returns original date for invalid timezone', () => {
156
+ const date = new Date('2024-01-15T10:00:00Z');
157
+ const result = toZonedTime(date, '');
158
+ expect(result).toBe(date);
159
+ });
160
+
161
+ it('returns original date for invalid date', () => {
162
+ const invalidDate = new Date('invalid');
163
+ const result = toZonedTime(invalidDate, 'America/New_York');
164
+ expect(result).toBe(invalidDate);
165
+ });
166
+
167
+ it('handles different timezones correctly', () => {
168
+ const utcDate = new Date('2024-01-15T10:00:00Z');
169
+ const ny = toZonedTime(utcDate, 'America/New_York');
170
+ const la = toZonedTime(utcDate, 'America/Los_Angeles');
171
+ expect(ny.getTime()).not.toBe(la.getTime());
172
+ });
173
+
174
+ it('returns original date on error', () => {
175
+ const date = new Date('2024-01-15T10:00:00Z');
176
+ // @ts-expect-error - Testing error handling
177
+ const result = toZonedTime(date, null);
178
+ expect(result).toBe(date);
179
+ });
180
+ });
181
+
182
+ describe('fromZonedTime', () => {
183
+ it('converts local time in timezone to UTC', () => {
184
+ const localDate = new Date(2024, 0, 15, 10, 0); // Jan 15, 2024 10:00 AM local
185
+ const result = fromZonedTime(localDate, 'America/New_York');
186
+ expect(result).toBeInstanceOf(Date);
187
+ });
188
+
189
+ it('returns original date for invalid timezone', () => {
190
+ const date = new Date(2024, 0, 15, 10, 0);
191
+ const result = fromZonedTime(date, '');
192
+ expect(result).toBe(date);
193
+ });
194
+
195
+ it('returns original date for invalid date', () => {
196
+ const invalidDate = new Date('invalid');
197
+ const result = fromZonedTime(invalidDate, 'America/New_York');
198
+ expect(result).toBe(invalidDate);
199
+ });
200
+
201
+ it('handles round-trip conversion', () => {
202
+ const utcDate = new Date('2024-01-15T10:00:00Z');
203
+ const zoned = toZonedTime(utcDate, 'America/New_York');
204
+ const backToUtc = fromZonedTime(zoned, 'America/New_York');
205
+ // Should be close to original (within a few milliseconds due to DST handling)
206
+ expect(Math.abs(backToUtc.getTime() - utcDate.getTime())).toBeLessThan(1000);
207
+ });
208
+
209
+ it('returns original date on error', () => {
210
+ const date = new Date(2024, 0, 15, 10, 0);
211
+ // @ts-expect-error - Testing error handling
212
+ const result = fromZonedTime(date, null);
213
+ expect(result).toBe(date);
214
+ });
215
+ });
216
+
217
+ describe('roundToNearestMinutes', () => {
218
+ it('rounds to nearest 5 minutes by default', () => {
219
+ const date = new Date('2024-01-15T10:23:00Z');
220
+ const result = roundToNearestMinutes(date);
221
+ expect(result.getMinutes() % 5).toBe(0);
222
+ });
223
+
224
+ it('rounds to nearest specified minutes', () => {
225
+ const date = new Date('2024-01-15T10:23:00Z');
226
+ const result = roundToNearestMinutes(date, 10);
227
+ expect(result.getMinutes() % 10).toBe(0);
228
+ });
229
+
230
+ it('rounds up when closer to next interval', () => {
231
+ const date = new Date('2024-01-15T10:28:00Z');
232
+ const result = roundToNearestMinutes(date, 5);
233
+ expect(result.getMinutes()).toBe(30);
234
+ });
235
+
236
+ it('rounds down when closer to previous interval', () => {
237
+ const date = new Date('2024-01-15T10:22:00Z');
238
+ const result = roundToNearestMinutes(date, 5);
239
+ expect(result.getMinutes()).toBe(20);
240
+ });
241
+
242
+ it('returns original date for invalid date', () => {
243
+ const invalidDate = new Date('invalid');
244
+ const result = roundToNearestMinutes(invalidDate, 5);
245
+ expect(result).toBe(invalidDate);
246
+ });
247
+
248
+ it('returns original date for invalid minutesStep', () => {
249
+ const date = new Date('2024-01-15T10:23:00Z');
250
+ const result = roundToNearestMinutes(date, 0);
251
+ expect(date).toBe(date);
252
+ });
253
+
254
+ it('returns original date for negative minutesStep', () => {
255
+ const date = new Date('2024-01-15T10:23:00Z');
256
+ const result = roundToNearestMinutes(date, -5);
257
+ expect(result).toBe(date);
258
+ });
259
+
260
+ it('returns original date for non-integer minutesStep', () => {
261
+ const date = new Date('2024-01-15T10:23:00Z');
262
+ const result = roundToNearestMinutes(date, 5.5);
263
+ expect(result).toBe(date);
264
+ });
265
+
266
+ it('handles edge case of exact minute', () => {
267
+ const date = new Date('2024-01-15T10:25:00Z');
268
+ const result = roundToNearestMinutes(date, 5);
269
+ expect(result.getMinutes()).toBe(25);
270
+ });
271
+ });
272
+
273
+ describe('getTimeZoneDifference', () => {
274
+ it('calculates difference between two timezones', () => {
275
+ const result = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
276
+ expect(typeof result).toBe('number');
277
+ // Los Angeles is typically 3 hours behind New York
278
+ expect(result).toBeLessThan(0);
279
+ });
280
+
281
+ it('returns 0 for same timezone', () => {
282
+ const result = getTimeZoneDifference('America/New_York', 'America/New_York');
283
+ expect(result).toBe(0);
284
+ });
285
+
286
+ it('returns 0 for invalid timezones', () => {
287
+ const result = getTimeZoneDifference('', 'America/New_York');
288
+ expect(result).toBe(0);
289
+ });
290
+
291
+ it('returns 0 for non-string timezones', () => {
292
+ // @ts-expect-error - Testing invalid input
293
+ const result = getTimeZoneDifference(null, 'America/New_York');
294
+ expect(result).toBe(0);
295
+ });
296
+
297
+ it('handles DST correctly', () => {
298
+ // Test in summer (EDT vs PDT = 3 hours)
299
+ const summerDate = new Date('2024-07-15T10:00:00Z');
300
+ const result = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
301
+ // Should be negative (LA behind NY)
302
+ expect(result).toBeLessThan(0);
303
+ });
304
+
305
+ it('returns 0 on error', () => {
306
+ // This test verifies error handling path
307
+ const result = getTimeZoneDifference('Invalid/Timezone1', 'Invalid/Timezone2');
308
+ expect(result).toBe(0);
309
+ });
310
+ });
311
+
312
+ describe('Edge Cases', () => {
313
+ it('handles null and undefined inputs gracefully', () => {
314
+ // @ts-expect-error - Testing edge cases
315
+ expect(formatInTimeZone(null, 'America/New_York', 'yyyy-MM-dd')).toBe('Invalid date');
316
+ // @ts-expect-error - Testing edge cases
317
+ expect(formatInTimeZone(undefined, 'America/New_York', 'yyyy-MM-dd')).toBe('Invalid date');
318
+ });
319
+
320
+ it('handles extreme dates', () => {
321
+ const farFuture = new Date('2099-12-31T23:59:59Z');
322
+ const result = formatInTimeZone(farFuture, 'America/New_York', 'yyyy-MM-dd');
323
+ expect(result).toMatch(/2099-12-31/);
324
+
325
+ const farPast = new Date('1900-01-01T00:00:00Z');
326
+ const result2 = formatInTimeZone(farPast, 'America/New_York', 'yyyy-MM-dd');
327
+ // Timezone conversion may shift the date to previous day due to timezone offset
328
+ // Accept either 1900-01-01 or 1899-12-31 depending on timezone offset
329
+ expect(result2).toMatch(/1899-12-31|1900-01-01/);
330
+ });
331
+
332
+ it('handles various timezone formats', () => {
333
+ const date = new Date('2024-01-15T10:00:00Z');
334
+ const timezones = [
335
+ 'America/New_York',
336
+ 'Europe/London',
337
+ 'Asia/Tokyo',
338
+ 'Australia/Sydney',
339
+ 'UTC'
340
+ ];
341
+
342
+ timezones.forEach(tz => {
343
+ const result = formatInTimeZone(date, tz, 'yyyy-MM-dd');
344
+ expect(result).toBe('2024-01-15');
345
+ });
346
+ });
347
+ });
348
+ });
349
+
@@ -0,0 +1,281 @@
1
+ /**
2
+ * @file Timezone Utilities
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Timezone
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive timezone conversion and formatting utilities using date-fns-tz and native Intl APIs.
8
+ * Provides functions for timezone-aware date operations, conversions, and formatting.
9
+ *
10
+ * Features:
11
+ * - Format dates in specific timezones
12
+ * - Get timezone abbreviations
13
+ * - Convert between UTC and timezone local times
14
+ * - Round dates to nearest minutes
15
+ * - Calculate timezone differences
16
+ * - Get user's browser timezone
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { toZonedTime, fromZonedTime, formatInTimeZone, getUserTimeZone } from '@jmruthers/pace-core/utils/timezone';
21
+ *
22
+ * // Convert UTC to local timezone
23
+ * const utcDate = new Date('2024-01-15T10:00:00Z');
24
+ * const localDate = toZonedTime(utcDate, 'America/New_York');
25
+ *
26
+ * // Convert local time to UTC
27
+ * const localInput = new Date(2024, 0, 15, 10, 0);
28
+ * const utcDate = fromZonedTime(localInput, 'America/New_York');
29
+ *
30
+ * // Format with timezone
31
+ * const formatted = formatInTimeZone(utcDate, 'America/New_York', 'MMM dd, yyyy HH:mm');
32
+ * ```
33
+ */
34
+
35
+ import { format, parseISO, addMinutes, differenceInHours, isValid } from 'date-fns';
36
+ import { formatInTimeZone as fnsFormatInTimeZone, toZonedTime as fnsToZonedTime, fromZonedTime as fnsFromZonedTime } from 'date-fns-tz';
37
+
38
+ /**
39
+ * Format a date in a specific timezone using date-fns format strings
40
+ *
41
+ * @param date - Date to format (Date object, ISO string, or timestamp)
42
+ * @param timeZone - IANA timezone string (e.g., 'America/New_York')
43
+ * @param formatStr - date-fns format string (e.g., 'MMM dd, yyyy HH:mm')
44
+ * @returns Formatted date string or 'Invalid date' on error
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * formatInTimeZone(new Date('2024-01-15T10:00:00Z'), 'America/New_York', 'MMM dd, yyyy HH:mm');
49
+ * // "Jan 15, 2024 05:00"
50
+ * ```
51
+ */
52
+ export function formatInTimeZone(
53
+ date: Date | string | number,
54
+ timeZone: string,
55
+ formatStr: string
56
+ ): string {
57
+ try {
58
+ if (!timeZone || typeof timeZone !== 'string') {
59
+ return 'Invalid date';
60
+ }
61
+
62
+ let dateObj: Date;
63
+ if (typeof date === 'string') {
64
+ dateObj = parseISO(date);
65
+ } else if (typeof date === 'number') {
66
+ dateObj = new Date(date);
67
+ } else {
68
+ dateObj = date;
69
+ }
70
+
71
+ if (!isValid(dateObj)) {
72
+ return 'Invalid date';
73
+ }
74
+
75
+ return fnsFormatInTimeZone(dateObj, timeZone, formatStr);
76
+ } catch {
77
+ return 'Invalid date';
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get the timezone abbreviation (EST, PST, etc.) for a date in a specific timezone
83
+ *
84
+ * @param date - Date to get abbreviation for
85
+ * @param timeZone - IANA timezone string
86
+ * @returns Timezone abbreviation or timezone name on error
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * getTimezoneAbbreviation(new Date(), 'America/New_York');
91
+ * // "EST" or "EDT" depending on date
92
+ * ```
93
+ */
94
+ export function getTimezoneAbbreviation(date: Date, timeZone: string): string {
95
+ try {
96
+ if (!timeZone || typeof timeZone !== 'string') {
97
+ return timeZone || 'UTC';
98
+ }
99
+
100
+ if (!isValid(date)) {
101
+ return timeZone;
102
+ }
103
+
104
+ const formatter = new Intl.DateTimeFormat('en-US', {
105
+ timeZone,
106
+ timeZoneName: 'short'
107
+ });
108
+
109
+ const parts = formatter.formatToParts(date);
110
+ const timeZoneName = parts.find(part => part.type === 'timeZoneName');
111
+
112
+ return timeZoneName?.value || timeZone;
113
+ } catch {
114
+ return timeZone || 'UTC';
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Format time only in a specific timezone
120
+ *
121
+ * @param date - Date to format
122
+ * @param timeZone - IANA timezone string
123
+ * @returns Formatted time string (HH:mm format) or 'Invalid date' on error
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * formatTimeInTimeZone(new Date('2024-01-15T10:00:00Z'), 'America/New_York');
128
+ * // "05:00"
129
+ * ```
130
+ */
131
+ export function formatTimeInTimeZone(date: Date | string, timeZone: string): string {
132
+ return formatInTimeZone(date, timeZone, 'HH:mm');
133
+ }
134
+
135
+ /**
136
+ * Get the user's local timezone from the browser
137
+ *
138
+ * @returns IANA timezone string (e.g., 'America/New_York')
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const userTz = getUserTimeZone();
143
+ * // "America/New_York" or user's actual timezone
144
+ * ```
145
+ */
146
+ export function getUserTimeZone(): string {
147
+ try {
148
+ if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
149
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
150
+ }
151
+ return 'UTC';
152
+ } catch {
153
+ return 'UTC';
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Convert a UTC date to a specific timezone's local time representation
159
+ *
160
+ * @param date - UTC date to convert
161
+ * @param timezone - IANA timezone string
162
+ * @returns Date object representing local time in the timezone
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const utcDate = new Date('2024-01-15T10:00:00Z');
167
+ * const localDate = toZonedTime(utcDate, 'America/New_York');
168
+ * // Returns Date object representing 5:00 AM in New York
169
+ * ```
170
+ */
171
+ export function toZonedTime(date: Date, timezone: string): Date {
172
+ try {
173
+ if (!timezone || typeof timezone !== 'string') {
174
+ return date;
175
+ }
176
+
177
+ if (!isValid(date)) {
178
+ return date;
179
+ }
180
+
181
+ return fnsToZonedTime(date, timezone);
182
+ } catch {
183
+ return date;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Convert a local time in a specific timezone to UTC
189
+ *
190
+ * @param localDate - Local date in the timezone
191
+ * @param timezone - IANA timezone string
192
+ * @returns Date object in UTC
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * const localDate = new Date(2024, 0, 15, 10, 0); // Jan 15, 2024 10:00 AM local
197
+ * const utcDate = fromZonedTime(localDate, 'America/New_York');
198
+ * // Returns Date object representing 3:00 PM UTC (if EST) or 2:00 PM UTC (if EDT)
199
+ * ```
200
+ */
201
+ export function fromZonedTime(localDate: Date, timezone: string): Date {
202
+ try {
203
+ if (!timezone || typeof timezone !== 'string') {
204
+ return localDate;
205
+ }
206
+
207
+ if (!isValid(localDate)) {
208
+ return localDate;
209
+ }
210
+
211
+ return fnsFromZonedTime(localDate, timezone);
212
+ } catch {
213
+ return localDate;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Round a date to the nearest X minutes
219
+ *
220
+ * @param date - Date to round
221
+ * @param minutesStep - Number of minutes to round to (default: 5)
222
+ * @returns Rounded date
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * const date = new Date('2024-01-15T10:23:00Z');
227
+ * roundToNearestMinutes(date, 5);
228
+ * // Returns Date object for 10:25:00
229
+ * ```
230
+ */
231
+ export function roundToNearestMinutes(date: Date, minutesStep: number = 5): Date {
232
+ try {
233
+ if (!isValid(date)) {
234
+ return date;
235
+ }
236
+
237
+ if (minutesStep <= 0 || !Number.isInteger(minutesStep)) {
238
+ return date;
239
+ }
240
+
241
+ const minutes = date.getMinutes();
242
+ const roundedMinutes = Math.round(minutes / minutesStep) * minutesStep;
243
+ const diff = roundedMinutes - minutes;
244
+
245
+ return addMinutes(date, diff);
246
+ } catch {
247
+ return date;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Calculate the time difference between two timezones in hours
253
+ *
254
+ * @param fromTimeZone - Source timezone (IANA string)
255
+ * @param toTimeZone - Target timezone (IANA string)
256
+ * @returns Difference in hours (positive if toTimeZone is ahead)
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
261
+ * // -3 (Los Angeles is 3 hours behind New York)
262
+ * ```
263
+ */
264
+ export function getTimeZoneDifference(fromTimeZone: string, toTimeZone: string): number {
265
+ try {
266
+ if (!fromTimeZone || !toTimeZone || typeof fromTimeZone !== 'string' || typeof toTimeZone !== 'string') {
267
+ return 0;
268
+ }
269
+
270
+ const now = new Date();
271
+ const fromDate = toZonedTime(now, fromTimeZone);
272
+ const toDate = toZonedTime(now, toTimeZone);
273
+
274
+ const diff = differenceInHours(toDate, fromDate);
275
+ // Handle NaN case (differenceInHours can return NaN for invalid dates)
276
+ return isNaN(diff) ? 0 : diff;
277
+ } catch {
278
+ return 0;
279
+ }
280
+ }
281
+