@jmruthers/pace-core 0.5.191 → 0.6.1

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 (380) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +7 -1
  3. package/cursor-rules/00-pace-core-compliance.mdc +372 -0
  4. package/cursor-rules/01-standards-compliance.mdc +275 -0
  5. package/cursor-rules/02-project-structure.mdc +200 -0
  6. package/cursor-rules/03-solid-principles.mdc +341 -0
  7. package/cursor-rules/04-testing-standards.mdc +315 -0
  8. package/cursor-rules/05-bug-reports-and-features.mdc +246 -0
  9. package/cursor-rules/06-code-quality.mdc +392 -0
  10. package/cursor-rules/07-tech-stack-compliance.mdc +309 -0
  11. package/cursor-rules/CHANGELOG.md +101 -0
  12. package/cursor-rules/README.md +191 -0
  13. package/dist/{AuthService-CbP_utw2.d.ts → AuthService-DjnJHDtC.d.ts} +1 -0
  14. package/dist/{DataTable-Be6dH_dR.d.ts → DataTable-CH1U5Tpy.d.ts} +1 -1
  15. package/dist/{DataTable-WKRZD47S.js → DataTable-DQ7RSOHE.js} +8 -7
  16. package/dist/{PublicPageProvider-ULXC_u6U.d.ts → PublicPageProvider-ce4xlHYA.d.ts} +37 -156
  17. package/dist/{UnifiedAuthProvider-BYA9qB-o.d.ts → UnifiedAuthProvider-185Ih4dj.d.ts} +2 -0
  18. package/dist/{UnifiedAuthProvider-FTSG5XH7.js → UnifiedAuthProvider-ATAP5UTR.js} +3 -3
  19. package/dist/{api-IHKALJZD.js → api-N774RPUA.js} +2 -2
  20. package/dist/{chunk-6C4YBBJM.js → chunk-3QRJFVBR.js} +1 -1
  21. package/dist/chunk-3QRJFVBR.js.map +1 -0
  22. package/dist/{chunk-OETXORNB.js → chunk-3XTALGJF.js} +211 -136
  23. package/dist/chunk-3XTALGJF.js.map +1 -0
  24. package/dist/{chunk-6TQDD426.js → chunk-4N5C5XZU.js} +47 -228
  25. package/dist/chunk-4N5C5XZU.js.map +1 -0
  26. package/dist/{chunk-LOMZXPSN.js → chunk-4ZC4GX36.js} +47 -74
  27. package/dist/chunk-4ZC4GX36.js.map +1 -0
  28. package/dist/{chunk-6LTQQAT6.js → chunk-BYFSK72L.js} +357 -158
  29. package/dist/chunk-BYFSK72L.js.map +1 -0
  30. package/dist/{chunk-XYXSXPUK.js → chunk-EXUD6RNJ.js} +50 -10
  31. package/dist/chunk-EXUD6RNJ.js.map +1 -0
  32. package/dist/{chunk-VKB2CO4Z.js → chunk-GLK6VM3F.js} +244 -249
  33. package/dist/chunk-GLK6VM3F.js.map +1 -0
  34. package/dist/{chunk-HW3OVDUF.js → chunk-J36DSWQK.js} +1 -1
  35. package/dist/{chunk-HW3OVDUF.js.map → chunk-J36DSWQK.js.map} +1 -1
  36. package/dist/{chunk-XNYQOL3Z.js → chunk-JBKQ3SAO.js} +9 -18
  37. package/dist/chunk-JBKQ3SAO.js.map +1 -0
  38. package/dist/{chunk-ROXMHMY2.js → chunk-KNC55RTG.js} +13 -3
  39. package/dist/{chunk-ROXMHMY2.js.map → chunk-KNC55RTG.js.map} +1 -1
  40. package/dist/{chunk-QWWZ5CAQ.js → chunk-LXQLPRQ2.js} +2 -2
  41. package/dist/{chunk-ULHIJK66.js → chunk-T33XF5ZC.js} +255 -140
  42. package/dist/chunk-T33XF5ZC.js.map +1 -0
  43. package/dist/{chunk-VRGWKHDB.js → chunk-XM25TVIE.js} +100 -33
  44. package/dist/chunk-XM25TVIE.js.map +1 -0
  45. package/dist/components.d.ts +4 -4
  46. package/dist/components.js +9 -9
  47. package/dist/hooks.d.ts +6 -6
  48. package/dist/hooks.js +20 -25
  49. package/dist/hooks.js.map +1 -1
  50. package/dist/index.d.ts +11 -11
  51. package/dist/index.js +18 -21
  52. package/dist/index.js.map +1 -1
  53. package/dist/providers.d.ts +3 -3
  54. package/dist/providers.js +2 -2
  55. package/dist/rbac/index.d.ts +2 -20
  56. package/dist/rbac/index.js +7 -9
  57. package/dist/{usePublicRouteParams-TZe0gy-4.d.ts → usePublicRouteParams-BJAlWfuJ.d.ts} +3 -3
  58. package/dist/{useToast-C8gR5ir4.d.ts → useToast-AyaT-x7p.d.ts} +2 -2
  59. package/dist/utils.d.ts +1 -1
  60. package/dist/utils.js +3 -3
  61. package/docs/api/classes/ColumnFactory.md +1 -1
  62. package/docs/api/classes/ErrorBoundary.md +1 -1
  63. package/docs/api/classes/InvalidScopeError.md +1 -1
  64. package/docs/api/classes/Logger.md +1 -1
  65. package/docs/api/classes/MissingUserContextError.md +1 -1
  66. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  67. package/docs/api/classes/PermissionDeniedError.md +2 -2
  68. package/docs/api/classes/RBACAuditManager.md +2 -2
  69. package/docs/api/classes/RBACCache.md +1 -1
  70. package/docs/api/classes/RBACEngine.md +2 -2
  71. package/docs/api/classes/RBACError.md +1 -1
  72. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  73. package/docs/api/classes/SecureSupabaseClient.md +10 -10
  74. package/docs/api/classes/StorageUtils.md +1 -1
  75. package/docs/api/enums/FileCategory.md +1 -1
  76. package/docs/api/enums/LogLevel.md +1 -1
  77. package/docs/api/enums/RBACErrorCode.md +1 -1
  78. package/docs/api/enums/RPCFunction.md +1 -1
  79. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  80. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  81. package/docs/api/interfaces/AggregateConfig.md +1 -1
  82. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  83. package/docs/api/interfaces/AvatarProps.md +1 -1
  84. package/docs/api/interfaces/BadgeProps.md +1 -1
  85. package/docs/api/interfaces/ButtonProps.md +1 -1
  86. package/docs/api/interfaces/CalendarProps.md +1 -1
  87. package/docs/api/interfaces/CardProps.md +1 -1
  88. package/docs/api/interfaces/ColorPalette.md +1 -1
  89. package/docs/api/interfaces/ColorShade.md +1 -1
  90. package/docs/api/interfaces/ComplianceResult.md +1 -1
  91. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  92. package/docs/api/interfaces/DataRecord.md +1 -1
  93. package/docs/api/interfaces/DataTableAction.md +1 -1
  94. package/docs/api/interfaces/DataTableColumn.md +1 -1
  95. package/docs/api/interfaces/DataTableProps.md +1 -1
  96. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  97. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  98. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  99. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  100. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  101. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  102. package/docs/api/interfaces/ExportColumn.md +1 -1
  103. package/docs/api/interfaces/ExportOptions.md +1 -1
  104. package/docs/api/interfaces/FileDisplayProps.md +24 -11
  105. package/docs/api/interfaces/FileMetadata.md +1 -1
  106. package/docs/api/interfaces/FileReference.md +1 -1
  107. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  108. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  109. package/docs/api/interfaces/FileUploadProps.md +1 -1
  110. package/docs/api/interfaces/FooterProps.md +1 -1
  111. package/docs/api/interfaces/FormFieldProps.md +1 -1
  112. package/docs/api/interfaces/FormProps.md +1 -1
  113. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  114. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  115. package/docs/api/interfaces/InputProps.md +1 -1
  116. package/docs/api/interfaces/LabelProps.md +1 -1
  117. package/docs/api/interfaces/LoggerConfig.md +1 -1
  118. package/docs/api/interfaces/LoginFormProps.md +1 -1
  119. package/docs/api/interfaces/NavigationAccessRecord.md +2 -2
  120. package/docs/api/interfaces/NavigationContextType.md +1 -1
  121. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  122. package/docs/api/interfaces/NavigationItem.md +1 -1
  123. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  124. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  125. package/docs/api/interfaces/Organisation.md +1 -1
  126. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  127. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  128. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  129. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  130. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  131. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  132. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  133. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  134. package/docs/api/interfaces/PagePermissionGuardProps.md +2 -2
  135. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  136. package/docs/api/interfaces/PaletteData.md +1 -1
  137. package/docs/api/interfaces/ParsedAddress.md +1 -1
  138. package/docs/api/interfaces/PermissionEnforcerProps.md +4 -4
  139. package/docs/api/interfaces/ProgressProps.md +1 -1
  140. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  141. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  142. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  143. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  144. package/docs/api/interfaces/QuickFix.md +1 -1
  145. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  146. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  147. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  148. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  149. package/docs/api/interfaces/RBACConfig.md +2 -2
  150. package/docs/api/interfaces/RBACContext.md +1 -1
  151. package/docs/api/interfaces/RBACLogger.md +1 -1
  152. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  153. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  154. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  155. package/docs/api/interfaces/RBACPermissionCheckResult.md +2 -2
  156. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  157. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  158. package/docs/api/interfaces/RBACResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleGrantParams.md +2 -2
  160. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  161. package/docs/api/interfaces/RBACRoleRevokeParams.md +2 -2
  162. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  163. package/docs/api/interfaces/RBACRoleValidateParams.md +2 -2
  164. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  165. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  166. package/docs/api/interfaces/RBACRolesListResult.md +2 -2
  167. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  168. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  169. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  170. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  171. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  172. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  173. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  174. package/docs/api/interfaces/RouteAccessRecord.md +2 -2
  175. package/docs/api/interfaces/RouteConfig.md +2 -2
  176. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  177. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  178. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  179. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  180. package/docs/api/interfaces/SetupIssue.md +1 -1
  181. package/docs/api/interfaces/StorageConfig.md +1 -1
  182. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  183. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  184. package/docs/api/interfaces/StorageListOptions.md +1 -1
  185. package/docs/api/interfaces/StorageListResult.md +1 -1
  186. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  187. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  188. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  189. package/docs/api/interfaces/StyleImport.md +1 -1
  190. package/docs/api/interfaces/SwitchProps.md +1 -1
  191. package/docs/api/interfaces/TabsContentProps.md +1 -1
  192. package/docs/api/interfaces/TabsListProps.md +1 -1
  193. package/docs/api/interfaces/TabsProps.md +1 -1
  194. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  195. package/docs/api/interfaces/TextareaProps.md +1 -1
  196. package/docs/api/interfaces/ToastActionElement.md +1 -1
  197. package/docs/api/interfaces/ToastProps.md +1 -1
  198. package/docs/api/interfaces/UnifiedAuthContextType.md +60 -38
  199. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  200. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  201. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  202. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  203. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  205. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  207. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  208. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  209. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  210. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  211. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  212. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  213. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  214. package/docs/api/interfaces/UserEventAccess.md +1 -1
  215. package/docs/api/interfaces/UserMenuProps.md +1 -1
  216. package/docs/api/interfaces/UserProfile.md +1 -1
  217. package/docs/api/modules.md +194 -209
  218. package/docs/getting-started/cursor-rules.md +262 -0
  219. package/docs/getting-started/installation-guide.md +6 -1
  220. package/docs/getting-started/quick-start.md +6 -1
  221. package/docs/migration/MIGRATION_GUIDE.md +4 -4
  222. package/docs/migration/REACT_19_MIGRATION.md +227 -0
  223. package/docs/migration/database-changes-december-2025.md +2 -1
  224. package/docs/rbac/event-based-apps.md +124 -6
  225. package/docs/standards/README.md +39 -0
  226. package/docs/troubleshooting/migration.md +4 -4
  227. package/examples/PublicPages/PublicEventPage.tsx +1 -1
  228. package/package.json +11 -6
  229. package/scripts/audit-consuming-app.cjs +961 -0
  230. package/scripts/check-pace-core-compliance.cjs +315 -61
  231. package/scripts/install-cursor-rules.cjs +236 -0
  232. package/src/__tests__/helpers/test-providers.tsx +1 -1
  233. package/src/__tests__/helpers/test-utils.tsx +1 -1
  234. package/src/__tests__/rls-policies.test.ts +3 -1
  235. package/src/components/Badge/Badge.tsx +2 -4
  236. package/src/components/Button/Button.tsx +5 -4
  237. package/src/components/Calendar/Calendar.tsx +1 -1
  238. package/src/components/DataTable/DataTable.test.tsx +57 -93
  239. package/src/components/DataTable/DataTable.tsx +2 -2
  240. package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +172 -45
  241. package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +121 -28
  242. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +9 -8
  243. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +20 -52
  244. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +170 -34
  245. package/src/components/DataTable/__tests__/keyboard.test.tsx +75 -12
  246. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +88 -16
  247. package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +12 -12
  248. package/src/components/DataTable/components/AccessDeniedPage.tsx +1 -1
  249. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
  250. package/src/components/DataTable/components/DataTableCore.tsx +4 -7
  251. package/src/components/DataTable/components/DataTableModals.tsx +1 -1
  252. package/src/components/DataTable/components/EditableRow.tsx +1 -1
  253. package/src/components/DataTable/components/UnifiedTableBody.tsx +86 -17
  254. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +23 -23
  255. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +11 -11
  256. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +36 -36
  257. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +27 -27
  258. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +39 -39
  259. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +33 -33
  260. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +29 -29
  261. package/src/components/DataTable/hooks/useColumnReordering.ts +2 -2
  262. package/src/components/DataTable/hooks/useDataTablePermissions.ts +75 -10
  263. package/src/components/DataTable/hooks/useKeyboardNavigation.ts +2 -2
  264. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -14
  265. package/src/components/Dialog/Dialog.tsx +6 -5
  266. package/src/components/ErrorBoundary/ErrorBoundary.tsx +1 -1
  267. package/src/components/EventSelector/EventSelector.tsx +1 -1
  268. package/src/components/FileDisplay/FileDisplay.test.tsx +4 -3
  269. package/src/components/FileDisplay/FileDisplay.tsx +16 -4
  270. package/src/components/Footer/Footer.tsx +1 -1
  271. package/src/components/Form/Form.test.tsx +36 -15
  272. package/src/components/Form/Form.tsx +30 -26
  273. package/src/components/Header/Header.tsx +1 -1
  274. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +40 -40
  275. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +1 -1
  276. package/src/components/Input/Input.tsx +28 -30
  277. package/src/components/Label/Label.tsx +1 -1
  278. package/src/components/LoadingSpinner/LoadingSpinner.tsx +1 -1
  279. package/src/components/LoginForm/LoginForm.test.tsx +42 -42
  280. package/src/components/LoginForm/LoginForm.tsx +8 -8
  281. package/src/components/NavigationMenu/NavigationMenu.test.tsx +6 -4
  282. package/src/components/NavigationMenu/NavigationMenu.tsx +2 -11
  283. package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -1
  284. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +1 -1
  285. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +75 -52
  286. package/src/components/PaceAppLayout/PaceAppLayout.tsx +98 -69
  287. package/src/components/PaceAppLayout/README.md +1 -1
  288. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -8
  289. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +33 -33
  290. package/src/components/PasswordChange/PasswordChangeForm.tsx +1 -1
  291. package/src/components/Progress/Progress.tsx +1 -1
  292. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +5 -9
  293. package/src/components/ProtectedRoute/ProtectedRoute.tsx +0 -1
  294. package/src/components/PublicLayout/PublicPageLayout.tsx +1 -1
  295. package/src/components/PublicLayout/PublicPageProvider.tsx +0 -1
  296. package/src/components/Select/Select.tsx +33 -22
  297. package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +1 -1
  298. package/src/components/Table/Table.tsx +1 -1
  299. package/src/components/Textarea/Textarea.tsx +27 -29
  300. package/src/components/Toast/Toast.tsx +1 -1
  301. package/src/components/Tooltip/Tooltip.tsx +1 -1
  302. package/src/components/UserMenu/UserMenu.tsx +1 -1
  303. package/src/hooks/__tests__/hooks.integration.test.tsx +80 -55
  304. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +14 -7
  305. package/src/hooks/__tests__/useStorage.unit.test.ts +36 -36
  306. package/src/hooks/public/usePublicEvent.ts +1 -1
  307. package/src/hooks/public/usePublicEventLogo.ts +1 -1
  308. package/src/hooks/public/usePublicRouteParams.ts +1 -1
  309. package/src/hooks/services/useAuthService.ts +21 -3
  310. package/src/hooks/services/useEventService.ts +21 -3
  311. package/src/hooks/services/useInactivityService.ts +21 -3
  312. package/src/hooks/services/useOrganisationService.ts +21 -3
  313. package/src/hooks/useDataTableState.ts +8 -18
  314. package/src/hooks/useFileDisplay.ts +10 -17
  315. package/src/hooks/useFocusManagement.ts +2 -2
  316. package/src/hooks/useFocusTrap.ts +4 -4
  317. package/src/hooks/useFormDialog.ts +8 -7
  318. package/src/hooks/useInactivityTracker.ts +1 -1
  319. package/src/hooks/usePermissionCache.ts +1 -1
  320. package/src/hooks/useSecureDataAccess.test.ts +16 -9
  321. package/src/hooks/useSecureDataAccess.ts +22 -6
  322. package/src/hooks/useToast.ts +2 -2
  323. package/src/providers/__tests__/OrganisationProvider.test.tsx +57 -13
  324. package/src/providers/__tests__/ProviderLifecycle.test.tsx +21 -6
  325. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +10 -10
  326. package/src/providers/services/EventServiceProvider.tsx +0 -8
  327. package/src/providers/services/UnifiedAuthProvider.tsx +196 -46
  328. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +13 -3
  329. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +34 -40
  330. package/src/rbac/__tests__/isSuperAdmin.real.test.ts +82 -0
  331. package/src/rbac/adapters.tsx +3 -22
  332. package/src/rbac/api.test.ts +2 -2
  333. package/src/rbac/api.ts +7 -1
  334. package/src/rbac/components/EnhancedNavigationMenu.tsx +3 -16
  335. package/src/rbac/components/NavigationGuard.tsx +2 -11
  336. package/src/rbac/components/NavigationProvider.tsx +1 -2
  337. package/src/rbac/components/PagePermissionGuard.tsx +1 -1
  338. package/src/rbac/components/PagePermissionProvider.tsx +1 -1
  339. package/src/rbac/components/PermissionEnforcer.tsx +46 -13
  340. package/src/rbac/components/RoleBasedRouter.tsx +1 -1
  341. package/src/rbac/components/SecureDataProvider.tsx +1 -2
  342. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +7 -43
  343. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +4 -11
  344. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +3 -3
  345. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +1 -1
  346. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +1 -1
  347. package/src/rbac/engine.ts +14 -2
  348. package/src/rbac/hooks/index.ts +0 -3
  349. package/src/rbac/hooks/usePermissions.ts +51 -11
  350. package/src/rbac/hooks/useRBAC.ts +3 -13
  351. package/src/rbac/hooks/useResolvedScope.test.ts +75 -54
  352. package/src/rbac/hooks/useResolvedScope.ts +58 -33
  353. package/src/rbac/hooks/useSecureSupabase.ts +4 -9
  354. package/src/rbac/secureClient.ts +43 -0
  355. package/src/services/EventService.ts +4 -57
  356. package/src/services/InactivityService.ts +127 -34
  357. package/src/services/OrganisationService.ts +68 -10
  358. package/src/utils/security/secureDataAccess.test.ts +31 -20
  359. package/src/utils/security/secureDataAccess.ts +4 -3
  360. package/dist/chunk-6C4YBBJM.js.map +0 -1
  361. package/dist/chunk-6LTQQAT6.js.map +0 -1
  362. package/dist/chunk-6TQDD426.js.map +0 -1
  363. package/dist/chunk-LOMZXPSN.js.map +0 -1
  364. package/dist/chunk-OETXORNB.js.map +0 -1
  365. package/dist/chunk-ULHIJK66.js.map +0 -1
  366. package/dist/chunk-VKB2CO4Z.js.map +0 -1
  367. package/dist/chunk-VRGWKHDB.js.map +0 -1
  368. package/dist/chunk-XNYQOL3Z.js.map +0 -1
  369. package/dist/chunk-XYXSXPUK.js.map +0 -1
  370. package/scripts/check-pace-core-compliance.js +0 -512
  371. package/src/rbac/hooks/useSuperAdminBypass.ts +0 -126
  372. package/src/utils/context/superAdminOverride.ts +0 -58
  373. /package/dist/{DataTable-WKRZD47S.js.map → DataTable-DQ7RSOHE.js.map} +0 -0
  374. /package/dist/{UnifiedAuthProvider-FTSG5XH7.js.map → UnifiedAuthProvider-ATAP5UTR.js.map} +0 -0
  375. /package/dist/{api-IHKALJZD.js.map → api-N774RPUA.js.map} +0 -0
  376. /package/dist/{chunk-QWWZ5CAQ.js.map → chunk-LXQLPRQ2.js.map} +0 -0
  377. /package/examples/{rbac → RBAC}/CompleteRBACExample.tsx +0 -0
  378. /package/examples/{rbac → RBAC}/EventBasedApp.tsx +0 -0
  379. /package/examples/{rbac → RBAC}/PermissionExample.tsx +0 -0
  380. /package/examples/{rbac → RBAC}/index.ts +0 -0
@@ -86,7 +86,7 @@
86
86
  * - Permission-based route protection
87
87
  *
88
88
  * @dependencies
89
- * - React 18+ - Component framework
89
+ * - React 19+ - Component framework
90
90
  * - React Router v6 - Routing
91
91
  * - UnifiedAuthProvider - Authentication
92
92
  * - usePermissionCache - Permission management
@@ -103,10 +103,10 @@ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
103
103
  import { useOrganisations } from '../../hooks/useOrganisations';
104
104
  import { useEvents } from '../../hooks/useEvents';
105
105
  import { useEventTheme } from '../../hooks/useEventTheme';
106
- import { useCan, useResolvedScope } from '../../rbac/hooks';
106
+ import { useCan, useResolvedScope, useRBAC } from '../../rbac/hooks';
107
107
  import { createScopeFromEvent } from '../../rbac/utils/eventContext';
108
108
  import { getCurrentAppName } from '../../utils/app/appNameResolver';
109
- import { isSuperAdmin } from '../../rbac/api';
109
+ import { isSuperAdmin as checkSuperAdminApi } from '../../rbac/api';
110
110
  import { logger } from '../../utils/core/logger';
111
111
  import type { Permission, Scope } from '../../rbac/types';
112
112
 
@@ -372,7 +372,7 @@ export function PaceAppLayout({
372
372
  onRouteAccessDenied,
373
373
  onRouteStrictModeViolation
374
374
  }: PaceAppLayoutProps) {
375
- const { user, signOut, updatePassword, supabase, appId: contextAppId } = useUnifiedAuth(); // Get appId from context (resolved on login)
375
+ const { user, signOut, updatePassword, supabase, appId: contextAppId, selectedOrganisationId } = useUnifiedAuth(); // Get appId from context (resolved on login)
376
376
  const {
377
377
  selectedOrganisation,
378
378
  isContextReady,
@@ -380,6 +380,47 @@ export function PaceAppLayout({
380
380
  ensureOrganisationContext,
381
381
  isLoading: organisationLoading
382
382
  } = useOrganisations();
383
+ // Use useRBAC to get super admin status - it's more reliable than async check
384
+ // Note: isSuperAdmin might be false initially while loading, but that's OK - we'll allow rendering
385
+ // if organisation loading completes or if we're a super admin
386
+ const { isSuperAdmin: isSuperAdminFromRBAC, isLoading: rbacLoading } = useRBAC();
387
+
388
+ // Also check super admin status directly as a fallback (for ADMIN/PORTAL apps)
389
+ // This allows super admins to proceed even if RBAC hasn't loaded yet
390
+ const [isSuperAdminDirect, setIsSuperAdminDirect] = useState<boolean>(false);
391
+ const [isCheckingSuperAdminDirect, setIsCheckingSuperAdminDirect] = useState<boolean>(false);
392
+
393
+ useEffect(() => {
394
+ const checkSuperAdminDirect = async () => {
395
+ if (!user?.id) {
396
+ setIsSuperAdminDirect(false);
397
+ setIsCheckingSuperAdminDirect(false);
398
+ return;
399
+ }
400
+
401
+ // Only skip if RBAC already confirmed super admin
402
+ if (isSuperAdminFromRBAC) {
403
+ setIsCheckingSuperAdminDirect(false);
404
+ return;
405
+ }
406
+
407
+ setIsCheckingSuperAdminDirect(true);
408
+ try {
409
+ const superAdminStatus = await checkSuperAdminApi(user.id);
410
+ setIsSuperAdminDirect(superAdminStatus);
411
+ } catch (error) {
412
+ logger.error('PaceAppLayout', 'Error checking super admin status directly', { userId: user?.id, error });
413
+ setIsSuperAdminDirect(false);
414
+ } finally {
415
+ setIsCheckingSuperAdminDirect(false);
416
+ }
417
+ };
418
+
419
+ checkSuperAdminDirect();
420
+ }, [user?.id, isSuperAdminFromRBAC]);
421
+
422
+ // Use direct check if RBAC hasn't loaded yet, otherwise use RBAC result
423
+ const isSuperAdmin = isSuperAdminFromRBAC || isSuperAdminDirect;
383
424
  const navigate = useNavigate();
384
425
  const location = useLocation();
385
426
 
@@ -408,28 +449,25 @@ export function PaceAppLayout({
408
449
 
409
450
  // Build scope from resolved values
410
451
  // Preserve appId from resolvedScope or fallback to resolvedAppId
452
+ // CRITICAL: Always create a new scope object from primitive values to ensure stable reference
453
+ // This prevents useCan from re-checking permissions when resolvedScope changes reference but values are the same
454
+ const scopeOrgId = resolvedScope?.organisationId || selectedOrganisation?.id || '';
455
+ const scopeEventId = resolvedScope?.eventId || selectedEvent?.event_id || undefined;
456
+ const scopeAppId = resolvedScope?.appId || resolvedAppId || undefined;
457
+
411
458
  const scope = useMemo<Scope>(() => {
412
- // Prefer resolvedScope if available (includes appId)
413
- if (resolvedScope?.organisationId) {
414
- return resolvedScope;
459
+ const newScope: Scope = {};
460
+ if (scopeOrgId) {
461
+ newScope.organisationId = scopeOrgId;
415
462
  }
416
-
417
- // Fallback: build scope from context values with resolvedAppId
418
- if (selectedOrganisation?.id) {
419
- return {
420
- organisationId: selectedOrganisation.id,
421
- eventId: selectedEvent?.event_id || undefined,
422
- appId: resolvedAppId || resolvedScope?.appId || undefined
423
- };
463
+ if (scopeEventId) {
464
+ newScope.eventId = scopeEventId;
424
465
  }
425
-
426
- // Last resort: return minimal scope
427
- return {
428
- organisationId: selectedOrganisation?.id || '',
429
- eventId: selectedEvent?.event_id || undefined,
430
- appId: resolvedAppId || resolvedScope?.appId || undefined
431
- };
432
- }, [resolvedScope, selectedOrganisation?.id, selectedEvent?.event_id, resolvedAppId]);
466
+ if (scopeAppId) {
467
+ newScope.appId = scopeAppId;
468
+ }
469
+ return newScope;
470
+ }, [scopeOrgId, scopeEventId, scopeAppId]);
433
471
 
434
472
  // Default navigation items if none provided
435
473
  const defaultNavItems: NavigationItem[] = useMemo(() => [
@@ -460,61 +498,45 @@ export function PaceAppLayout({
460
498
  }
461
499
  // Extract first path segment (base page name)
462
500
  const pathSegments = currentPath.slice(1).split('/').filter(Boolean);
463
- return pathSegments[0] || 'home';
501
+ // Only return 'home' if there's actually a path segment, otherwise return empty string
502
+ // This prevents checking permissions for a non-existent "home" page when the index route is used
503
+ return pathSegments[0] || '';
464
504
  }, [location.pathname, pageIdMapping]);
465
505
 
466
506
  // Build permission string in format: operation:page.pageId
467
507
  const currentPermission = useMemo<Permission>(() => {
468
- if (!enforcePermissions) {
469
- return 'read:page.home' as Permission;
508
+ // If enforcePermissions is false, don't check any permission (return empty string)
509
+ // If currentPageId is empty (index route with no path segments), don't check permissions
510
+ if (!enforcePermissions || !currentPageId) {
511
+ return '' as Permission;
470
512
  }
471
513
  const permissionString = `${currentRoutePermission}:page.${currentPageId}`;
472
514
  return permissionString as Permission;
473
515
  }, [enforcePermissions, currentRoutePermission, currentPageId]);
474
516
 
475
517
  // Check super admin status before permission enforcement
476
- const [isSuperAdminUser, setIsSuperAdminUser] = useState<boolean>(false);
477
- const [isCheckingSuperAdmin, setIsCheckingSuperAdmin] = useState<boolean>(false);
478
-
479
- useEffect(() => {
480
- const checkSuperAdminStatus = async () => {
481
- if (!user?.id) {
482
- setIsSuperAdminUser(false);
483
- setIsCheckingSuperAdmin(false);
484
- return;
485
- }
486
-
487
- setIsCheckingSuperAdmin(true);
488
- try {
489
- const superAdminStatus = await isSuperAdmin(user.id);
490
- setIsSuperAdminUser(superAdminStatus);
491
- } catch (error) {
492
- logger.error('PaceAppLayout', 'Error checking super admin status', { userId: user?.id, error });
493
- setIsSuperAdminUser(false);
494
- } finally {
495
- setIsCheckingSuperAdmin(false);
496
- }
497
- };
498
-
499
- checkSuperAdminStatus();
500
- }, [user?.id]);
518
+ // Removed duplicate super admin check - using useRBAC hook instead
519
+ // The useRBAC hook provides isSuperAdmin which is more reliable
501
520
 
502
521
  // Use useCan hook for permission checking (standardized approach)
503
522
  // Note: The database function already handles super admin bypass, but we check here
504
523
  // as an additional safety layer to prevent unnecessary permission checks
505
524
  // Pass appName to useCan so it can be passed to isPermitted for PORTAL/ADMIN special case
525
+ // Only check permissions if enforcePermissions is true and we have a valid permission string
526
+ const shouldCheckPermission = enforcePermissions && !!currentPermission && !!currentPageId;
506
527
  const { can: canFromHook, isLoading: isCheckingPermission, error: permissionError } = useCan(
507
528
  user?.id || '',
508
529
  scope,
509
- currentPermission,
510
- currentPageId,
530
+ shouldCheckPermission ? currentPermission : ('' as Permission),
531
+ shouldCheckPermission ? currentPageId : '',
511
532
  true, // useCache
512
533
  appName // Pass appName for PORTAL/ADMIN special case
513
534
  );
514
535
 
515
536
  // Permission enforcement state - super admin bypasses all checks
516
537
  // This ensures super admins never see permission errors even if useCan hasn't completed
517
- const can = isSuperAdminUser ? true : canFromHook;
538
+ // Use combined super admin check (RBAC + direct check)
539
+ const can = isSuperAdmin ? true : canFromHook;
518
540
  const hasPermission = enforcePermissions ? can : true;
519
541
 
520
542
  // Handle permission check results with audit logging and callbacks
@@ -524,19 +546,20 @@ export function PaceAppLayout({
524
546
  }
525
547
 
526
548
  // Only proceed when permission check is complete (not loading)
527
- // Wait for both super admin check and permission check to complete
528
- if (isCheckingSuperAdmin || isCheckingPermission) {
549
+ // Super admin status is checked via useRBAC hook (isSuperAdminFromRBAC)
550
+ // If RBAC is still loading, allow rendering to proceed (optimistic for super admins)
551
+ if (isCheckingPermission) {
529
552
  return;
530
553
  }
531
554
 
532
555
  // NEW: Phase 1 - Enhanced Security Features
533
556
  // Handle strict mode violations - skip for super admins
534
- if (strictMode && !isSuperAdminUser && !can) {
557
+ if (strictMode && !isSuperAdmin && !can) {
535
558
  logger.error('PaceAppLayout', 'STRICT MODE VIOLATION: User attempted to access protected page without permission', {
536
559
  pageName: currentPageId,
537
560
  operation: currentRoutePermission,
538
561
  userId: user?.id,
539
- isSuperAdmin: isSuperAdminUser,
562
+ isSuperAdmin: isSuperAdmin,
540
563
  timestamp: new Date().toISOString()
541
564
  });
542
565
 
@@ -546,10 +569,10 @@ export function PaceAppLayout({
546
569
  }
547
570
 
548
571
  // Handle page access denied callback - skip for super admins
549
- if (!isSuperAdminUser && !can && onPageAccessDenied) {
572
+ if (!isSuperAdmin && !can && onPageAccessDenied) {
550
573
  onPageAccessDenied(currentPageId, currentRoutePermission);
551
574
  }
552
- }, [enforcePermissions, can, isCheckingPermission, isCheckingSuperAdmin, isSuperAdminUser, currentPageId, currentRoutePermission, user?.id, strictMode, auditLog, onPageAccessDenied, onStrictModeViolation]);
575
+ }, [enforcePermissions, can, isCheckingPermission, isSuperAdmin, currentPageId, currentRoutePermission, user?.id, strictMode, auditLog, onPageAccessDenied, onStrictModeViolation]);
553
576
 
554
577
  // Filter navigation items based on permissions
555
578
  // Permission filtering is always enabled - users only see navigation items they have permission to access
@@ -610,8 +633,8 @@ export function PaceAppLayout({
610
633
  // For super admins, show all items (they bypass permission checks)
611
634
  // Gracefully handle RBAC not being initialized (e.g., in tests)
612
635
  try {
613
- const { isSuperAdmin } = await import('../../rbac/api');
614
- const isSuper = await isSuperAdmin(user.id);
636
+ const { isSuperAdmin: checkSuperAdminDynamic } = await import('../../rbac/api');
637
+ const isSuper = await checkSuperAdminDynamic(user.id);
615
638
 
616
639
  if (isSuper) {
617
640
  // Super admins see all navigation items
@@ -825,7 +848,13 @@ export function PaceAppLayout({
825
848
  // This is critical - we must wait for organisation context before allowing any data access
826
849
  // BUT: Allow rendering to proceed if loading is complete, even if user has no organisations (valid state for profile pages)
827
850
  // Only block if we're actively loading - once loading completes (success or error), allow rendering
828
- if (user?.id && organisationLoading) {
851
+ // EXCEPTION: Super admins can proceed even during organisation loading (they can access all orgs)
852
+ // Use combined super admin check (RBAC + direct check) to allow super admins to proceed immediately
853
+ // IMPORTANT: If we're still checking super admin status, allow rendering to proceed (optimistic approach)
854
+ // This prevents blocking super admins while their status is being determined
855
+ // Also allow rendering if we already have a selectedOrganisationId (even if organisationLoading is still true)
856
+ // This prevents blank pages when organisation context is available but loading state hasn't cleared yet
857
+ if (user?.id && organisationLoading && !isSuperAdmin && !isCheckingSuperAdminDirect && !rbacLoading && !selectedOrganisationId) {
829
858
  return (
830
859
  <div className="flex items-center justify-center min-h-screen">
831
860
  <div className="text-center">
@@ -841,10 +870,10 @@ export function PaceAppLayout({
841
870
  // These pages work with user context only and don't require organisation context
842
871
  // The app can check hasValidOrganisationContext() to determine if org context is available for org-specific features
843
872
 
844
- // Show loading state while checking permissions or super admin status
845
- // Keep loading active until BOTH checks complete to prevent exposing protected content
846
- // This ensures we don't render the main layout when permission check failed but super admin check is pending
847
- if (enforcePermissions && (isCheckingSuperAdmin || isCheckingPermission)) {
873
+ // Show loading state while checking permissions
874
+ // Keep loading active until permission check completes to prevent exposing protected content
875
+ // Super admin status is checked via useRBAC hook (isSuperAdminFromRBAC)
876
+ if (enforcePermissions && isCheckingPermission) {
848
877
  return (
849
878
  <div className="flex items-center justify-center min-h-screen">
850
879
  <div className="text-center">
@@ -857,7 +886,7 @@ export function PaceAppLayout({
857
886
 
858
887
  // Show permission error (only after BOTH checks are complete)
859
888
  // Super admins bypass all permission checks, so don't show errors for them
860
- if (enforcePermissions && permissionError && !isSuperAdminUser) {
889
+ if (enforcePermissions && permissionError && !isSuperAdmin) {
861
890
  return (
862
891
  <div className="flex items-center justify-center min-h-screen">
863
892
  <div className="text-center">
@@ -871,7 +900,7 @@ export function PaceAppLayout({
871
900
 
872
901
  // Show permission fallback if user lacks permission
873
902
  // Only show this if super admin check is complete and user is not a super admin
874
- if (enforcePermissions && hasPermission === false && !isCheckingSuperAdmin && !isSuperAdminUser) {
903
+ if (enforcePermissions && hasPermission === false && !isCheckingSuperAdminDirect && !isSuperAdmin) {
875
904
  // NEW: Phase 1 - Use page permission fallback if available
876
905
  if (enforcePagePermissions && pagePermissionFallback) {
877
906
  return <>{pagePermissionFallback}</>;
@@ -309,7 +309,7 @@ function App() {
309
309
 
310
310
  ## Dependencies
311
311
 
312
- - React 18+
312
+ - React 19+
313
313
  - React Router v6
314
314
  - UnifiedAuthProvider
315
315
  - Header component
@@ -111,7 +111,7 @@
111
111
  * - Automatic redirect prevention loops
112
112
  *
113
113
  * @dependencies
114
- * - React 18+ - Hooks and effects
114
+ * - React 19+ - Hooks and effects
115
115
  * - React Router v6 - Navigation
116
116
  * - UnifiedAuthProvider - Authentication
117
117
  * - LoginForm component
@@ -208,7 +208,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
208
208
  }
209
209
  } catch (error) {
210
210
  // Service may not be available yet or events not loaded - that's okay
211
- logger.debug('PaceLoginPage', 'Could not restore persisted event (service may not be ready):', error);
212
211
  }
213
212
  };
214
213
 
@@ -266,7 +265,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
266
265
  .eq('app_id', appData.id);
267
266
 
268
267
  if (pagesError || !pagesData || pagesData.length === 0) {
269
- logger.debug('PaceLoginPage', 'No pages configured for app:', appName);
270
268
  setAccessError(`You do not have permission to access ${appName}. This application is currently unavailable. Please contact your administrator if you believe you should have access.`);
271
269
  setIsCheckingAccess(false);
272
270
  return;
@@ -285,7 +283,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
285
283
  const organisationId = orgRow?.organisation_id;
286
284
 
287
285
  if (!organisationId) {
288
- logger.debug('PaceLoginPage', 'User has no organisation access');
289
286
  setAccessError(`You do not have permission to access ${appName}. You are not assigned to any organisation. Please contact your administrator.`);
290
287
  setIsCheckingAccess(false);
291
288
  return;
@@ -305,8 +302,6 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
305
302
  p_page_id: page.page_name // Page name to resolve to UUID
306
303
  });
307
304
 
308
- logger.debug('PaceLoginPage', 'Permission check for page:', { pageName: page.page_name, hasPermission, error: permError });
309
-
310
305
  if (!permError && hasPermission === true) {
311
306
  hasAnyAccess = true;
312
307
  break;
@@ -314,14 +309,12 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
314
309
  }
315
310
 
316
311
  if (hasAnyAccess) {
317
- logger.debug('PaceLoginPage', 'User has access to app');
318
312
  setIsCheckingAccess(false);
319
313
  navigate(onSuccessRedirectPath, { replace: true });
320
314
  return;
321
315
  }
322
316
 
323
317
  // No access - deny
324
- logger.debug('PaceLoginPage', 'Access denied - no permissions');
325
318
  setAccessError(`You do not have permission to access ${appName}. This application is restricted to authorized users only. Please contact your administrator if you believe you should have access.`);
326
319
  setIsCheckingAccess(false);
327
320
  } catch (error) {
@@ -55,13 +55,13 @@ vi.mock('../Label', () => ({
55
55
 
56
56
  describe('PasswordChangeForm', () => {
57
57
  const mockOnSubmit = vi.fn();
58
- const defaultProps: PasswordChangeFormProps = {
58
+ const baseProps: PasswordChangeFormProps = {
59
59
  onSubmit: mockOnSubmit
60
60
  };
61
61
 
62
62
  describe('Rendering', () => {
63
63
  it('renders password change form with all elements', () => {
64
- render(<PasswordChangeForm {...defaultProps} />);
64
+ render(<PasswordChangeForm {...baseProps} />);
65
65
 
66
66
  expect(screen.getByLabelText('New Password')).toBeInTheDocument();
67
67
  expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument();
@@ -70,14 +70,14 @@ describe('PasswordChangeForm', () => {
70
70
 
71
71
  it('renders with custom className', () => {
72
72
  const customClass = 'custom-password-change-form';
73
- const { container } = render(<PasswordChangeForm {...defaultProps} className={customClass} />);
73
+ const { container } = render(<PasswordChangeForm {...baseProps} className={customClass} />);
74
74
 
75
75
  const form = container.querySelector('form');
76
76
  expect(form).toHaveClass(customClass);
77
77
  });
78
78
 
79
79
  it('has proper form structure and accessibility', () => {
80
- render(<PasswordChangeForm {...defaultProps} />);
80
+ render(<PasswordChangeForm {...baseProps} />);
81
81
 
82
82
  const newPasswordInput = screen.getByLabelText('New Password');
83
83
  expect(newPasswordInput).toHaveAttribute('type', 'password');
@@ -97,7 +97,7 @@ describe('PasswordChangeForm', () => {
97
97
  describe('Form Interaction', () => {
98
98
  it('updates new password input value when user types', async () => {
99
99
  const user = userEvent.setup();
100
- render(<PasswordChangeForm {...defaultProps} />);
100
+ render(<PasswordChangeForm {...baseProps} />);
101
101
 
102
102
  const newPasswordInput = screen.getByLabelText('New Password');
103
103
  await user.type(newPasswordInput, 'newpassword123');
@@ -107,7 +107,7 @@ describe('PasswordChangeForm', () => {
107
107
 
108
108
  it('updates confirm password input value when user types', async () => {
109
109
  const user = userEvent.setup();
110
- render(<PasswordChangeForm {...defaultProps} />);
110
+ render(<PasswordChangeForm {...baseProps} />);
111
111
 
112
112
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
113
113
  await user.type(confirmPasswordInput, 'newpassword123');
@@ -117,7 +117,7 @@ describe('PasswordChangeForm', () => {
117
117
 
118
118
  it('enables submit button when both passwords are provided', async () => {
119
119
  const user = userEvent.setup();
120
- render(<PasswordChangeForm {...defaultProps} />);
120
+ render(<PasswordChangeForm {...baseProps} />);
121
121
 
122
122
  const newPasswordInput = screen.getByLabelText('New Password');
123
123
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -133,7 +133,7 @@ describe('PasswordChangeForm', () => {
133
133
 
134
134
  it('disables submit button when new password is empty', async () => {
135
135
  const user = userEvent.setup();
136
- render(<PasswordChangeForm {...defaultProps} />);
136
+ render(<PasswordChangeForm {...baseProps} />);
137
137
 
138
138
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
139
139
  const submitButton = screen.getByRole('button', { name: 'Change Password' });
@@ -145,7 +145,7 @@ describe('PasswordChangeForm', () => {
145
145
 
146
146
  it('disables submit button when confirm password is empty', async () => {
147
147
  const user = userEvent.setup();
148
- render(<PasswordChangeForm {...defaultProps} />);
148
+ render(<PasswordChangeForm {...baseProps} />);
149
149
 
150
150
  const newPasswordInput = screen.getByLabelText('New Password');
151
151
  const submitButton = screen.getByRole('button', { name: 'Change Password' });
@@ -159,7 +159,7 @@ describe('PasswordChangeForm', () => {
159
159
  describe('Form Validation', () => {
160
160
  it('shows error when password is too short', async () => {
161
161
  const user = userEvent.setup();
162
- render(<PasswordChangeForm {...defaultProps} />);
162
+ render(<PasswordChangeForm {...baseProps} />);
163
163
 
164
164
  const newPasswordInput = screen.getByLabelText('New Password');
165
165
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -175,7 +175,7 @@ describe('PasswordChangeForm', () => {
175
175
 
176
176
  it('shows error when passwords do not match', async () => {
177
177
  const user = userEvent.setup();
178
- render(<PasswordChangeForm {...defaultProps} />);
178
+ render(<PasswordChangeForm {...baseProps} />);
179
179
 
180
180
  const newPasswordInput = screen.getByLabelText('New Password');
181
181
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -191,7 +191,7 @@ describe('PasswordChangeForm', () => {
191
191
 
192
192
  it('validates password length before checking match', async () => {
193
193
  const user = userEvent.setup();
194
- render(<PasswordChangeForm {...defaultProps} />);
194
+ render(<PasswordChangeForm {...baseProps} />);
195
195
 
196
196
  const newPasswordInput = screen.getByLabelText('New Password');
197
197
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -207,7 +207,7 @@ describe('PasswordChangeForm', () => {
207
207
 
208
208
  it('clears error when form is resubmitted with valid data', async () => {
209
209
  const user = userEvent.setup();
210
- render(<PasswordChangeForm {...defaultProps} />);
210
+ render(<PasswordChangeForm {...baseProps} />);
211
211
 
212
212
  const newPasswordInput = screen.getByLabelText('New Password');
213
213
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -236,7 +236,7 @@ describe('PasswordChangeForm', () => {
236
236
  const user = userEvent.setup();
237
237
  mockOnSubmit.mockResolvedValue({});
238
238
 
239
- render(<PasswordChangeForm {...defaultProps} />);
239
+ render(<PasswordChangeForm {...baseProps} />);
240
240
 
241
241
  const newPasswordInput = screen.getByLabelText('New Password');
242
242
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -254,7 +254,7 @@ describe('PasswordChangeForm', () => {
254
254
 
255
255
  it('prevents form submission when validation fails', async () => {
256
256
  const user = userEvent.setup();
257
- render(<PasswordChangeForm {...defaultProps} />);
257
+ render(<PasswordChangeForm {...baseProps} />);
258
258
 
259
259
  const newPasswordInput = screen.getByLabelText('New Password');
260
260
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -273,7 +273,7 @@ describe('PasswordChangeForm', () => {
273
273
  const user = userEvent.setup();
274
274
  mockOnSubmit.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
275
275
 
276
- render(<PasswordChangeForm {...defaultProps} />);
276
+ render(<PasswordChangeForm {...baseProps} />);
277
277
 
278
278
  const newPasswordInput = screen.getByLabelText('New Password');
279
279
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -291,7 +291,7 @@ describe('PasswordChangeForm', () => {
291
291
  const user = userEvent.setup();
292
292
  mockOnSubmit.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
293
293
 
294
- render(<PasswordChangeForm {...defaultProps} />);
294
+ render(<PasswordChangeForm {...baseProps} />);
295
295
 
296
296
  const newPasswordInput = screen.getByLabelText('New Password');
297
297
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -309,7 +309,7 @@ describe('PasswordChangeForm', () => {
309
309
  const user = userEvent.setup();
310
310
  mockOnSubmit.mockResolvedValue({});
311
311
 
312
- render(<PasswordChangeForm {...defaultProps} />);
312
+ render(<PasswordChangeForm {...baseProps} />);
313
313
 
314
314
  const newPasswordInput = screen.getByLabelText('New Password');
315
315
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -329,7 +329,7 @@ describe('PasswordChangeForm', () => {
329
329
  const user = userEvent.setup();
330
330
  mockOnSubmit.mockResolvedValue({ error: { message: 'Test error' } });
331
331
 
332
- render(<PasswordChangeForm {...defaultProps} />);
332
+ render(<PasswordChangeForm {...baseProps} />);
333
333
 
334
334
  const newPasswordInput = screen.getByLabelText('New Password');
335
335
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -352,7 +352,7 @@ describe('PasswordChangeForm', () => {
352
352
  const errorMessage = 'Password change failed';
353
353
  mockOnSubmit.mockResolvedValue({ error: { message: errorMessage } });
354
354
 
355
- render(<PasswordChangeForm {...defaultProps} />);
355
+ render(<PasswordChangeForm {...baseProps} />);
356
356
 
357
357
  const newPasswordInput = screen.getByLabelText('New Password');
358
358
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -373,7 +373,7 @@ describe('PasswordChangeForm', () => {
373
373
  const errorMessage = 'Network error';
374
374
  mockOnSubmit.mockRejectedValue(new Error(errorMessage));
375
375
 
376
- render(<PasswordChangeForm {...defaultProps} />);
376
+ render(<PasswordChangeForm {...baseProps} />);
377
377
 
378
378
  const newPasswordInput = screen.getByLabelText('New Password');
379
379
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -393,7 +393,7 @@ describe('PasswordChangeForm', () => {
393
393
  const user = userEvent.setup();
394
394
  mockOnSubmit.mockRejectedValue('String error');
395
395
 
396
- render(<PasswordChangeForm {...defaultProps} />);
396
+ render(<PasswordChangeForm {...baseProps} />);
397
397
 
398
398
  const newPasswordInput = screen.getByLabelText('New Password');
399
399
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -412,7 +412,7 @@ describe('PasswordChangeForm', () => {
412
412
  const user = userEvent.setup();
413
413
  mockOnSubmit.mockResolvedValue({ error: { code: 'INVALID_PASSWORD' } });
414
414
 
415
- render(<PasswordChangeForm {...defaultProps} />);
415
+ render(<PasswordChangeForm {...baseProps} />);
416
416
 
417
417
  const newPasswordInput = screen.getByLabelText('New Password');
418
418
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -433,7 +433,7 @@ describe('PasswordChangeForm', () => {
433
433
  .mockResolvedValueOnce({ error: { message: 'First error' } })
434
434
  .mockResolvedValueOnce({});
435
435
 
436
- render(<PasswordChangeForm {...defaultProps} />);
436
+ render(<PasswordChangeForm {...baseProps} />);
437
437
 
438
438
  const newPasswordInput = screen.getByLabelText('New Password');
439
439
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -457,7 +457,7 @@ describe('PasswordChangeForm', () => {
457
457
 
458
458
  describe('Accessibility', () => {
459
459
  it('has proper form labels and associations', () => {
460
- render(<PasswordChangeForm {...defaultProps} />);
460
+ render(<PasswordChangeForm {...baseProps} />);
461
461
 
462
462
  const newPasswordInput = screen.getByLabelText('New Password');
463
463
  expect(newPasswordInput).toHaveAttribute('id', 'new-password');
@@ -472,7 +472,7 @@ describe('PasswordChangeForm', () => {
472
472
  const user = userEvent.setup();
473
473
  mockOnSubmit.mockResolvedValue({ error: { message: 'Test error' } });
474
474
 
475
- render(<PasswordChangeForm {...defaultProps} />);
475
+ render(<PasswordChangeForm {...baseProps} />);
476
476
 
477
477
  const newPasswordInput = screen.getByLabelText('New Password');
478
478
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -492,7 +492,7 @@ describe('PasswordChangeForm', () => {
492
492
  const user = userEvent.setup();
493
493
  mockOnSubmit.mockResolvedValue({});
494
494
 
495
- render(<PasswordChangeForm {...defaultProps} />);
495
+ render(<PasswordChangeForm {...baseProps} />);
496
496
 
497
497
  const newPasswordInput = screen.getByLabelText('New Password');
498
498
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -510,7 +510,7 @@ describe('PasswordChangeForm', () => {
510
510
 
511
511
  describe('Edge Cases', () => {
512
512
  it('handles empty password values', () => {
513
- render(<PasswordChangeForm {...defaultProps} />);
513
+ render(<PasswordChangeForm {...baseProps} />);
514
514
 
515
515
  const submitButton = screen.getByRole('button', { name: 'Change Password' });
516
516
 
@@ -519,7 +519,7 @@ describe('PasswordChangeForm', () => {
519
519
 
520
520
  it('handles whitespace-only passwords', async () => {
521
521
  const user = userEvent.setup();
522
- render(<PasswordChangeForm {...defaultProps} />);
522
+ render(<PasswordChangeForm {...baseProps} />);
523
523
 
524
524
  const newPasswordInput = screen.getByLabelText('New Password');
525
525
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -538,7 +538,7 @@ describe('PasswordChangeForm', () => {
538
538
  const longPassword = 'a'.repeat(1000);
539
539
  mockOnSubmit.mockResolvedValue({});
540
540
 
541
- render(<PasswordChangeForm {...defaultProps} />);
541
+ render(<PasswordChangeForm {...baseProps} />);
542
542
 
543
543
  const newPasswordInput = screen.getByLabelText('New Password');
544
544
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -559,7 +559,7 @@ describe('PasswordChangeForm', () => {
559
559
  const specialPassword = 'P@ssw0rd!@#$%^&*()_+-=';
560
560
  mockOnSubmit.mockResolvedValue({});
561
561
 
562
- render(<PasswordChangeForm {...defaultProps} />);
562
+ render(<PasswordChangeForm {...baseProps} />);
563
563
 
564
564
  const newPasswordInput = screen.getByLabelText('New Password');
565
565
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -580,7 +580,7 @@ describe('PasswordChangeForm', () => {
580
580
  const unicodePassword = 'pássw0rd_测试_пароль';
581
581
  mockOnSubmit.mockResolvedValue({});
582
582
 
583
- render(<PasswordChangeForm {...defaultProps} />);
583
+ render(<PasswordChangeForm {...baseProps} />);
584
584
 
585
585
  const newPasswordInput = screen.getByLabelText('New Password');
586
586
  const confirmPasswordInput = screen.getByLabelText('Confirm Password');
@@ -600,7 +600,7 @@ describe('PasswordChangeForm', () => {
600
600
  describe('Performance', () => {
601
601
  it('handles rapid input changes efficiently', async () => {
602
602
  const user = userEvent.setup();
603
- render(<PasswordChangeForm {...defaultProps} />);
603
+ render(<PasswordChangeForm {...baseProps} />);
604
604
 
605
605
  const newPasswordInput = screen.getByLabelText('New Password');
606
606
 
@@ -90,7 +90,7 @@
90
90
  * - Proper error handling
91
91
  *
92
92
  * @dependencies
93
- * - React 18+ - Hooks and state
93
+ * - React 19+ - Hooks and state
94
94
  * - Button component
95
95
  * - Input component
96
96
  * - Label component