@jmruthers/pace-core 0.5.184 → 0.5.186

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 (319) 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-DIzEzwKl.d.ts} +23 -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-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
  13. package/dist/chunk-DAGICKHT.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-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
  17. package/dist/chunk-GRIQLQ52.js.map +1 -0
  18. package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
  19. package/dist/chunk-HDCUMOOI.js.map +1 -0
  20. package/dist/chunk-HESYZWZW.js +388 -0
  21. package/dist/chunk-HESYZWZW.js.map +1 -0
  22. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  23. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  24. package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
  25. package/dist/chunk-OALXJH4Y.js.map +1 -0
  26. package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
  27. package/dist/chunk-TC7D3CR3.js.map +1 -0
  28. package/dist/chunk-THRPYOFK.js +215 -0
  29. package/dist/chunk-THRPYOFK.js.map +1 -0
  30. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  31. package/dist/chunk-U6WNSFX5.js.map +1 -0
  32. package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
  33. package/dist/chunk-UQWSHFVX.js.map +1 -0
  34. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  35. package/dist/components.d.ts +182 -6
  36. package/dist/components.js +157 -11
  37. package/dist/components.js.map +1 -1
  38. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  39. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  40. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
  41. package/dist/hooks.d.ts +52 -15
  42. package/dist/hooks.js +12 -22
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +12 -12
  45. package/dist/index.js +82 -18
  46. package/dist/index.js.map +1 -1
  47. package/dist/providers.d.ts +1 -1
  48. package/dist/providers.js +3 -1
  49. package/dist/rbac/index.d.ts +206 -15
  50. package/dist/rbac/index.js +28 -6
  51. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  52. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  53. package/dist/types.d.ts +2 -2
  54. package/dist/types.js +1 -1
  55. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
  56. package/dist/utils.d.ts +110 -152
  57. package/dist/utils.js +128 -138
  58. package/dist/utils.js.map +1 -1
  59. package/docs/api/README.md +60 -1
  60. package/docs/api/classes/ColumnFactory.md +1 -1
  61. package/docs/api/classes/ErrorBoundary.md +1 -1
  62. package/docs/api/classes/InvalidScopeError.md +1 -1
  63. package/docs/api/classes/Logger.md +178 -0
  64. package/docs/api/classes/MissingUserContextError.md +1 -1
  65. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  66. package/docs/api/classes/PermissionDeniedError.md +1 -1
  67. package/docs/api/classes/RBACAuditManager.md +2 -2
  68. package/docs/api/classes/RBACCache.md +1 -1
  69. package/docs/api/classes/RBACEngine.md +2 -2
  70. package/docs/api/classes/RBACError.md +1 -1
  71. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  72. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  73. package/docs/api/classes/StorageUtils.md +1 -1
  74. package/docs/api/enums/FileCategory.md +1 -1
  75. package/docs/api/enums/LogLevel.md +54 -0
  76. package/docs/api/enums/RBACErrorCode.md +1 -1
  77. package/docs/api/enums/RPCFunction.md +1 -1
  78. package/docs/api/interfaces/AggregateConfig.md +1 -1
  79. package/docs/api/interfaces/BadgeProps.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +1 -1
  81. package/docs/api/interfaces/CalendarProps.md +18 -2
  82. package/docs/api/interfaces/CardProps.md +1 -1
  83. package/docs/api/interfaces/ColorPalette.md +1 -1
  84. package/docs/api/interfaces/ColorShade.md +1 -1
  85. package/docs/api/interfaces/ComplianceResult.md +30 -0
  86. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  87. package/docs/api/interfaces/DataRecord.md +1 -1
  88. package/docs/api/interfaces/DataTableAction.md +1 -1
  89. package/docs/api/interfaces/DataTableColumn.md +1 -1
  90. package/docs/api/interfaces/DataTableProps.md +1 -1
  91. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  92. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  93. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  94. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  95. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  97. package/docs/api/interfaces/ExportColumn.md +1 -1
  98. package/docs/api/interfaces/ExportOptions.md +1 -1
  99. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  100. package/docs/api/interfaces/FileMetadata.md +1 -1
  101. package/docs/api/interfaces/FileReference.md +1 -1
  102. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  103. package/docs/api/interfaces/FileUploadOptions.md +48 -8
  104. package/docs/api/interfaces/FileUploadProps.md +46 -13
  105. package/docs/api/interfaces/FooterProps.md +1 -1
  106. package/docs/api/interfaces/FormFieldProps.md +1 -1
  107. package/docs/api/interfaces/FormProps.md +1 -1
  108. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  109. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  110. package/docs/api/interfaces/InputProps.md +1 -1
  111. package/docs/api/interfaces/LabelProps.md +1 -1
  112. package/docs/api/interfaces/LoggerConfig.md +62 -0
  113. package/docs/api/interfaces/LoginFormProps.md +1 -1
  114. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  115. package/docs/api/interfaces/NavigationContextType.md +1 -1
  116. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  117. package/docs/api/interfaces/NavigationItem.md +1 -1
  118. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  119. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  120. package/docs/api/interfaces/Organisation.md +1 -1
  121. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  122. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  123. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  124. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  125. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  126. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  127. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  128. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  129. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  130. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  131. package/docs/api/interfaces/PaletteData.md +1 -1
  132. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  133. package/docs/api/interfaces/ProgressProps.md +1 -1
  134. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  135. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  136. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  138. package/docs/api/interfaces/QuickFix.md +52 -0
  139. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  140. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  141. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  142. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  143. package/docs/api/interfaces/RBACConfig.md +4 -4
  144. package/docs/api/interfaces/RBACContext.md +1 -1
  145. package/docs/api/interfaces/RBACLogger.md +1 -1
  146. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  149. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  151. package/docs/api/interfaces/RBACResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  156. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  157. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  158. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  159. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  160. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  161. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  162. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  163. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  164. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  165. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  166. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  167. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  168. package/docs/api/interfaces/RouteConfig.md +1 -1
  169. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  170. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  171. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  172. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  173. package/docs/api/interfaces/SetupIssue.md +41 -0
  174. package/docs/api/interfaces/StorageConfig.md +1 -1
  175. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  176. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  177. package/docs/api/interfaces/StorageListOptions.md +1 -1
  178. package/docs/api/interfaces/StorageListResult.md +1 -1
  179. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  180. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  181. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  182. package/docs/api/interfaces/StyleImport.md +1 -1
  183. package/docs/api/interfaces/SwitchProps.md +1 -1
  184. package/docs/api/interfaces/TabsContentProps.md +1 -1
  185. package/docs/api/interfaces/TabsListProps.md +1 -1
  186. package/docs/api/interfaces/TabsProps.md +1 -1
  187. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  188. package/docs/api/interfaces/TextareaProps.md +1 -1
  189. package/docs/api/interfaces/ToastActionElement.md +1 -1
  190. package/docs/api/interfaces/ToastProps.md +1 -1
  191. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  192. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  193. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  194. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  195. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  196. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  198. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  202. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  203. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  204. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  205. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  206. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  207. package/docs/api/interfaces/UserEventAccess.md +1 -1
  208. package/docs/api/interfaces/UserMenuProps.md +1 -1
  209. package/docs/api/interfaces/UserProfile.md +1 -1
  210. package/docs/api/modules.md +746 -50
  211. package/docs/api-reference/components.md +26 -12
  212. package/docs/api-reference/hooks.md +111 -0
  213. package/docs/api-reference/rpc-functions.md +1 -1
  214. package/docs/api-reference/utilities.md +184 -0
  215. package/docs/getting-started/installation-guide.md +75 -16
  216. package/docs/getting-started/quick-start.md +61 -11
  217. package/docs/implementation-guides/authentication.md +88 -12
  218. package/docs/implementation-guides/file-reference-system.md +26 -3
  219. package/docs/implementation-guides/file-upload-storage.md +30 -1
  220. package/docs/rbac/README.md +1 -0
  221. package/docs/rbac/compliance/compliance-guide.md +544 -0
  222. package/docs/rbac/getting-started.md +158 -33
  223. package/docs/standards/pace-core-compliance.md +432 -0
  224. package/eslint-config-pace-core.cjs +93 -0
  225. package/package.json +15 -3
  226. package/scripts/analyze-bundle.js +232 -0
  227. package/scripts/build-css.js +56 -0
  228. package/scripts/build-docs-incremental.js +1015 -0
  229. package/scripts/check-pace-core-compliance.cjs +2353 -0
  230. package/scripts/check-pace-core-compliance.js +512 -0
  231. package/scripts/generate-docs.js +157 -0
  232. package/scripts/setup-build-cache.js +73 -0
  233. package/scripts/utils/command-runner.js +131 -0
  234. package/scripts/utils/env.js +33 -0
  235. package/scripts/utils/index.js +10 -0
  236. package/scripts/utils/logger.js +88 -0
  237. package/scripts/utils/path-helpers.js +37 -0
  238. package/scripts/validate-formats.js +133 -0
  239. package/scripts/validate-master.js +155 -0
  240. package/scripts/validate-pre-publish.js +140 -0
  241. package/scripts/validate-theme.js +142 -0
  242. package/src/components/Calendar/Calendar.tsx +8 -1
  243. package/src/components/Card/Card.tsx +47 -8
  244. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  245. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  246. package/src/components/DatePickerWithTimezone/README.md +135 -0
  247. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  248. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  249. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  250. package/src/components/DateTimeField/README.md +148 -0
  251. package/src/components/DateTimeField/index.ts +10 -0
  252. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  253. package/src/components/FileUpload/FileUpload.tsx +10 -1
  254. package/src/components/Header/Header.test.tsx +47 -18
  255. package/src/components/Header/Header.tsx +22 -7
  256. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  257. package/src/components/PaceAppLayout/README.md +9 -0
  258. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  259. package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
  260. package/src/components/index.ts +8 -0
  261. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  262. package/src/eslint-rules/pace-core-compliance.js +640 -0
  263. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  264. package/src/hooks/index.ts +5 -0
  265. package/src/hooks/useFileReference.test.ts +2 -0
  266. package/src/hooks/useFormDialog.ts +147 -0
  267. package/src/hooks/usePreventTabReload.ts +106 -0
  268. package/src/hooks/useSecureDataAccess.ts +2 -2
  269. package/src/index.ts +27 -0
  270. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  271. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  272. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  273. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  274. package/src/rbac/compliance/database-validator.ts +165 -0
  275. package/src/rbac/compliance/index.ts +38 -0
  276. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  277. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  278. package/src/rbac/compliance/setup-validator.ts +131 -0
  279. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  280. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  281. package/src/rbac/docs/event-based-apps.md +285 -0
  282. package/src/rbac/errors.ts +11 -0
  283. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  284. package/src/rbac/index.ts +30 -0
  285. package/src/services/OrganisationService.ts +4 -0
  286. package/src/styles/core.css +5 -5
  287. package/src/types/database.generated.ts +63 -9
  288. package/src/types/file-reference.ts +9 -0
  289. package/src/utils/__tests__/timezone.test.ts +345 -0
  290. package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
  291. package/src/utils/file-reference/index.ts +13 -2
  292. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  293. package/src/utils/formatting/formatting.ts +179 -0
  294. package/src/utils/index.ts +27 -1
  295. package/src/utils/location/index.ts +16 -0
  296. package/src/utils/location/location.test.ts +286 -0
  297. package/src/utils/location/location.ts +175 -0
  298. package/src/utils/security/secureDataAccess.ts +1 -1
  299. package/src/utils/storage/helpers.ts +68 -0
  300. package/src/utils/timezone/index.ts +17 -0
  301. package/src/utils/timezone/timezone.test.ts +349 -0
  302. package/src/utils/timezone/timezone.ts +281 -0
  303. package/dist/chunk-CSOFYHAG.js.map +0 -1
  304. package/dist/chunk-FUEYYMX5.js.map +0 -1
  305. package/dist/chunk-HKIT6O7W.js +0 -198
  306. package/dist/chunk-HKIT6O7W.js.map +0 -1
  307. package/dist/chunk-KUEN3HFB.js +0 -94
  308. package/dist/chunk-KUEN3HFB.js.map +0 -1
  309. package/dist/chunk-M7W4CP3M.js.map +0 -1
  310. package/dist/chunk-MI7HBHN3.js.map +0 -1
  311. package/dist/chunk-NQPMQGS2.js.map +0 -1
  312. package/dist/chunk-PWAHJW4G.js.map +0 -1
  313. package/dist/chunk-UHNYIBXL.js.map +0 -1
  314. package/dist/chunk-W22JP75J.js.map +0 -1
  315. package/dist/formatting-5wETwiGF.d.ts +0 -162
  316. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  317. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  318. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  319. /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @file DatePickerWithTimezone Component
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DatePickerWithTimezone
5
+ * @since 0.1.0
6
+ *
7
+ * Date picker component that displays timezone information alongside the calendar.
8
+ * Provides a calendar interface with timezone context for date selection.
9
+ *
10
+ * Features:
11
+ * - Calendar date selection
12
+ * - Timezone display (shows "Local" when matches user timezone)
13
+ * - Optional "Done" button
14
+ * - Accessible date selection
15
+ * - Keyboard navigation support
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { DatePickerWithTimezone } from '@jmruthers/pace-core/components';
20
+ * import { useState } from 'react';
21
+ *
22
+ * function DateSelector() {
23
+ * const [date, setDate] = useState<Date>();
24
+ *
25
+ * return (
26
+ * <DatePickerWithTimezone
27
+ * selected={date}
28
+ * onSelect={setDate}
29
+ * timezone="America/New_York"
30
+ * onDone={() => console.log('Date selected')}
31
+ * />
32
+ * );
33
+ * }
34
+ * ```
35
+ *
36
+ * @accessibility
37
+ * - WCAG 2.1 AA compliant
38
+ * - Keyboard navigation support
39
+ * - Screen reader friendly
40
+ * - Focus management
41
+ * - Proper ARIA attributes
42
+ */
43
+
44
+ import * as React from 'react';
45
+ import { Calendar } from '../Calendar';
46
+ import { Button } from '../Button';
47
+ import { Clock } from 'lucide-react';
48
+ import { getUserTimeZone } from '../../utils/timezone';
49
+ import { cn } from '../../utils/core/cn';
50
+
51
+ /**
52
+ * Props for the DatePickerWithTimezone component
53
+ */
54
+ export interface DatePickerWithTimezoneProps {
55
+ /**
56
+ * Currently selected date
57
+ */
58
+ selected?: Date;
59
+ /**
60
+ * Date selection handler
61
+ */
62
+ onSelect: (date: Date | undefined) => void;
63
+ /**
64
+ * Optional callback when "Done" button is clicked
65
+ */
66
+ onDone?: () => void;
67
+ /**
68
+ * Timezone to display (defaults to user's timezone)
69
+ */
70
+ timezone?: string;
71
+ /**
72
+ * Additional CSS classes
73
+ */
74
+ className?: string;
75
+ }
76
+
77
+ /**
78
+ * DatePickerWithTimezone component
79
+ * Date picker with timezone information display
80
+ *
81
+ * @param props - DatePickerWithTimezone configuration
82
+ * @returns JSX.Element - The rendered date picker with timezone
83
+ */
84
+ export function DatePickerWithTimezone({
85
+ selected,
86
+ onSelect,
87
+ onDone,
88
+ timezone,
89
+ className
90
+ }: DatePickerWithTimezoneProps) {
91
+ const userTimezone = getUserTimeZone();
92
+ const displayTimezone = timezone || userTimezone;
93
+ const timezoneDisplay = displayTimezone === userTimezone ? 'Local' : displayTimezone;
94
+
95
+ return (
96
+ <div className={cn('flex flex-col', className)}>
97
+ <div className="p-3">
98
+ <Calendar
99
+ mode="single"
100
+ selected={selected}
101
+ onSelect={onSelect}
102
+ initialFocus
103
+ captionLayout="dropdown-buttons"
104
+ fromYear={1900}
105
+ toYear={2100}
106
+ className="p-0"
107
+ />
108
+ </div>
109
+
110
+ <div className="flex items-center justify-between border-t border-border px-3 py-2">
111
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
112
+ <Clock className="h-4 w-4" aria-hidden="true" />
113
+ <span>
114
+ Timezone: <span aria-label={`Timezone ${timezoneDisplay}`}>{timezoneDisplay}</span>
115
+ </span>
116
+ </div>
117
+ {onDone && (
118
+ <Button onClick={onDone} size="sm" className="h-8">
119
+ Done
120
+ </Button>
121
+ )}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
@@ -0,0 +1,135 @@
1
+ # DatePickerWithTimezone Component
2
+
3
+ Date picker component that displays timezone information alongside the calendar.
4
+
5
+ ## Overview
6
+
7
+ The `DatePickerWithTimezone` component provides a calendar interface for date selection with timezone context. It displays the timezone information below the calendar and optionally includes a "Done" button.
8
+
9
+ ## Features
10
+
11
+ - Calendar date selection
12
+ - Timezone display (shows "Local" when matches user timezone)
13
+ - Optional "Done" button
14
+ - Accessible date selection
15
+ - Keyboard navigation support
16
+ - Integration with pace-core Calendar component
17
+
18
+ ## Usage
19
+
20
+ ### Basic Example
21
+
22
+ ```tsx
23
+ import { DatePickerWithTimezone } from '@jmruthers/pace-core/components';
24
+ import { useState } from 'react';
25
+
26
+ function DateSelector() {
27
+ const [date, setDate] = useState<Date>();
28
+
29
+ return (
30
+ <DatePickerWithTimezone
31
+ selected={date}
32
+ onSelect={setDate}
33
+ timezone="America/New_York"
34
+ />
35
+ );
36
+ }
37
+ ```
38
+
39
+ ### With Done Button
40
+
41
+ ```tsx
42
+ <DatePickerWithTimezone
43
+ selected={date}
44
+ onSelect={setDate}
45
+ timezone="America/New_York"
46
+ onDone={() => {
47
+ console.log('Date selected:', date);
48
+ // Close dialog or perform other actions
49
+ }}
50
+ />
51
+ ```
52
+
53
+ ### Using User's Timezone
54
+
55
+ ```tsx
56
+ // When timezone is not provided, uses user's browser timezone
57
+ <DatePickerWithTimezone
58
+ selected={date}
59
+ onSelect={setDate}
60
+ />
61
+ ```
62
+
63
+ ## API
64
+
65
+ ### Props
66
+
67
+ | Prop | Type | Default | Description |
68
+ |------|------|---------|-------------|
69
+ | `selected` | `Date \| undefined` | - | Currently selected date |
70
+ | `onSelect` | `(date: Date \| undefined) => void` | **required** | Date selection handler |
71
+ | `onDone` | `() => void` | - | Optional callback when "Done" button is clicked |
72
+ | `timezone` | `string` | User's timezone | Timezone to display (IANA timezone string) |
73
+ | `className` | `string` | - | Additional CSS classes |
74
+
75
+ ## Behavior
76
+
77
+ ### Timezone Display
78
+
79
+ - When `timezone` is not provided: Uses the user's browser timezone
80
+ - When `timezone` matches user's timezone: Shows "Local"
81
+ - When `timezone` is different: Shows the timezone name (e.g., "America/New_York")
82
+
83
+ ### Date Selection
84
+
85
+ The component uses the pace-core `Calendar` component for date selection. Selected dates are passed directly to `onSelect` without timezone conversion (dates are timezone-agnostic).
86
+
87
+ ### Done Button
88
+
89
+ When `onDone` is provided, a "Done" button appears in the footer. Clicking it calls the `onDone` callback without affecting the selected date.
90
+
91
+ ## Accessibility
92
+
93
+ - WCAG 2.1 AA compliant
94
+ - Keyboard navigation support (via Calendar component)
95
+ - Screen reader friendly
96
+ - Proper ARIA labels for timezone information
97
+ - Focus management
98
+
99
+ ## Integration
100
+
101
+ ### With Dialog
102
+
103
+ ```tsx
104
+ import { Dialog, DatePickerWithTimezone } from '@jmruthers/pace-core/components';
105
+
106
+ function DatePickerDialog() {
107
+ const [date, setDate] = useState<Date>();
108
+ const [open, setOpen] = useState(false);
109
+
110
+ return (
111
+ <Dialog open={open} onOpenChange={setOpen}>
112
+ <DialogContent>
113
+ <DatePickerWithTimezone
114
+ selected={date}
115
+ onSelect={setDate}
116
+ onDone={() => setOpen(false)}
117
+ />
118
+ </DialogContent>
119
+ </Dialog>
120
+ );
121
+ }
122
+ ```
123
+
124
+ ## Edge Cases
125
+
126
+ - **Undefined selected date**: Calendar displays with no date selected
127
+ - **Null selected date**: Treated as undefined
128
+
129
+ ## Dependencies
130
+
131
+ - `@jmruthers/pace-core/components/Calendar` - Calendar component
132
+ - `@jmruthers/pace-core/components/Button` - Button component
133
+ - `lucide-react` - Clock icon
134
+ - `@jmruthers/pace-core/utils/timezone` - Timezone utilities
135
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file DatePickerWithTimezone Component Exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DatePickerWithTimezone
5
+ * @since 0.1.0
6
+ */
7
+
8
+ export { DatePickerWithTimezone } from './DatePickerWithTimezone';
9
+ export type { DatePickerWithTimezoneProps } from './DatePickerWithTimezone';
10
+
@@ -0,0 +1,358 @@
1
+ /**
2
+ * @file DateTimeField Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DateTimeField/__tests__
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive test suite for DateTimeField component.
8
+ * Tests cover all major functionality, edge cases, and accessibility.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
15
+ import { DateTimeField } from './DateTimeField';
16
+
17
+ // Mock timezone utilities
18
+ vi.mock('../../utils/timezone', () => ({
19
+ toZonedTime: vi.fn((date: Date, tz: string) => {
20
+ // Simple mock: return date as-is for testing
21
+ return date;
22
+ }),
23
+ fromZonedTime: vi.fn((date: Date, tz: string) => {
24
+ // Simple mock: return date as-is for testing
25
+ return date;
26
+ }),
27
+ getUserTimeZone: vi.fn(() => 'America/New_York')
28
+ }));
29
+
30
+ describe('DateTimeField Component', () => {
31
+ beforeEach(() => {
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ describe('Rendering', () => {
36
+ it('renders with default props', () => {
37
+ const onChange = vi.fn();
38
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
39
+
40
+ expect(screen.getByText('Start Time')).toBeInTheDocument();
41
+ expect(screen.getByLabelText(/Start Time/)).toBeInTheDocument();
42
+ });
43
+
44
+ it('renders with required indicator', () => {
45
+ const onChange = vi.fn();
46
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} required />);
47
+
48
+ const label = screen.getByText('Start Time');
49
+ expect(label).toBeInTheDocument();
50
+ // Required indicator should be present
51
+ expect(screen.getByLabelText(/required/i)).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders with helper text', () => {
55
+ const onChange = vi.fn();
56
+ render(
57
+ <DateTimeField
58
+ label="Start Time"
59
+ value={undefined}
60
+ onChange={onChange}
61
+ helperText="Select a start time"
62
+ />
63
+ );
64
+
65
+ expect(screen.getByText('Select a start time')).toBeInTheDocument();
66
+ });
67
+
68
+ it('renders with error message', () => {
69
+ const onChange = vi.fn();
70
+ render(
71
+ <DateTimeField
72
+ label="Start Time"
73
+ value={undefined}
74
+ onChange={onChange}
75
+ error="Invalid date"
76
+ />
77
+ );
78
+
79
+ expect(screen.getByText('Invalid date')).toBeInTheDocument();
80
+ expect(screen.getByRole('alert')).toBeInTheDocument();
81
+ });
82
+
83
+ it('renders with custom id', () => {
84
+ const onChange = vi.fn();
85
+ render(
86
+ <DateTimeField
87
+ id="custom-id"
88
+ label="Start Time"
89
+ value={undefined}
90
+ onChange={onChange}
91
+ />
92
+ );
93
+
94
+ expect(screen.getByLabelText(/Start Time/)).toHaveAttribute('id', 'custom-id');
95
+ });
96
+ });
97
+
98
+ describe('Value Display and Conversion', () => {
99
+ it('displays ISO string value correctly', () => {
100
+ const onChange = vi.fn();
101
+ const value = '2024-01-15T10:00:00Z';
102
+ render(<DateTimeField label="Start Time" value={value} onChange={onChange} />);
103
+
104
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
105
+ expect(input.value).toBeTruthy();
106
+ });
107
+
108
+ it('displays Date object value correctly', () => {
109
+ const onChange = vi.fn();
110
+ const value = new Date('2024-01-15T10:00:00Z');
111
+ render(<DateTimeField label="Start Time" value={value} onChange={onChange} />);
112
+
113
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
114
+ expect(input.value).toBeTruthy();
115
+ });
116
+
117
+ it('handles undefined value', () => {
118
+ const onChange = vi.fn();
119
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
120
+
121
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
122
+ expect(input.value).toBe('');
123
+ });
124
+
125
+ it('displays timezone information when not UTC', () => {
126
+ const onChange = vi.fn();
127
+ render(
128
+ <DateTimeField
129
+ label="Start Time"
130
+ value={undefined}
131
+ onChange={onChange}
132
+ timezone="Europe/London"
133
+ />
134
+ );
135
+
136
+ expect(screen.getByText('Europe/London')).toBeInTheDocument();
137
+ });
138
+
139
+ it('displays "Local" when timezone matches user timezone', () => {
140
+ const onChange = vi.fn();
141
+ render(
142
+ <DateTimeField
143
+ label="Start Time"
144
+ value={undefined}
145
+ onChange={onChange}
146
+ timezone="America/New_York"
147
+ />
148
+ );
149
+
150
+ expect(screen.getByText('Local')).toBeInTheDocument();
151
+ });
152
+
153
+ it('does not display timezone for UTC', () => {
154
+ const onChange = vi.fn();
155
+ const { container } = render(
156
+ <DateTimeField
157
+ label="Start Time"
158
+ value={undefined}
159
+ onChange={onChange}
160
+ timezone="UTC"
161
+ />
162
+ );
163
+
164
+ const timezoneDisplay = container.querySelector('.text-muted-foreground');
165
+ expect(timezoneDisplay).not.toBeInTheDocument();
166
+ });
167
+ });
168
+
169
+ describe('onChange Behavior', () => {
170
+ it('calls onChange with ISO string when returnAsDate is false', async () => {
171
+ const onChange = vi.fn();
172
+ const user = userEvent.setup();
173
+
174
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
175
+
176
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
177
+ await user.type(input, '2024-01-15T10:00');
178
+
179
+ await waitFor(() => {
180
+ expect(onChange).toHaveBeenCalled();
181
+ });
182
+ });
183
+
184
+ it('calls onChange with Date object when returnAsDate is true', async () => {
185
+ const onChange = vi.fn();
186
+ const user = userEvent.setup();
187
+
188
+ render(
189
+ <DateTimeField
190
+ label="Start Time"
191
+ value={undefined}
192
+ onChange={onChange}
193
+ returnAsDate
194
+ />
195
+ );
196
+
197
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
198
+ await user.type(input, '2024-01-15T10:00');
199
+
200
+ await waitFor(() => {
201
+ expect(onChange).toHaveBeenCalled();
202
+ const callArg = onChange.mock.calls[onChange.mock.calls.length - 1][0];
203
+ expect(callArg).toBeInstanceOf(Date);
204
+ });
205
+ });
206
+
207
+ it('calls onChange with undefined when input is cleared', async () => {
208
+ const onChange = vi.fn();
209
+ const user = userEvent.setup();
210
+
211
+ // Set an initial value first
212
+ const { rerender } = render(
213
+ <DateTimeField label="Start Time" value="2024-01-15T10:00:00Z" onChange={onChange} />
214
+ );
215
+
216
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
217
+ await user.clear(input);
218
+
219
+ // Trigger change event manually to simulate clearing
220
+ fireEvent.change(input, { target: { value: '' } });
221
+
222
+ await waitFor(() => {
223
+ expect(onChange).toHaveBeenCalledWith(undefined);
224
+ });
225
+ });
226
+ });
227
+
228
+ describe('User Editing', () => {
229
+ it('prevents unwanted conversions during editing', async () => {
230
+ const onChange = vi.fn();
231
+ const user = userEvent.setup();
232
+ const value = '2024-01-15T10:00:00Z';
233
+
234
+ render(<DateTimeField label="Start Time" value={value} onChange={onChange} />);
235
+
236
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
237
+ await user.click(input);
238
+ await user.type(input, '2024-01-16');
239
+
240
+ // Should not trigger multiple onChange calls during typing
241
+ await waitFor(() => {
242
+ expect(onChange).toHaveBeenCalled();
243
+ });
244
+ });
245
+
246
+ it('handles blur event correctly', async () => {
247
+ const onChange = vi.fn();
248
+ const user = userEvent.setup();
249
+
250
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
251
+
252
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
253
+ await user.click(input);
254
+ await user.tab();
255
+
256
+ // Blur should stop editing mode
257
+ expect(input).not.toHaveFocus();
258
+ });
259
+ });
260
+
261
+ describe('Edge Cases', () => {
262
+ it('handles invalid date string gracefully', () => {
263
+ const onChange = vi.fn();
264
+ render(<DateTimeField label="Start Time" value="invalid-date" onChange={onChange} />);
265
+
266
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
267
+ expect(input.value).toBe('');
268
+ });
269
+
270
+ it('handles null value gracefully', () => {
271
+ const onChange = vi.fn();
272
+ // @ts-expect-error - Testing edge case
273
+ render(<DateTimeField label="Start Time" value={null} onChange={onChange} />);
274
+
275
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
276
+ expect(input.value).toBe('');
277
+ });
278
+
279
+ it('handles empty string value', () => {
280
+ const onChange = vi.fn();
281
+ render(<DateTimeField label="Start Time" value="" onChange={onChange} />);
282
+
283
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
284
+ expect(input.value).toBe('');
285
+ });
286
+ });
287
+
288
+ describe('Accessibility', () => {
289
+ it('associates label with input via htmlFor', () => {
290
+ const onChange = vi.fn();
291
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
292
+
293
+ const label = screen.getByText('Start Time');
294
+ const input = screen.getByLabelText(/Start Time/);
295
+
296
+ expect(label).toBeInTheDocument();
297
+ expect(input).toBeInTheDocument();
298
+ expect(input).toHaveAttribute('id');
299
+ });
300
+
301
+ it('supports keyboard navigation', async () => {
302
+ const onChange = vi.fn();
303
+ const user = userEvent.setup();
304
+
305
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} />);
306
+
307
+ const input = screen.getByLabelText(/Start Time/);
308
+ await user.tab();
309
+
310
+ expect(input).toHaveFocus();
311
+ });
312
+
313
+ it('announces errors to screen readers', () => {
314
+ const onChange = vi.fn();
315
+ render(
316
+ <DateTimeField
317
+ label="Start Time"
318
+ value={undefined}
319
+ onChange={onChange}
320
+ error="Invalid date"
321
+ />
322
+ );
323
+
324
+ const errorMessage = screen.getByRole('alert');
325
+ expect(errorMessage).toBeInTheDocument();
326
+ expect(errorMessage).toHaveTextContent('Invalid date');
327
+ });
328
+
329
+ it('indicates required field to screen readers', () => {
330
+ const onChange = vi.fn();
331
+ render(<DateTimeField label="Start Time" value={undefined} onChange={onChange} required />);
332
+
333
+ const requiredIndicator = screen.getByLabelText(/required/i);
334
+ expect(requiredIndicator).toBeInTheDocument();
335
+ });
336
+ });
337
+
338
+ describe('Controlled vs Uncontrolled', () => {
339
+ it('works as controlled component', () => {
340
+ const onChange = vi.fn();
341
+ const value = '2024-01-15T10:00:00Z';
342
+
343
+ const { rerender } = render(
344
+ <DateTimeField label="Start Time" value={value} onChange={onChange} />
345
+ );
346
+
347
+ const input = screen.getByLabelText(/Start Time/) as HTMLInputElement;
348
+ const initialValue = input.value;
349
+
350
+ rerender(
351
+ <DateTimeField label="Start Time" value="2024-01-16T10:00:00Z" onChange={onChange} />
352
+ );
353
+
354
+ expect(input.value).not.toBe(initialValue);
355
+ });
356
+ });
357
+ });
358
+