@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,232 @@
1
+ /**
2
+ * @file DateTimeField Component
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DateTimeField
5
+ * @since 0.1.0
6
+ *
7
+ * Form input component for datetime values with timezone support.
8
+ * Handles UTC ↔ timezone conversion automatically.
9
+ *
10
+ * Features:
11
+ * - Automatic UTC ↔ timezone conversion
12
+ * - Prevents unwanted conversions during user editing
13
+ * - Shows timezone information when not UTC
14
+ * - Supports both ISO string and Date object values
15
+ * - Uses native datetime-local input type
16
+ * - Accessible form field with proper labels
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { DateTimeField } from '@jmruthers/pace-core/components';
21
+ * import { useState } from 'react';
22
+ *
23
+ * function EventForm() {
24
+ * const [startTime, setStartTime] = useState<string>();
25
+ *
26
+ * return (
27
+ * <DateTimeField
28
+ * label="Start Time"
29
+ * value={startTime}
30
+ * onChange={setStartTime}
31
+ * timezone="America/New_York"
32
+ * required
33
+ * />
34
+ * );
35
+ * }
36
+ * ```
37
+ *
38
+ * @accessibility
39
+ * - Proper label association with htmlFor
40
+ * - Required field indicators
41
+ * - Screen reader friendly
42
+ * - Keyboard navigation support
43
+ * - Focus management
44
+ */
45
+
46
+ import * as React from 'react';
47
+ import { format, parse } from 'date-fns';
48
+ import { Label } from '../Label';
49
+ import { Input } from '../Input';
50
+ import { cn } from '../../utils/core/cn';
51
+ import { toZonedTime, fromZonedTime, getUserTimeZone } from '../../utils/timezone';
52
+
53
+ /**
54
+ * Props for the DateTimeField component
55
+ */
56
+ export interface DateTimeFieldProps {
57
+ /**
58
+ * Field label
59
+ */
60
+ label: string;
61
+ /**
62
+ * UTC date value (ISO string, Date object, or undefined)
63
+ */
64
+ value: string | Date | undefined;
65
+ /**
66
+ * Change handler that receives UTC value (ISO string or Date object)
67
+ */
68
+ onChange: (value: string | Date | undefined) => void;
69
+ /**
70
+ * Target timezone for display (default: 'UTC')
71
+ */
72
+ timezone?: string;
73
+ /**
74
+ * Whether the field is required
75
+ */
76
+ required?: boolean;
77
+ /**
78
+ * Additional CSS classes
79
+ */
80
+ className?: string;
81
+ /**
82
+ * If true, onChange returns Date object instead of ISO string
83
+ */
84
+ returnAsDate?: boolean;
85
+ /**
86
+ * Input id (auto-generated if not provided)
87
+ */
88
+ id?: string;
89
+ /**
90
+ * Helper text to display below the label
91
+ */
92
+ helperText?: string;
93
+ /**
94
+ * Error message to display
95
+ */
96
+ error?: string;
97
+ }
98
+
99
+ /**
100
+ * DateTimeField component
101
+ * Form input for datetime values with automatic timezone conversion
102
+ *
103
+ * @param props - DateTimeField configuration
104
+ * @returns JSX.Element - The rendered datetime field
105
+ */
106
+ export function DateTimeField({
107
+ label,
108
+ value,
109
+ onChange,
110
+ timezone = 'UTC',
111
+ required = false,
112
+ className,
113
+ returnAsDate = false,
114
+ id,
115
+ helperText,
116
+ error
117
+ }: DateTimeFieldProps) {
118
+ const [isEditing, setIsEditing] = React.useState(false);
119
+ const inputRef = React.useRef<HTMLInputElement>(null);
120
+ const fieldId = id || `datetime-field-${React.useId()}`;
121
+
122
+ // Convert UTC value to timezone for display
123
+ const getDisplayValue = React.useCallback((): string => {
124
+ if (!value) {
125
+ return '';
126
+ }
127
+
128
+ try {
129
+ let dateObj: Date;
130
+ if (typeof value === 'string') {
131
+ dateObj = new Date(value);
132
+ } else {
133
+ dateObj = value;
134
+ }
135
+
136
+ if (!dateObj || isNaN(dateObj.getTime())) {
137
+ return '';
138
+ }
139
+
140
+ // Convert UTC to timezone
141
+ const zonedDate = toZonedTime(dateObj, timezone);
142
+
143
+ // Format for datetime-local input (YYYY-MM-DDTHH:mm)
144
+ return format(zonedDate, "yyyy-MM-dd'T'HH:mm");
145
+ } catch {
146
+ return '';
147
+ }
148
+ }, [value, timezone]);
149
+
150
+ const displayValue = isEditing ? undefined : getDisplayValue();
151
+
152
+ // Handle input change
153
+ const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
154
+ setIsEditing(true);
155
+ const inputValue = e.target.value;
156
+
157
+ if (!inputValue) {
158
+ onChange(undefined);
159
+ return;
160
+ }
161
+
162
+ try {
163
+ // Parse the datetime-local value (in timezone)
164
+ const localDate = parse(inputValue, "yyyy-MM-dd'T'HH:mm", new Date());
165
+
166
+ if (isNaN(localDate.getTime())) {
167
+ onChange(undefined);
168
+ return;
169
+ }
170
+
171
+ // Convert from timezone to UTC
172
+ const utcDate = fromZonedTime(localDate, timezone);
173
+
174
+ // Return as ISO string or Date object
175
+ if (returnAsDate) {
176
+ onChange(utcDate);
177
+ } else {
178
+ onChange(utcDate.toISOString());
179
+ }
180
+ } catch {
181
+ onChange(undefined);
182
+ }
183
+ }, [timezone, returnAsDate, onChange]);
184
+
185
+ // Handle blur to stop editing mode
186
+ const handleBlur = React.useCallback(() => {
187
+ setIsEditing(false);
188
+ }, []);
189
+
190
+ // Get timezone display text
191
+ const getTimezoneDisplay = React.useCallback((): string => {
192
+ if (timezone === 'UTC') {
193
+ return '';
194
+ }
195
+
196
+ const userTz = getUserTimeZone();
197
+ if (timezone === userTz) {
198
+ return 'Local';
199
+ }
200
+
201
+ return timezone;
202
+ }, [timezone]);
203
+
204
+ const timezoneDisplay = getTimezoneDisplay();
205
+
206
+ return (
207
+ <div className={cn('space-y-2', className)}>
208
+ <Label htmlFor={fieldId} required={required} helperText={helperText} error={error}>
209
+ {label}
210
+ </Label>
211
+ <div className="relative">
212
+ <Input
213
+ ref={inputRef}
214
+ id={fieldId}
215
+ type="datetime-local"
216
+ value={displayValue}
217
+ onChange={handleChange}
218
+ onBlur={handleBlur}
219
+ required={required}
220
+ error={!!error}
221
+ className="w-full"
222
+ />
223
+ {timezoneDisplay && (
224
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none">
225
+ {timezoneDisplay}
226
+ </span>
227
+ )}
228
+ </div>
229
+ </div>
230
+ );
231
+ }
232
+
@@ -0,0 +1,148 @@
1
+ # DateTimeField Component
2
+
3
+ Form input component for datetime values with automatic UTC ↔ timezone conversion.
4
+
5
+ ## Overview
6
+
7
+ The `DateTimeField` component provides a datetime input field that automatically handles conversion between UTC (for storage) and a specified timezone (for display). It prevents unwanted conversions during user editing and displays timezone information when not using UTC.
8
+
9
+ ## Features
10
+
11
+ - Automatic UTC ↔ timezone conversion
12
+ - Prevents unwanted conversions during user editing
13
+ - Shows timezone information when not UTC
14
+ - Supports both ISO string and Date object values
15
+ - Uses native `datetime-local` input type
16
+ - Accessible form field with proper labels
17
+ - Error state support
18
+ - Helper text support
19
+
20
+ ## Usage
21
+
22
+ ### Basic Example
23
+
24
+ ```tsx
25
+ import { DateTimeField } from '@jmruthers/pace-core/components';
26
+ import { useState } from 'react';
27
+
28
+ function EventForm() {
29
+ const [startTime, setStartTime] = useState<string>();
30
+
31
+ return (
32
+ <DateTimeField
33
+ label="Start Time"
34
+ value={startTime}
35
+ onChange={setStartTime}
36
+ timezone="America/New_York"
37
+ required
38
+ />
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### With Date Object
44
+
45
+ ```tsx
46
+ import { DateTimeField } from '@jmruthers/pace-core/components';
47
+ import { useState } from 'react';
48
+
49
+ function EventForm() {
50
+ const [startTime, setStartTime] = useState<Date>();
51
+
52
+ return (
53
+ <DateTimeField
54
+ label="Start Time"
55
+ value={startTime}
56
+ onChange={setStartTime}
57
+ timezone="America/New_York"
58
+ returnAsDate
59
+ />
60
+ );
61
+ }
62
+ ```
63
+
64
+ ### With Error State
65
+
66
+ ```tsx
67
+ <DateTimeField
68
+ label="Start Time"
69
+ value={startTime}
70
+ onChange={setStartTime}
71
+ timezone="America/New_York"
72
+ error="Please select a valid start time"
73
+ required
74
+ />
75
+ ```
76
+
77
+ ### With Helper Text
78
+
79
+ ```tsx
80
+ <DateTimeField
81
+ label="Start Time"
82
+ value={startTime}
83
+ onChange={setStartTime}
84
+ timezone="America/New_York"
85
+ helperText="Select the event start time in your local timezone"
86
+ />
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### Props
92
+
93
+ | Prop | Type | Default | Description |
94
+ |------|------|---------|-------------|
95
+ | `label` | `string` | **required** | Field label |
96
+ | `value` | `string \| Date \| undefined` | - | UTC date value (ISO string, Date object, or undefined) |
97
+ | `onChange` | `(value: string \| Date \| undefined) => void` | **required** | Change handler that receives UTC value |
98
+ | `timezone` | `string` | `'UTC'` | Target timezone for display (IANA timezone string) |
99
+ | `required` | `boolean` | `false` | Whether the field is required |
100
+ | `className` | `string` | - | Additional CSS classes |
101
+ | `returnAsDate` | `boolean` | `false` | If true, onChange returns Date object instead of ISO string |
102
+ | `id` | `string` | - | Input id (auto-generated if not provided) |
103
+ | `helperText` | `string` | - | Helper text to display below the label |
104
+ | `error` | `string` | - | Error message to display |
105
+
106
+ ## Behavior
107
+
108
+ ### Timezone Conversion
109
+
110
+ The component automatically converts between UTC (for storage) and the specified timezone (for display):
111
+
112
+ - **Display**: UTC values are converted to the specified timezone for display in the input
113
+ - **Input**: User-entered values (in the timezone) are converted back to UTC before calling `onChange`
114
+
115
+ ### Editing Prevention
116
+
117
+ The component tracks editing state to prevent unwanted conversions while the user is typing. Conversions only occur when:
118
+ - The component receives a new `value` prop (controlled component)
119
+ - The user finishes editing (blur event)
120
+
121
+ ### Timezone Display
122
+
123
+ - When `timezone` is `'UTC'`: No timezone indicator is shown
124
+ - When `timezone` matches the user's browser timezone: Shows "Local"
125
+ - When `timezone` is different: Shows the timezone name (e.g., "America/New_York")
126
+
127
+ ## Accessibility
128
+
129
+ - Proper label association with `htmlFor`
130
+ - Required field indicators for screen readers
131
+ - Error messages with `role="alert"`
132
+ - Keyboard navigation support
133
+ - Focus management
134
+
135
+ ## Edge Cases
136
+
137
+ - **Invalid dates**: Returns empty string in input, calls `onChange(undefined)`
138
+ - **Null/undefined values**: Displays empty input
139
+ - **Invalid timezone**: Falls back to UTC behavior
140
+
141
+ ## Dependencies
142
+
143
+ - `date-fns` - Date formatting and parsing
144
+ - `date-fns-tz` - Timezone conversion
145
+ - `@jmruthers/pace-core/components/Label` - Label component
146
+ - `@jmruthers/pace-core/components/Input` - Input component
147
+ - `@jmruthers/pace-core/utils/timezone` - Timezone utilities
148
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file DateTimeField Component Exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DateTimeField
5
+ * @since 0.1.0
6
+ */
7
+
8
+ export { DateTimeField } from './DateTimeField';
9
+ export type { DateTimeFieldProps } from './DateTimeField';
10
+
@@ -71,6 +71,8 @@ describe('[component] FileUpload', () => {
71
71
  organisation_id: 'org-1',
72
72
  app_id: 'app-123',
73
73
  category: FileCategory.PROFILE_PHOTOS,
74
+ folder: 'profile_photos',
75
+ pageContext: 'test',
74
76
  accept: 'image/*',
75
77
  } as const;
76
78
 
@@ -22,6 +22,9 @@ export interface FileUploadProps {
22
22
  organisation_id: string;
23
23
  app_id?: string; // Optional - will be resolved from app name if not provided
24
24
  category: FileCategory;
25
+ folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
26
+ pageContext: string; // The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
27
+ event_id?: string; // Optional event ID for event-scoped permission checks (required for event-based apps)
25
28
  accept?: string;
26
29
  maxSize?: number;
27
30
  multiple?: boolean;
@@ -50,6 +53,9 @@ export function FileUpload({
50
53
  organisation_id,
51
54
  app_id,
52
55
  category,
56
+ folder,
57
+ pageContext,
58
+ event_id,
53
59
  accept = '*/*',
54
60
  maxSize = 10 * 1024 * 1024, // 10MB default
55
61
  multiple = false,
@@ -284,6 +290,9 @@ export function FileUpload({
284
290
  organisation_id,
285
291
  app_id: resolvedAppId ? assertAppId(resolvedAppId) : assertAppId(''),
286
292
  category,
293
+ folder,
294
+ pageContext,
295
+ event_id,
287
296
  is_public: isPublic
288
297
  }, file);
289
298
 
@@ -381,7 +390,7 @@ export function FileUpload({
381
390
  onUploadError?.(errorMessage, file);
382
391
  }
383
392
  }
384
- }, [uploadFile, table_name, record_id, organisation_id, resolvedAppId, category, isPublic, maxSize, onUploadSuccess, onUploadError, onProgress, validateFile, generatePreview, showPreview, appIdError]);
393
+ }, [uploadFile, table_name, record_id, organisation_id, resolvedAppId, category, folder, isPublic, maxSize, onUploadSuccess, onUploadError, onProgress, validateFile, generatePreview, showPreview, appIdError]);
385
394
 
386
395
  const handleDragOver = useCallback((e: React.DragEvent) => {
387
396
  e.preventDefault();
@@ -60,6 +60,14 @@ vi.mock('../EventSelector', () => ({
60
60
  ),
61
61
  }));
62
62
 
63
+ vi.mock('../OrganisationSelector', () => ({
64
+ OrganisationSelector: ({ placeholder, className, 'data-testid': testId }: any) => (
65
+ <div data-testid={testId || 'org-selector'} className={className}>
66
+ <button>{placeholder}</button>
67
+ </div>
68
+ ),
69
+ }));
70
+
63
71
  // Test data
64
72
  const mockUser: User = {
65
73
  id: '123',
@@ -312,16 +320,8 @@ describe('Header Component', () => {
312
320
  expect(screen.getByRole('button', { name: 'Select event' })).toBeInTheDocument();
313
321
  });
314
322
 
315
- it('renders placeholder when showEventSelector is false', () => {
316
- const { container } = renderWithProviders(<Header showEventSelector={false} />);
317
-
318
- const placeholder = container.querySelector('del.invisible');
319
- expect(placeholder).toBeInTheDocument();
320
- expect(placeholder).toHaveTextContent('Event Selector N/A');
321
- });
322
-
323
- it('preserves grid layout when event selector is hidden', () => {
324
- const { container } = renderWithProviders(
323
+ it('preserves layout when event selector is hidden', () => {
324
+ renderWithProviders(
325
325
  <Header
326
326
  showEventSelector={false}
327
327
  user={mockUser}
@@ -333,15 +333,47 @@ describe('Header Component', () => {
333
333
  expect(nav).toBeInTheDocument();
334
334
  expect(nav).toBeVisible();
335
335
 
336
- // Placeholder should maintain grid structure
337
- const placeholder = container.querySelector('del.invisible');
338
- expect(placeholder).toBeInTheDocument();
336
+ // Event selector should not be rendered
337
+ expect(screen.queryByTestId('event-selector')).not.toBeInTheDocument();
339
338
 
340
339
  // User menu should still be present and positioned correctly
341
340
  expect(screen.getByTestId('user-menu')).toBeInTheDocument();
342
341
  });
343
342
  });
344
343
 
344
+ // Organisation Selector tests
345
+ describe('Organisation Selector', () => {
346
+ it('does not render organisation selector by default', () => {
347
+ renderWithProviders(<Header />);
348
+
349
+ expect(screen.queryByTestId('org-selector')).not.toBeInTheDocument();
350
+ });
351
+
352
+ it('renders organisation selector when showOrgSelector is true', () => {
353
+ renderWithProviders(<Header showOrgSelector={true} />);
354
+
355
+ expect(screen.getByTestId('org-selector')).toBeInTheDocument();
356
+ });
357
+
358
+ it('does not render organisation selector when showOrgSelector is false', () => {
359
+ renderWithProviders(<Header showOrgSelector={false} />);
360
+
361
+ expect(screen.queryByTestId('org-selector')).not.toBeInTheDocument();
362
+ });
363
+
364
+ it('can render both organisation and event selectors together', () => {
365
+ renderWithProviders(
366
+ <Header
367
+ showOrgSelector={true}
368
+ showEventSelector={true}
369
+ />
370
+ );
371
+
372
+ expect(screen.getByTestId('org-selector')).toBeInTheDocument();
373
+ expect(screen.getByTestId('event-selector')).toBeInTheDocument();
374
+ });
375
+ });
376
+
345
377
  // Custom actions tests
346
378
  describe('Custom Actions', () => {
347
379
  it('renders custom actions when provided', () => {
@@ -560,7 +592,7 @@ describe('Header Component', () => {
560
592
  });
561
593
 
562
594
  it('renders minimal configuration', () => {
563
- const { container } = renderWithProviders(
595
+ renderWithProviders(
564
596
  <Header
565
597
  showEventSelector={false}
566
598
  showUserMenu={false}
@@ -573,10 +605,7 @@ describe('Header Component', () => {
573
605
  expect(screen.queryByTestId('navigation-menu')).not.toBeInTheDocument();
574
606
  expect(screen.queryByTestId('event-selector')).not.toBeInTheDocument();
575
607
  expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
576
-
577
- // Should have placeholder for event selector
578
- const placeholder = container.querySelector('del.invisible');
579
- expect(placeholder).toBeInTheDocument();
608
+ expect(screen.queryByTestId('org-selector')).not.toBeInTheDocument();
580
609
  });
581
610
  });
582
611
 
@@ -5,13 +5,14 @@
5
5
  * @since 0.1.0
6
6
  *
7
7
  * A comprehensive header component for application layouts with navigation,
8
- * user menu, event selector, and customizable branding.
8
+ * user menu, organisation selector, event selector, and customizable branding.
9
9
  *
10
10
  * Features:
11
11
  * - Customizable logo (URL or component)
12
12
  * - Clickable logo that routes to dashboard (configurable)
13
13
  * - Navigation menu integration
14
14
  * - User menu with authentication
15
+ * - Organisation selector for multi-organisation applications
15
16
  * - Event selector for multi-tenant applications
16
17
  * - Custom actions support
17
18
  * - Responsive design
@@ -82,6 +83,7 @@
82
83
  * - Tailwind CSS - Styling
83
84
  * - NavigationMenu component
84
85
  * - UserMenu component
86
+ * - OrganisationSelector component
85
87
  * - EventSelector component
86
88
  */
87
89
 
@@ -90,6 +92,7 @@ import { Link } from 'react-router-dom';
90
92
  import { User } from '@supabase/supabase-js';
91
93
  import { cn } from '../../utils/core/cn';
92
94
  import { EventSelector } from '../EventSelector';
95
+ import { OrganisationSelector } from '../OrganisationSelector';
93
96
  import { UserMenu } from '../UserMenu';
94
97
  import { NavigationMenu } from '../NavigationMenu';
95
98
  import type { NavigationItem } from '../NavigationMenu';
@@ -121,6 +124,8 @@ export interface HeaderProps {
121
124
  className?: string;
122
125
  /** Show/hide event selector */
123
126
  showEventSelector?: boolean;
127
+ /** Show/hide organisation selector */
128
+ showOrgSelector?: boolean;
124
129
  /** Show/hide user menu */
125
130
  showUserMenu?: boolean;
126
131
  /** Current path for navigation highlighting */
@@ -244,6 +249,7 @@ export function Header({
244
249
  userMenu,
245
250
  className,
246
251
  showEventSelector = true,
252
+ showOrgSelector = false,
247
253
  showUserMenu = true,
248
254
  currentPath,
249
255
  onNavigate,
@@ -254,7 +260,7 @@ export function Header({
254
260
  "w-full border-b border-main-200 h-16 shadow-sm bg-main-100 ",
255
261
  className
256
262
  )} role="banner">
257
- <nav className="px-4 w-[min(var(--app-width),100%)] mx-auto grid grid-cols-[auto_auto_1fr_auto] gap-4 h-full items-center">
263
+ <nav className="px-4 w-[min(var(--app-width),100%)] mx-auto flex items-center gap-4 h-full">
258
264
  {/* Logo */}
259
265
  {logo ? (
260
266
  logoHref ? (
@@ -311,18 +317,26 @@ export function Header({
311
317
  )}
312
318
 
313
319
 
314
- {/* Right side: Event Selector, Actions, and User Menu */}
320
+ {/* Right side: Organisation Selector, Event Selector, Actions, and User Menu */}
321
+ <div className="flex items-center gap-4 ml-auto">
322
+ {/* Organisation Selector */}
323
+ {showOrgSelector ? (
324
+ <OrganisationSelector
325
+ placeholder="Select organisation"
326
+ className="w-64"
327
+ data-testid="org-selector"
328
+ compact={true}
329
+ />
330
+ ) : null}
315
331
 
316
332
  {/* Event Selector */}
317
333
  {showEventSelector ? (
318
334
  <EventSelector
319
335
  placeholder="Select event"
320
- className="justify-self-end w-96"
336
+ className="w-96"
321
337
  data-testid="event-selector"
322
338
  />
323
- ) : (
324
- <del className="justify-self-end invisible">Event Selector N/A</del>
325
- )}
339
+ ) : null}
326
340
 
327
341
  {/* Custom Actions */}
328
342
  {actions}
@@ -340,6 +354,7 @@ export function Header({
340
354
  />
341
355
  )
342
356
  )}
357
+ </div>
343
358
 
344
359
  </nav>
345
360
  </header>