@jmruthers/pace-core 0.5.188 → 0.5.190

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 (424) hide show
  1. package/core-usage-manifest.json +0 -4
  2. package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
  3. package/dist/{DataTable-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
  4. package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-C4uxosp6.d.ts} +129 -40
  5. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  6. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
  7. package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
  8. package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
  9. package/dist/chunk-4QYC5L4K.js.map +1 -0
  10. package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
  11. package/dist/chunk-73HSNNOQ.js.map +1 -0
  12. package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
  13. package/dist/chunk-DZWK57KZ.js.map +1 -0
  14. package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
  15. package/dist/chunk-HQVPB5MZ.js.map +1 -0
  16. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  17. package/dist/chunk-HW3OVDUF.js.map +1 -0
  18. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  19. package/dist/chunk-I7PSE6JW.js.map +1 -0
  20. package/dist/{chunk-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
  21. package/dist/chunk-J2XXC7R5.js.map +1 -0
  22. package/dist/{chunk-UNOTYLQF.js → chunk-NIU6J6OX.js} +772 -725
  23. package/dist/chunk-NIU6J6OX.js.map +1 -0
  24. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  25. package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
  26. package/dist/chunk-RUYZKXOD.js.map +1 -0
  27. package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
  28. package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
  29. package/dist/{chunk-IPCH26AG.js → chunk-STYK4OH2.js} +11 -11
  30. package/dist/chunk-STYK4OH2.js.map +1 -0
  31. package/dist/{chunk-EFCLXK7F.js → chunk-VVBAW5A5.js} +4201 -3809
  32. package/dist/chunk-VVBAW5A5.js.map +1 -0
  33. package/dist/chunk-Y4BUBBHD.js +614 -0
  34. package/dist/chunk-Y4BUBBHD.js.map +1 -0
  35. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  36. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  37. package/dist/components.d.ts +3 -5
  38. package/dist/components.js +19 -23
  39. package/dist/components.js.map +1 -1
  40. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  41. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  42. package/dist/hooks.d.ts +10 -5
  43. package/dist/hooks.js +14 -8
  44. package/dist/hooks.js.map +1 -1
  45. package/dist/index.d.ts +13 -12
  46. package/dist/index.js +79 -73
  47. package/dist/index.js.map +1 -1
  48. package/dist/providers.d.ts +3 -3
  49. package/dist/providers.js +3 -1
  50. package/dist/rbac/index.d.ts +76 -12
  51. package/dist/rbac/index.js +12 -9
  52. package/dist/types.d.ts +1 -1
  53. package/dist/types.js +1 -1
  54. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
  55. package/dist/utils.js +16 -16
  56. package/docs/README.md +2 -2
  57. package/docs/api/classes/ColumnFactory.md +1 -1
  58. package/docs/api/classes/ErrorBoundary.md +1 -1
  59. package/docs/api/classes/InvalidScopeError.md +2 -2
  60. package/docs/api/classes/Logger.md +1 -1
  61. package/docs/api/classes/MissingUserContextError.md +2 -2
  62. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  63. package/docs/api/classes/PermissionDeniedError.md +1 -1
  64. package/docs/api/classes/RBACAuditManager.md +1 -1
  65. package/docs/api/classes/RBACCache.md +1 -1
  66. package/docs/api/classes/RBACEngine.md +4 -4
  67. package/docs/api/classes/RBACError.md +1 -1
  68. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  69. package/docs/api/classes/SecureSupabaseClient.md +21 -16
  70. package/docs/api/classes/StorageUtils.md +7 -4
  71. package/docs/api/enums/FileCategory.md +1 -1
  72. package/docs/api/enums/LogLevel.md +1 -1
  73. package/docs/api/enums/RBACErrorCode.md +1 -1
  74. package/docs/api/enums/RPCFunction.md +1 -1
  75. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  76. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  77. package/docs/api/interfaces/AggregateConfig.md +1 -1
  78. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  79. package/docs/api/interfaces/AvatarProps.md +128 -0
  80. package/docs/api/interfaces/BadgeProps.md +1 -1
  81. package/docs/api/interfaces/ButtonProps.md +1 -1
  82. package/docs/api/interfaces/CalendarProps.md +20 -6
  83. package/docs/api/interfaces/CardProps.md +1 -1
  84. package/docs/api/interfaces/ColorPalette.md +1 -1
  85. package/docs/api/interfaces/ColorShade.md +1 -1
  86. package/docs/api/interfaces/ComplianceResult.md +1 -1
  87. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  88. package/docs/api/interfaces/DataRecord.md +1 -1
  89. package/docs/api/interfaces/DataTableAction.md +1 -1
  90. package/docs/api/interfaces/DataTableColumn.md +1 -1
  91. package/docs/api/interfaces/DataTableProps.md +1 -1
  92. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  93. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  94. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  95. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  96. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  97. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  98. package/docs/api/interfaces/ExportColumn.md +1 -1
  99. package/docs/api/interfaces/ExportOptions.md +1 -1
  100. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  101. package/docs/api/interfaces/FileMetadata.md +1 -1
  102. package/docs/api/interfaces/FileReference.md +2 -2
  103. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  104. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  105. package/docs/api/interfaces/FileUploadProps.md +30 -19
  106. package/docs/api/interfaces/FooterProps.md +1 -1
  107. package/docs/api/interfaces/FormFieldProps.md +1 -1
  108. package/docs/api/interfaces/FormProps.md +1 -1
  109. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  110. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  111. package/docs/api/interfaces/InputProps.md +1 -1
  112. package/docs/api/interfaces/LabelProps.md +1 -1
  113. package/docs/api/interfaces/LoggerConfig.md +1 -1
  114. package/docs/api/interfaces/LoginFormProps.md +1 -1
  115. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  116. package/docs/api/interfaces/NavigationContextType.md +9 -9
  117. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  118. package/docs/api/interfaces/NavigationItem.md +1 -1
  119. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  120. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  121. package/docs/api/interfaces/Organisation.md +1 -1
  122. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  123. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  124. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  125. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  126. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  127. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  128. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  129. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  130. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  131. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  132. package/docs/api/interfaces/PaletteData.md +1 -1
  133. package/docs/api/interfaces/ParsedAddress.md +1 -1
  134. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  135. package/docs/api/interfaces/ProgressProps.md +3 -11
  136. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  138. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  139. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  140. package/docs/api/interfaces/QuickFix.md +1 -1
  141. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  142. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  143. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  144. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  145. package/docs/api/interfaces/RBACConfig.md +1 -1
  146. package/docs/api/interfaces/RBACContext.md +1 -1
  147. package/docs/api/interfaces/RBACLogger.md +1 -1
  148. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  149. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  151. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  152. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  153. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  154. package/docs/api/interfaces/RBACResult.md +1 -1
  155. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  156. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  157. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  158. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  161. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  162. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  163. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  164. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  165. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  166. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  167. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  168. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  169. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  170. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  171. package/docs/api/interfaces/RouteConfig.md +10 -10
  172. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  173. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  174. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  175. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  176. package/docs/api/interfaces/SetupIssue.md +1 -1
  177. package/docs/api/interfaces/StorageConfig.md +4 -4
  178. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  179. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  180. package/docs/api/interfaces/StorageListOptions.md +22 -9
  181. package/docs/api/interfaces/StorageListResult.md +4 -4
  182. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  183. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  184. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  185. package/docs/api/interfaces/StyleImport.md +1 -1
  186. package/docs/api/interfaces/SwitchProps.md +1 -1
  187. package/docs/api/interfaces/TabsContentProps.md +1 -1
  188. package/docs/api/interfaces/TabsListProps.md +1 -1
  189. package/docs/api/interfaces/TabsProps.md +1 -1
  190. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  191. package/docs/api/interfaces/TextareaProps.md +1 -1
  192. package/docs/api/interfaces/ToastActionElement.md +1 -1
  193. package/docs/api/interfaces/ToastProps.md +1 -1
  194. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  195. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  196. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  197. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  198. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  199. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  201. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  202. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  203. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  205. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  207. package/docs/api/interfaces/UseResolvedScopeOptions.md +4 -4
  208. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  209. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  210. package/docs/api/interfaces/UserEventAccess.md +11 -11
  211. package/docs/api/interfaces/UserMenuProps.md +1 -1
  212. package/docs/api/interfaces/UserProfile.md +1 -1
  213. package/docs/api/modules.md +155 -135
  214. package/docs/api-reference/components.md +72 -29
  215. package/docs/api-reference/providers.md +2 -2
  216. package/docs/api-reference/rpc-functions.md +1 -0
  217. package/docs/best-practices/README.md +1 -1
  218. package/docs/best-practices/deployment.md +8 -8
  219. package/docs/getting-started/examples/README.md +2 -2
  220. package/docs/getting-started/installation-guide.md +4 -4
  221. package/docs/getting-started/quick-start.md +3 -3
  222. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  223. package/docs/rbac/compliance/compliance-guide.md +2 -2
  224. package/docs/rbac/event-based-apps.md +2 -2
  225. package/docs/rbac/getting-started.md +2 -2
  226. package/docs/rbac/quick-start.md +2 -2
  227. package/docs/security/README.md +4 -4
  228. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  229. package/docs/troubleshooting/README.md +2 -2
  230. package/docs/troubleshooting/migration.md +3 -3
  231. package/package.json +1 -4
  232. package/scripts/check-pace-core-compliance.cjs +1 -1
  233. package/scripts/check-pace-core-compliance.js +1 -1
  234. package/src/__tests__/fixtures/supabase.ts +301 -0
  235. package/src/__tests__/public-recipe-view.test.ts +9 -9
  236. package/src/__tests__/rls-policies.test.ts +197 -61
  237. package/src/components/AddressField/AddressField.test.tsx +42 -0
  238. package/src/components/AddressField/AddressField.tsx +71 -60
  239. package/src/components/AddressField/README.md +1 -0
  240. package/src/components/Alert/Alert.test.tsx +50 -10
  241. package/src/components/Alert/Alert.tsx +5 -3
  242. package/src/components/Avatar/Avatar.test.tsx +252 -226
  243. package/src/components/Avatar/Avatar.tsx +179 -53
  244. package/src/components/Avatar/index.ts +1 -1
  245. package/src/components/Button/Button.test.tsx +2 -1
  246. package/src/components/Button/Button.tsx +3 -3
  247. package/src/components/Calendar/Calendar.test.tsx +53 -37
  248. package/src/components/Calendar/Calendar.tsx +409 -82
  249. package/src/components/Card/Card.test.tsx +7 -4
  250. package/src/components/Card/Card.tsx +3 -6
  251. package/src/components/Checkbox/Checkbox.tsx +2 -2
  252. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  253. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  254. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  255. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  256. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  257. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  258. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  259. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  260. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  261. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  262. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  263. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  264. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  265. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  266. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  267. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  268. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  269. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  270. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  271. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  272. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  273. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  274. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  275. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  276. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  277. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  278. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  279. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  280. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  281. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  282. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  283. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  284. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  285. package/src/components/Dialog/Dialog.tsx +2 -2
  286. package/src/components/EventSelector/EventSelector.tsx +7 -7
  287. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  288. package/src/components/FileUpload/FileUpload.tsx +7 -4
  289. package/src/components/Header/Header.test.tsx +28 -0
  290. package/src/components/Header/Header.tsx +22 -9
  291. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  292. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  293. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  294. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  295. package/src/components/OrganisationSelector/OrganisationSelector.tsx +8 -8
  296. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  297. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  298. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  299. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  300. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  301. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  302. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  303. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  304. package/src/components/Progress/Progress.test.tsx +18 -19
  305. package/src/components/Progress/Progress.tsx +31 -32
  306. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  307. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  308. package/src/components/Select/Select.tsx +5 -5
  309. package/src/components/Switch/Switch.test.tsx +2 -1
  310. package/src/components/Switch/Switch.tsx +1 -1
  311. package/src/components/Toast/Toast.tsx +1 -1
  312. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  313. package/src/components/UserMenu/UserMenu.test.tsx +7 -9
  314. package/src/components/UserMenu/UserMenu.tsx +10 -8
  315. package/src/components/index.ts +2 -1
  316. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  317. package/src/eslint-rules/pace-core-compliance.js +0 -2
  318. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  319. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  320. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  321. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  322. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  323. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  324. package/src/hooks/__tests__/{usePublicEvent.unit.test.ts → usePublicEvent.test.ts} +28 -1
  325. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  326. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
  327. package/src/hooks/index.ts +1 -1
  328. package/src/hooks/public/usePublicEvent.ts +2 -2
  329. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  330. package/src/hooks/useAppConfig.ts +24 -5
  331. package/src/hooks/useFileDisplay.ts +297 -34
  332. package/src/hooks/useFileReference.ts +56 -11
  333. package/src/hooks/useFileUrl.ts +1 -1
  334. package/src/hooks/useInactivityTracker.ts +16 -7
  335. package/src/hooks/usePermissionCache.test.ts +85 -8
  336. package/src/hooks/useQueryCache.ts +21 -0
  337. package/src/hooks/useSecureDataAccess.test.ts +80 -35
  338. package/src/hooks/useSecureDataAccess.ts +80 -37
  339. package/src/index.ts +2 -1
  340. package/src/providers/services/EventServiceProvider.tsx +37 -17
  341. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  342. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  343. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  344. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  345. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  346. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  347. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  348. package/src/rbac/api.ts +240 -36
  349. package/src/rbac/cache-invalidation.ts +21 -7
  350. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  351. package/src/rbac/components/NavigationGuard.tsx +23 -63
  352. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  353. package/src/rbac/components/NavigationProvider.tsx +13 -11
  354. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  355. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  356. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  357. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  358. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  359. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  360. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  361. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  362. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  363. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  364. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  365. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  366. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  367. package/src/rbac/engine.ts +4 -2
  368. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  369. package/src/rbac/hooks/index.ts +3 -0
  370. package/src/rbac/hooks/useCan.test.ts +101 -53
  371. package/src/rbac/hooks/usePermissions.ts +108 -41
  372. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  373. package/src/rbac/hooks/useRBAC.ts +83 -40
  374. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  375. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  376. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  377. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  378. package/src/rbac/request-deduplication.ts +1 -1
  379. package/src/rbac/secureClient.ts +72 -12
  380. package/src/rbac/security.ts +29 -23
  381. package/src/rbac/types.ts +10 -0
  382. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  383. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  384. package/src/rbac/utils/__tests__/eventContext.test.ts +6 -1
  385. package/src/rbac/utils/contextValidator.ts +288 -0
  386. package/src/rbac/utils/eventContext.ts +48 -2
  387. package/src/services/EventService.ts +165 -21
  388. package/src/services/OrganisationService.ts +37 -2
  389. package/src/services/__tests__/EventService.test.ts +26 -21
  390. package/src/types/file-reference.ts +13 -10
  391. package/src/utils/app/appNameResolver.test.ts +346 -73
  392. package/src/utils/context/superAdminOverride.ts +58 -0
  393. package/src/utils/file-reference/index.ts +61 -33
  394. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  395. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  396. package/src/utils/storage/helpers.test.ts +1 -1
  397. package/src/utils/storage/helpers.ts +38 -19
  398. package/src/utils/storage/types.ts +15 -8
  399. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  400. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  401. package/src/vite-env.d.ts +2 -2
  402. package/dist/chunk-3GOZZZYH.js.map +0 -1
  403. package/dist/chunk-DDM4CCYT.js.map +0 -1
  404. package/dist/chunk-E7UAOUMY.js +0 -75
  405. package/dist/chunk-E7UAOUMY.js.map +0 -1
  406. package/dist/chunk-EFCLXK7F.js.map +0 -1
  407. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  408. package/dist/chunk-HEHYGYOX.js.map +0 -1
  409. package/dist/chunk-IM4QE42D.js.map +0 -1
  410. package/dist/chunk-IPCH26AG.js.map +0 -1
  411. package/dist/chunk-SAUPYVLF.js.map +0 -1
  412. package/dist/chunk-THRPYOFK.js.map +0 -1
  413. package/dist/chunk-UNOTYLQF.js.map +0 -1
  414. package/dist/chunk-VGZZXKBR.js.map +0 -1
  415. package/dist/chunk-YHCN776L.js.map +0 -1
  416. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
  417. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
  418. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
  419. package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
  420. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
  421. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
  422. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
  423. /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
  424. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -19,7 +19,8 @@ export interface FileUploadProps {
19
19
  supabase: SupabaseClient;
20
20
  table_name: string;
21
21
  record_id: string;
22
- organisation_id: string;
22
+ organisation_id?: string | null; // Optional for user-scoped files (e.g., profile photos)
23
+ userId?: string; // Optional userId for user-scoped files (required if organisation_id is not provided)
23
24
  app_id?: string; // Optional - will be resolved from app name if not provided
24
25
  category: FileCategory;
25
26
  folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
@@ -51,6 +52,7 @@ export function FileUpload({
51
52
  table_name,
52
53
  record_id,
53
54
  organisation_id,
55
+ userId,
54
56
  app_id,
55
57
  category,
56
58
  folder,
@@ -287,7 +289,8 @@ export function FileUpload({
287
289
  const result = await uploadFile({
288
290
  table_name,
289
291
  record_id,
290
- organisation_id,
292
+ organisation_id: organisation_id || null,
293
+ userId: userId, // Pass userId prop directly - it's required for user-scoped files when organisation_id is null
291
294
  app_id: resolvedAppId ? assertAppId(resolvedAppId) : assertAppId(''),
292
295
  category,
293
296
  folder,
@@ -500,7 +503,7 @@ export function FileUpload({
500
503
  aria-live="polite"
501
504
  aria-label="Uploading file"
502
505
  >
503
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-main-500" aria-hidden="true"></div>
506
+ <div className="animate-spin rounded-full size-8 border-b-2 border-main-500" aria-hidden="true"></div>
504
507
  </div>
505
508
  )}
506
509
  </div>
@@ -581,7 +584,7 @@ export function FileUpload({
581
584
  )}
582
585
  {isUploading && (
583
586
  <div
584
- className="animate-spin rounded-full h-5 w-5 border-b-2 border-main-500"
587
+ className="animate-spin rounded-full size-5 border-b-2 border-main-500"
585
588
  role="status"
586
589
  aria-label="Uploading"
587
590
  aria-hidden="true"
@@ -68,6 +68,34 @@ vi.mock('../OrganisationSelector', () => ({
68
68
  ),
69
69
  }));
70
70
 
71
+ // Mock useOrganisations hook
72
+ vi.mock('../../hooks/useOrganisations', () => ({
73
+ useOrganisations: vi.fn(() => ({
74
+ organisations: [
75
+ {
76
+ id: 'test-org-id',
77
+ name: 'Test Organisation',
78
+ slug: 'test-org',
79
+ created_at: '2023-01-01T00:00:00Z',
80
+ updated_at: '2023-01-01T00:00:00Z'
81
+ }
82
+ ],
83
+ selectedOrganisation: {
84
+ id: 'test-org-id',
85
+ name: 'Test Organisation',
86
+ slug: 'test-org',
87
+ created_at: '2023-01-01T00:00:00Z',
88
+ updated_at: '2023-01-01T00:00:00Z'
89
+ },
90
+ isContextReady: true,
91
+ isLoading: false,
92
+ error: null,
93
+ selectOrganisation: vi.fn(),
94
+ refreshOrganisations: vi.fn(),
95
+ userMemberships: []
96
+ }))
97
+ }));
98
+
71
99
  // Test data
72
100
  const mockUser: User = {
73
101
  id: '123',
@@ -97,6 +97,7 @@ import { UserMenu } from '../UserMenu';
97
97
  import { NavigationMenu } from '../NavigationMenu';
98
98
  import type { NavigationItem } from '../NavigationMenu';
99
99
  import type { PasswordChangeFormError } from '../PasswordChange/PasswordChangeForm';
100
+ import { useOrganisations } from '../../hooks/useOrganisations';
100
101
 
101
102
  /**
102
103
  * Props for the Header component
@@ -255,6 +256,23 @@ export function Header({
255
256
  onNavigate,
256
257
  logoHref
257
258
  }: HeaderProps) {
259
+ // Conditional wrapper for organisation selector - only show if user has organisations
260
+ const OrganisationSelectorConditional = () => {
261
+ const { organisations, isContextReady } = useOrganisations();
262
+ // Only show selector if user has organisations and context is ready
263
+ if (!isContextReady || !organisations || organisations.length === 0) {
264
+ return null;
265
+ }
266
+ return (
267
+ <OrganisationSelector
268
+ placeholder="Select organisation"
269
+ className="w-64"
270
+ data-testid="org-selector"
271
+ compact={true}
272
+ />
273
+ );
274
+ };
275
+
258
276
  return (
259
277
  <header className={cn(
260
278
  "w-full border-b border-main-200 h-16 shadow-sm bg-main-100 ",
@@ -292,14 +310,14 @@ export function Header({
292
310
  <img
293
311
  src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
294
312
  alt={logoAlt || 'Logo'}
295
- className="h-8 w-8 shadow-md"
313
+ className="size-8 shadow-md"
296
314
  />
297
315
  </Link>
298
316
  ) : (
299
317
  <img
300
318
  src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
301
319
  alt={logoAlt || 'Logo'}
302
- className="h-8 w-8 shadow-md"
320
+ className="size-8 shadow-md"
303
321
  />
304
322
  )
305
323
  )}
@@ -319,14 +337,9 @@ export function Header({
319
337
 
320
338
  {/* Right side: Organisation Selector, Event Selector, Actions, and User Menu */}
321
339
  <div className="flex items-center gap-4 ml-auto">
322
- {/* Organisation Selector */}
340
+ {/* Organisation Selector - Only show if user has organisations */}
323
341
  {showOrgSelector ? (
324
- <OrganisationSelector
325
- placeholder="Select organisation"
326
- className="w-64"
327
- data-testid="org-selector"
328
- compact={true}
329
- />
342
+ <OrganisationSelectorConditional />
330
343
  ) : null}
331
344
 
332
345
  {/* Event Selector */}
@@ -103,7 +103,7 @@ export function InactivityWarningModal({
103
103
  <DialogHeader>
104
104
  <div className="flex items-center gap-3">
105
105
  <div className="flex-shrink-0">
106
- <AlertTriangle className="h-6 w-6 text-acc-600" />
106
+ <AlertTriangle className="size-6 text-acc-600" />
107
107
  </div>
108
108
  <div>
109
109
  <DialogTitle className="text-lg font-semibold text-main-900">
@@ -120,7 +120,7 @@ export function InactivityWarningModal({
120
120
  {/* Countdown Timer */}
121
121
  <div className="text-center">
122
122
  <div className="inline-flex items-center gap-2 px-4 py-3 bg-acc-50 border border-acc-200 rounded-lg">
123
- <Clock className="h-5 w-5 text-acc-600" />
123
+ <Clock className="size-5 text-acc-600" />
124
124
  <span className="text-2xl font-mono font-bold text-acc-700">
125
125
  {formatTime(displayTime)}
126
126
  </span>
@@ -54,44 +54,47 @@ describe('LoadingSpinner Component', () => {
54
54
  renderWithProviders(<LoadingSpinner size="sm" />);
55
55
 
56
56
  const spinner = screen.getByRole('status');
57
- expect(spinner).toHaveClass('w-4', 'h-4');
57
+ // LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
58
+ expect(spinner).toHaveClass('size-4');
58
59
  });
59
60
 
60
61
  it('renders medium size spinner (default)', () => {
61
62
  renderWithProviders(<LoadingSpinner size="md" />);
62
63
 
63
64
  const spinner = screen.getByRole('status');
64
- expect(spinner).toHaveClass('w-6', 'h-6');
65
+ // LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
66
+ expect(spinner).toHaveClass('size-6');
65
67
  });
66
68
 
67
69
  it('renders large size spinner', () => {
68
70
  renderWithProviders(<LoadingSpinner size="lg" />);
69
71
 
70
72
  const spinner = screen.getByRole('status');
71
- expect(spinner).toHaveClass('w-8', 'h-8');
73
+ // LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
74
+ expect(spinner).toHaveClass('size-8');
72
75
  });
73
76
 
74
77
  it('uses medium size as default when no size specified', () => {
75
78
  renderWithProviders(<LoadingSpinner />);
76
79
 
77
80
  const spinner = screen.getByRole('status');
78
- expect(spinner).toHaveClass('w-6', 'h-6');
81
+ // LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
82
+ expect(spinner).toHaveClass('size-6');
79
83
  });
80
84
 
81
85
  it('applies correct size classes for each variant', () => {
82
86
  const sizeTests: Array<{ size: LoadingSpinnerProps['size']; expectedClasses: string[] }> = [
83
- { size: 'sm', expectedClasses: ['w-4', 'h-4'] },
84
- { size: 'md', expectedClasses: ['w-6', 'h-6'] },
85
- { size: 'lg', expectedClasses: ['w-8', 'h-8'] },
87
+ { size: 'sm', expectedClass: 'size-4' },
88
+ { size: 'md', expectedClass: 'size-6' },
89
+ { size: 'lg', expectedClass: 'size-8' },
86
90
  ];
87
91
 
88
- sizeTests.forEach(({ size, expectedClasses }) => {
92
+ sizeTests.forEach(({ size, expectedClass }) => {
89
93
  const { unmount } = renderWithProviders(<LoadingSpinner size={size} />);
90
94
 
91
95
  const spinner = screen.getByRole('status');
92
- expectedClasses.forEach(className => {
93
- expect(spinner).toHaveClass(className);
94
- });
96
+ // LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
97
+ expect(spinner).toHaveClass(expectedClass);
95
98
 
96
99
  unmount();
97
100
  });
@@ -246,8 +249,9 @@ describe('LoadingSpinner Component', () => {
246
249
  // Component defaults to 'md' size when invalid size is passed
247
250
  expect(spinner).toHaveClass('inline-block', 'animate-spin');
248
251
  // Should default to medium size classes when invalid size is passed
249
- expect(spinner).toHaveClass('w-6', 'h-6');
250
- expect(spinner).not.toHaveClass('w-4', 'h-4', 'w-8', 'h-8');
252
+ // LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
253
+ expect(spinner).toHaveClass('size-6');
254
+ expect(spinner).not.toHaveClass('size-4', 'size-8');
251
255
  });
252
256
 
253
257
  it('handles null className gracefully', () => {
@@ -313,7 +317,8 @@ describe('LoadingSpinner Component', () => {
313
317
 
314
318
  expect(updatedSpinner).toBeInTheDocument();
315
319
  expect(updatedScreenReaderText).toBeInTheDocument();
316
- expect(updatedSpinner).toHaveClass('w-8', 'h-8', 'custom');
320
+ // LoadingSpinner uses Tailwind v4 size-* utility instead of h-* w-*
321
+ expect(updatedSpinner).toHaveClass('size-8', 'custom');
317
322
  });
318
323
  });
319
324
 
@@ -85,9 +85,9 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
85
85
  className = ''
86
86
  }) => {
87
87
  const sizeClasses: Record<'sm' | 'md' | 'lg', string> = {
88
- sm: 'w-4 h-4',
89
- md: 'w-6 h-6',
90
- lg: 'w-8 h-8'
88
+ sm: 'size-4',
89
+ md: 'size-6',
90
+ lg: 'size-8'
91
91
  };
92
92
 
93
93
  // Ensure we always have a valid size class, defaulting to 'md' if invalid
@@ -95,8 +95,8 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
95
95
  const sizeClass = sizeClasses[validSize];
96
96
 
97
97
  return (
98
- <div className={`inline-block animate-spin rounded-full border-2 border-solid border-current border-r-transparent motion-reduce:animate-[spin_1.5s_linear_infinite] ${sizeClass} ${className}`.trim()} role="status">
98
+ <canvas className={`inline-block animate-spin rounded-full border-2 border-solid border-current border-r-transparent motion-reduce:animate-[spin_1.5s_linear_infinite] ${sizeClass} ${className}`.trim()} role="status">
99
99
  <span className="sr-only">Loading...</span>
100
- </div>
100
+ </canvas>
101
101
  );
102
102
  };
@@ -373,6 +373,40 @@ describe('NavigationMenu Component', () => {
373
373
  expect(dashboardItem).toBeInTheDocument();
374
374
  }, { interval: 10 });
375
375
  });
376
+
377
+ it('trusts pre-filtered items while still hiding meta-hidden entries', async () => {
378
+ const user = userEvent.setup();
379
+
380
+ mockUsePermissions.mockReturnValue({
381
+ permissions: {},
382
+ isLoading: false,
383
+ error: null,
384
+ hasPermission: vi.fn(() => false),
385
+ hasAnyPermission: vi.fn(() => false),
386
+ hasAllPermissions: vi.fn(() => false),
387
+ refetch: vi.fn(),
388
+ });
389
+
390
+ renderWithProviders(
391
+ <NavigationMenu
392
+ items={[
393
+ { id: 'home', label: 'Home', href: '/' },
394
+ { id: 'hidden', label: 'Hidden', href: '/hidden', meta: { hidden: true } },
395
+ ]}
396
+ itemsPreFiltered
397
+ onNavigate={mockNavigate}
398
+ buttonText="Menu"
399
+ />
400
+ );
401
+
402
+ await user.click(screen.getByRole('combobox'));
403
+
404
+ await waitFor(() => {
405
+ expect(screen.getByText('Home')).toBeInTheDocument();
406
+ }, { interval: 10 });
407
+
408
+ expect(screen.queryByText('Hidden')).not.toBeInTheDocument();
409
+ });
376
410
  });
377
411
 
378
412
  // Hierarchical mode tests
@@ -931,6 +965,46 @@ describe('NavigationMenu Component', () => {
931
965
  expect.stringContaining('Insufficient permissions')
932
966
  );
933
967
  });
968
+
969
+ it('blocks navigation and reports violations when pre-filtered items lack permission', async () => {
970
+ const user = userEvent.setup();
971
+
972
+ mockUsePermissions.mockReturnValue({
973
+ permissions: { 'read:page.restricted': true } as any,
974
+ isLoading: false,
975
+ error: null,
976
+ hasPermission: vi.fn(() => false),
977
+ hasAnyPermission: vi.fn(() => false),
978
+ hasAllPermissions: vi.fn(() => false),
979
+ refetch: vi.fn(),
980
+ });
981
+
982
+ renderWithProviders(
983
+ <NavigationMenu
984
+ items={[{ id: 'restricted', label: 'Restricted', href: '/restricted', permissions: ['restricted:read'] }]}
985
+ onNavigate={mockNavigate}
986
+ onNavigationAccessDenied={mockOnNavigationAccessDenied}
987
+ onStrictModeViolation={mockOnStrictModeViolation}
988
+ itemsPreFiltered
989
+ strictMode
990
+ buttonText="Menu"
991
+ />
992
+ );
993
+
994
+ await user.click(screen.getByRole('combobox'));
995
+
996
+ const restrictedItem = await screen.findByText('Restricted');
997
+ await user.click(restrictedItem);
998
+
999
+ expect(mockNavigate).not.toHaveBeenCalled();
1000
+ expect(mockOnNavigationAccessDenied).toHaveBeenCalledWith(
1001
+ expect.objectContaining({ id: 'restricted' })
1002
+ );
1003
+ expect(mockOnStrictModeViolation).toHaveBeenCalledWith(
1004
+ expect.objectContaining({ id: 'restricted' }),
1005
+ 'Insufficient permissions'
1006
+ );
1007
+ });
934
1008
  });
935
1009
 
936
1010
  // Accessibility tests
@@ -1210,6 +1284,30 @@ describe('NavigationMenu Component', () => {
1210
1284
  expect(logoutItem.closest('[role="option"]')).toHaveAttribute('data-disabled', 'true');
1211
1285
  });
1212
1286
 
1287
+ it('handles space key activation for hierarchical leaf items', async () => {
1288
+ const user = userEvent.setup();
1289
+ const leafOnlyItems: NavigationItem[] = [
1290
+ { id: 'leaf', label: 'Leaf', href: '/leaf' },
1291
+ ];
1292
+
1293
+ renderWithProviders(
1294
+ <NavigationMenu
1295
+ items={leafOnlyItems}
1296
+ mode="hierarchical"
1297
+ onNavigate={mockNavigate}
1298
+ />
1299
+ );
1300
+
1301
+ const leafLink = screen.getByText('Leaf');
1302
+ leafLink.focus();
1303
+
1304
+ await user.keyboard(' ');
1305
+
1306
+ expect(mockNavigate).toHaveBeenCalledWith(
1307
+ expect.objectContaining({ id: 'leaf', href: '/leaf' })
1308
+ );
1309
+ });
1310
+
1213
1311
  it('forwards ref correctly', () => {
1214
1312
  const ref = React.createRef<HTMLDivElement>();
1215
1313
  renderWithProviders(
@@ -1305,6 +1403,35 @@ describe('NavigationMenu Component', () => {
1305
1403
  const listbox = screen.getByRole('listbox');
1306
1404
  expect(listbox.childNodes.length).toBe(0);
1307
1405
  });
1406
+
1407
+ it('surfaces items when permission map is empty but scope is available', async () => {
1408
+ const user = userEvent.setup();
1409
+
1410
+ mockUsePermissions.mockReturnValue({
1411
+ permissions: {},
1412
+ isLoading: false,
1413
+ error: null,
1414
+ hasPermission: vi.fn(() => false),
1415
+ hasAnyPermission: vi.fn(() => false),
1416
+ hasAllPermissions: vi.fn(() => false),
1417
+ refetch: vi.fn(),
1418
+ });
1419
+
1420
+ renderWithProviders(
1421
+ <NavigationMenu
1422
+ items={basicNavItems}
1423
+ onNavigate={mockNavigate}
1424
+ buttonText="Menu"
1425
+ />
1426
+ );
1427
+
1428
+ await user.click(screen.getByRole('combobox'));
1429
+
1430
+ await waitFor(() => {
1431
+ expect(screen.getByText('Home')).toBeInTheDocument();
1432
+ expect(screen.getByText('Dashboard')).toBeInTheDocument();
1433
+ }, { interval: 10 });
1434
+ });
1308
1435
  });
1309
1436
 
1310
1437
  // Audit logging tests
@@ -1369,4 +1496,3 @@ describe('NavigationMenu Component', () => {
1369
1496
  });
1370
1497
  });
1371
1498
  });
1372
- mockUseUnifiedAuthFn.mockImplementation(() => mockAuthContext);
@@ -184,7 +184,7 @@ export function OrganisationSelector({
184
184
  return (
185
185
  <div className={`space-y-2 ${className}`}>
186
186
  <Alert variant="destructive">
187
- <AlertCircle className="h-4 w-4" />
187
+ <AlertCircle className="size-4" />
188
188
  <AlertDescription>
189
189
  Failed to load organisations: {orgError.message}
190
190
  </AlertDescription>
@@ -197,7 +197,7 @@ export function OrganisationSelector({
197
197
  disabled={isLoading}
198
198
  className="w-full"
199
199
  >
200
- <RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
200
+ <RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
201
201
  Retry
202
202
  </Button>
203
203
  )}
@@ -211,7 +211,7 @@ export function OrganisationSelector({
211
211
  return (
212
212
  <div className={`space-y-2 ${className}`}>
213
213
  <Alert>
214
- <Building2 className="h-4 w-4" />
214
+ <Building2 className="size-4" />
215
215
  <AlertDescription>
216
216
  No organisations available. Please contact your administrator to be added to an organisation.
217
217
  </AlertDescription>
@@ -224,7 +224,7 @@ export function OrganisationSelector({
224
224
  disabled={isLoading}
225
225
  className="w-full"
226
226
  >
227
- <RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
227
+ <RefreshCw className={`size-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
228
228
  Check Again
229
229
  </Button>
230
230
  )}
@@ -237,7 +237,7 @@ export function OrganisationSelector({
237
237
  // Switch error display
238
238
  const switchErrorDisplay = switchError && (
239
239
  <Alert variant="destructive" className="mt-2">
240
- <AlertCircle className="h-4 w-4" />
240
+ <AlertCircle className="size-4" />
241
241
  <AlertDescription>{switchError}</AlertDescription>
242
242
  </Alert>
243
243
  );
@@ -255,7 +255,7 @@ export function OrganisationSelector({
255
255
  {isLoading ? (
256
256
  <LoadingSpinner size="sm" />
257
257
  ) : (
258
- <Building2 className="h-4 w-4 text-muted-foreground" />
258
+ <Building2 className="size-4 text-muted-foreground" />
259
259
  )}
260
260
  <SelectValue placeholder={placeholder} />
261
261
  </div>
@@ -274,7 +274,7 @@ export function OrganisationSelector({
274
274
  >
275
275
  <div className="flex items-center justify-between w-full">
276
276
  <div className="flex items-center gap-2">
277
- <Building2 className="h-4 w-4" />
277
+ <Building2 className="size-4" />
278
278
  <div className="flex flex-col">
279
279
  <span className="font-medium">{org.display_name}</span>
280
280
  {!compact && org.description && (
@@ -286,7 +286,7 @@ export function OrganisationSelector({
286
286
  </div>
287
287
  {showRole && (
288
288
  <div className="flex items-center gap-1 ml-4">
289
- <Shield className="h-3 w-3 text-muted-foreground" />
289
+ <Shield className="size-3 text-muted-foreground" />
290
290
  <span className="text-xs text-muted-foreground capitalize">
291
291
  {userRole?.replace('_', ' ') || 'No Role'}
292
292
  </span>
@@ -48,8 +48,10 @@ const mockOrganisationContext = vi.hoisted(() => {
48
48
  updated_at: '2023-01-01T00:00:00Z'
49
49
  }],
50
50
  isLoading: false,
51
+ isContextReady: true,
51
52
  error: null,
52
53
  hasValidOrganisationContext: true,
54
+ isContextReady: true,
53
55
  setSelectedOrganisation: vi.fn(),
54
56
  switchOrganisation: vi.fn().mockResolvedValue(undefined),
55
57
  getUserRole: vi.fn().mockReturnValue('member'),
@@ -94,6 +96,7 @@ const mockUseUnifiedAuthFn = vi.hoisted(() => vi.fn(() => {
94
96
  selectedOrganisation: mockOrganisationObj(),
95
97
  selectedEvent: null,
96
98
  isLoading: false,
99
+ isContextReady: true,
97
100
  error: null,
98
101
  isAuthenticated: true,
99
102
  selectedOrganisationId: 'test-org-id',
@@ -117,6 +120,7 @@ vi.mock('../../hooks/services/useOrganisationService', () => ({
117
120
  getOrganisations: () => [mockOrganisationObj()],
118
121
  getUserMemberships: () => mockOrganisationContext().userMemberships,
119
122
  isLoading: () => false,
123
+ isContextReady: () => true,
120
124
  getError: () => null,
121
125
  hasValidOrganisationContext: () => true,
122
126
  setSelectedOrganisation: vi.fn(),
@@ -75,8 +75,10 @@ const mockOrganisationContext = {
75
75
  updated_at: '2023-01-01T00:00:00Z'
76
76
  }],
77
77
  isLoading: false,
78
+ isContextReady: true,
78
79
  error: null,
79
80
  hasValidOrganisationContext: true,
81
+ isContextReady: true,
80
82
  setSelectedOrganisation: vi.fn(),
81
83
  switchOrganisation: vi.fn().mockResolvedValue(undefined),
82
84
  getUserRole: vi.fn().mockReturnValue('member'),
@@ -94,6 +96,7 @@ vi.mock('../../hooks/services/useOrganisationService', () => ({
94
96
  getOrganisations: () => [mockOrganisation],
95
97
  getUserMemberships: () => mockOrganisationContext.userMemberships,
96
98
  isLoading: () => false,
99
+ isContextReady: () => true,
97
100
  getError: () => null,
98
101
  hasValidOrganisationContext: () => true,
99
102
  setSelectedOrganisation: vi.fn(),
@@ -112,8 +112,10 @@ const mockOrganisationContext = {
112
112
  updated_at: '2023-01-01T00:00:00Z'
113
113
  }],
114
114
  isLoading: false,
115
+ isContextReady: true,
115
116
  error: null,
116
117
  hasValidOrganisationContext: true,
118
+ isContextReady: true,
117
119
  setSelectedOrganisation: vi.fn(),
118
120
  switchOrganisation: vi.fn().mockResolvedValue(undefined),
119
121
  getUserRole: vi.fn().mockReturnValue('member'),
@@ -131,6 +133,7 @@ vi.mock('../../hooks/services/useOrganisationService', () => ({
131
133
  getOrganisations: () => [mockOrganisation],
132
134
  getUserMemberships: () => mockOrganisationContext.userMemberships,
133
135
  isLoading: () => false,
136
+ isContextReady: () => true,
134
137
  getError: () => null,
135
138
  hasValidOrganisationContext: () => true,
136
139
  setSelectedOrganisation: vi.fn(),
@@ -866,13 +866,18 @@ describe('PaceAppLayout Component', () => {
866
866
  );
867
867
 
868
868
  await waitFor(() => {
869
- // useCan is called with userId, scope, permission, pageId, useCache
869
+ // useCan is called with userId, scope, permission, pageId, useCache, appName
870
870
  expect(mockUseCan).toHaveBeenCalledWith(
871
871
  'user-123',
872
- expect.objectContaining({ organisationId: 'org-123' }),
872
+ expect.objectContaining({
873
+ organisationId: 'org-123',
874
+ eventId: 'event-123',
875
+ appId: 'app-123',
876
+ }),
873
877
  'update:page.dashboard-page',
874
878
  'dashboard-page',
875
- true
879
+ true,
880
+ 'Test App'
876
881
  );
877
882
  }, { timeout: 2000 });
878
883
  }, { timeout: 3000 });
@@ -893,14 +898,19 @@ describe('PaceAppLayout Component', () => {
893
898
  );
894
899
 
895
900
  await waitFor(() => {
896
- // useCan is called with userId, scope, permission, pageId, useCache
901
+ // useCan is called with userId, scope, permission, pageId, useCache, appName
897
902
  // Uses defaultPermission "create" since /dashboard is not in routePermissions
898
903
  expect(mockUseCan).toHaveBeenCalledWith(
899
904
  'user-123',
900
- expect.objectContaining({ organisationId: 'org-123' }),
905
+ expect.objectContaining({
906
+ organisationId: 'org-123',
907
+ eventId: 'event-123',
908
+ appId: 'app-123',
909
+ }),
901
910
  'create:page.dashboard',
902
911
  'dashboard',
903
- true
912
+ true,
913
+ 'Test App'
904
914
  );
905
915
  }, { timeout: 2000 });
906
916
  }, { timeout: 3000 });