@jmruthers/pace-core 0.6.4 → 0.6.6

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 (387) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +5 -403
  3. package/core-usage-manifest.json +93 -0
  4. package/cursor-rules/00-pace-core-compliance.mdc +128 -26
  5. package/cursor-rules/01-standards-compliance.mdc +49 -8
  6. package/cursor-rules/02-project-structure.mdc +6 -0
  7. package/cursor-rules/03-solid-principles.mdc +2 -0
  8. package/cursor-rules/04-testing-standards.mdc +2 -0
  9. package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
  10. package/cursor-rules/06-code-quality.mdc +2 -0
  11. package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
  12. package/cursor-rules/08-markup-quality.mdc +52 -27
  13. package/cursor-rules/09-rbac-compliance.mdc +462 -0
  14. package/cursor-rules/10-error-handling-patterns.mdc +179 -0
  15. package/cursor-rules/11-performance-optimization.mdc +169 -0
  16. package/cursor-rules/12-ci-cd-integration.mdc +150 -0
  17. package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
  18. package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
  19. package/dist/DataTable-LRJL4IRV.js +15 -0
  20. package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
  21. package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
  22. package/dist/api-Y4MQWOFW.js +4 -0
  23. package/dist/audit-MYQXYZFU.js +3 -0
  24. package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
  25. package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
  26. package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
  27. package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
  28. package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
  29. package/dist/chunk-4T7OBVTU.js +62 -0
  30. package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
  31. package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
  32. package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
  33. package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
  34. package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
  35. package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
  36. package/dist/chunk-C7NSAPTL.js +1 -0
  37. package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
  38. package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
  39. package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
  40. package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
  41. package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
  42. package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
  43. package/dist/chunk-MBADTM7L.js +64 -0
  44. package/dist/chunk-OHIK3MIO.js +994 -0
  45. package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
  46. package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
  47. package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
  48. package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
  49. package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
  50. package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
  51. package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
  52. package/dist/components.d.ts +6 -6
  53. package/dist/components.js +57 -267
  54. package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
  55. package/dist/eslint-rules/index.cjs +22 -0
  56. package/dist/eslint-rules/rules/compliance.cjs +348 -0
  57. package/dist/eslint-rules/rules/components.cjs +113 -0
  58. package/dist/eslint-rules/rules/imports.cjs +102 -0
  59. package/dist/eslint-rules/rules/rbac.cjs +790 -0
  60. package/dist/eslint-rules/utils/helpers.cjs +42 -0
  61. package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
  62. package/dist/hooks.d.ts +5 -5
  63. package/dist/hooks.js +62 -270
  64. package/dist/icons/index.d.ts +1 -0
  65. package/dist/icons/index.js +1 -0
  66. package/dist/index.d.ts +36 -26
  67. package/dist/index.js +87 -690
  68. package/dist/providers.d.ts +2 -2
  69. package/dist/providers.js +8 -35
  70. package/dist/rbac/eslint-rules.d.ts +46 -44
  71. package/dist/rbac/eslint-rules.js +7 -4
  72. package/dist/rbac/index.d.ts +124 -594
  73. package/dist/rbac/index.js +14 -207
  74. package/dist/styles/index.js +2 -12
  75. package/dist/theming/runtime.js +3 -19
  76. package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
  77. package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
  78. package/dist/types-t9H8qKRw.d.ts +55 -0
  79. package/dist/types.d.ts +1 -1
  80. package/dist/types.js +7 -94
  81. package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
  82. package/dist/utils.d.ts +24 -117
  83. package/dist/utils.js +54 -392
  84. package/docs/README.md +16 -6
  85. package/docs/api/README.md +4 -402
  86. package/docs/api/modules.md +454 -930
  87. package/docs/api-reference/components.md +3 -1
  88. package/docs/api-reference/deprecated.md +31 -6
  89. package/docs/api-reference/rpc-functions.md +78 -3
  90. package/docs/best-practices/accessibility.md +6 -3
  91. package/docs/getting-started/cursor-rules.md +3 -23
  92. package/docs/getting-started/dependencies.md +650 -0
  93. package/docs/getting-started/installation-guide.md +20 -7
  94. package/docs/getting-started/quick-start.md +23 -12
  95. package/docs/implementation-guides/permission-enforcement.md +4 -0
  96. package/docs/rbac/MIGRATION_GUIDE.md +819 -0
  97. package/docs/rbac/RBAC_CONTRACT.md +724 -0
  98. package/docs/rbac/README.md +12 -3
  99. package/docs/rbac/edge-functions-guide.md +376 -0
  100. package/docs/rbac/secure-client-protection.md +0 -34
  101. package/docs/standards/00-pace-core-compliance.md +967 -0
  102. package/docs/standards/01-standards-compliance.md +188 -0
  103. package/docs/standards/02-project-structure.md +985 -0
  104. package/docs/standards/03-solid-principles.md +39 -0
  105. package/docs/standards/04-testing-standards.md +36 -0
  106. package/docs/standards/05-bug-reports-and-features.md +27 -0
  107. package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
  108. package/docs/standards/07-tech-stack-compliance.md +30 -0
  109. package/docs/standards/08-markup-quality.md +345 -0
  110. package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
  111. package/docs/standards/10-error-handling-patterns.md +401 -0
  112. package/docs/standards/11-performance-optimization.md +348 -0
  113. package/docs/standards/12-ci-cd-integration.md +370 -0
  114. package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
  115. package/docs/standards/README.md +62 -33
  116. package/docs/troubleshooting/organisation-context-setup.md +42 -19
  117. package/eslint-config-pace-core.cjs +20 -4
  118. package/package.json +31 -21
  119. package/scripts/audit/audit-compliance.cjs +1295 -0
  120. package/scripts/audit/audit-components.cjs +260 -0
  121. package/scripts/audit/audit-dependencies.cjs +395 -0
  122. package/scripts/audit/audit-rbac.cjs +954 -0
  123. package/scripts/audit/audit-standards.cjs +1268 -0
  124. package/scripts/audit/index.cjs +1898 -194
  125. package/scripts/install-cursor-rules.cjs +259 -8
  126. package/scripts/validate-master.js +1 -1
  127. package/src/__tests__/fixtures/supabase.ts +1 -1
  128. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
  129. package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
  130. package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
  131. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
  132. package/src/__tests__/helpers/component-test-utils.tsx +1 -1
  133. package/src/__tests__/helpers/supabaseMock.ts +2 -2
  134. package/src/__tests__/public-recipe-view.test.ts +38 -9
  135. package/src/components/Button/Button.tsx +5 -1
  136. package/src/components/ContextSelector/ContextSelector.tsx +42 -39
  137. package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
  138. package/src/components/DataTable/components/DataTableBody.tsx +55 -31
  139. package/src/components/DataTable/components/DataTableCore.tsx +186 -13
  140. package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
  141. package/src/components/DataTable/components/EditFields.tsx +23 -3
  142. package/src/components/DataTable/components/EditableRow.tsx +7 -2
  143. package/src/components/DataTable/components/ImportModal.tsx +4 -6
  144. package/src/components/DataTable/components/RowComponent.tsx +12 -0
  145. package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
  146. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
  147. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
  148. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
  149. package/src/components/DataTable/core/DataTableContext.tsx +1 -1
  150. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
  151. package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
  152. package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
  153. package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
  154. package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
  155. package/src/components/DataTable/types.ts +5 -0
  156. package/src/components/DateTimeField/DateTimeField.tsx +20 -20
  157. package/src/components/DateTimeField/README.md +5 -2
  158. package/src/components/Dialog/Dialog.test.tsx +361 -318
  159. package/src/components/Dialog/Dialog.tsx +1154 -323
  160. package/src/components/Dialog/index.ts +3 -3
  161. package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
  162. package/src/components/FileDisplay/FileDisplay.tsx +28 -22
  163. package/src/components/Form/Form.test.tsx +9 -10
  164. package/src/components/Form/Form.tsx +369 -9
  165. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
  166. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
  167. package/src/components/LoginForm/LoginForm.tsx +2 -2
  168. package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
  169. package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
  170. package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
  171. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
  172. package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
  173. package/src/components/PaceAppLayout/README.md +10 -9
  174. package/src/components/PaceAppLayout/test-setup.tsx +40 -31
  175. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
  176. package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
  177. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
  178. package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
  179. package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
  180. package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
  181. package/src/components/Select/Select.tsx +23 -21
  182. package/src/components/Select/types.ts +1 -1
  183. package/src/components/UserMenu/UserMenu.test.tsx +38 -6
  184. package/src/components/UserMenu/UserMenu.tsx +39 -34
  185. package/src/components/index.ts +3 -4
  186. package/src/eslint-rules/index.cjs +22 -0
  187. package/src/eslint-rules/rules/compliance.cjs +348 -0
  188. package/src/eslint-rules/rules/components.cjs +113 -0
  189. package/src/eslint-rules/rules/imports.cjs +102 -0
  190. package/src/eslint-rules/rules/rbac.cjs +790 -0
  191. package/src/eslint-rules/utils/helpers.cjs +42 -0
  192. package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
  193. package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
  194. package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
  195. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
  196. package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
  197. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
  198. package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
  199. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
  200. package/src/hooks/public/usePublicEvent.ts +62 -190
  201. package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
  202. package/src/hooks/public/usePublicEventLogo.ts +19 -9
  203. package/src/hooks/useAppConfig.ts +26 -24
  204. package/src/hooks/useEventTheme.test.ts +211 -233
  205. package/src/hooks/useEventTheme.ts +19 -28
  206. package/src/hooks/useEvents.ts +11 -7
  207. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  208. package/src/hooks/useOrganisationPermissions.ts +9 -11
  209. package/src/hooks/useOrganisations.ts +13 -7
  210. package/src/hooks/useQueryCache.ts +0 -1
  211. package/src/hooks/useSessionDraft.ts +380 -0
  212. package/src/hooks/useSessionRestoration.ts +3 -1
  213. package/src/icons/index.ts +27 -0
  214. package/src/index.ts +16 -1
  215. package/src/providers/OrganisationProvider.tsx +23 -14
  216. package/src/providers/services/EventServiceProvider.tsx +1 -24
  217. package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
  218. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
  219. package/src/rbac/README.md +20 -20
  220. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
  221. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
  222. package/src/rbac/adapters.tsx +7 -295
  223. package/src/rbac/api.test.ts +44 -56
  224. package/src/rbac/api.ts +10 -17
  225. package/src/rbac/cache-invalidation.ts +0 -1
  226. package/src/rbac/compliance/index.ts +10 -0
  227. package/src/rbac/compliance/pattern-detector.ts +553 -0
  228. package/src/rbac/compliance/runtime-compliance.ts +22 -0
  229. package/src/rbac/components/AccessDenied.tsx +150 -0
  230. package/src/rbac/components/NavigationGuard.tsx +12 -20
  231. package/src/rbac/components/PagePermissionGuard.tsx +4 -24
  232. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
  233. package/src/rbac/components/index.ts +3 -41
  234. package/src/rbac/eslint-rules.js +1 -1
  235. package/src/rbac/hooks/index.ts +0 -3
  236. package/src/rbac/hooks/permissions/index.ts +0 -3
  237. package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
  238. package/src/rbac/hooks/usePermissions.ts +0 -3
  239. package/src/rbac/hooks/useRBAC.test.ts +21 -3
  240. package/src/rbac/hooks/useRBAC.ts +4 -3
  241. package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
  242. package/src/rbac/hooks/useResolvedScope.ts +58 -140
  243. package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
  244. package/src/rbac/hooks/useResourcePermissions.ts +182 -63
  245. package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
  246. package/src/rbac/hooks/useRoleManagement.ts +147 -19
  247. package/src/rbac/hooks/useSecureSupabase.ts +4 -8
  248. package/src/rbac/index.ts +7 -9
  249. package/src/rbac/permissions.ts +17 -17
  250. package/src/rbac/utils/contextValidator.ts +45 -7
  251. package/src/services/AuthService.ts +132 -23
  252. package/src/services/EventService.ts +4 -97
  253. package/src/services/InactivityService.ts +155 -58
  254. package/src/services/OrganisationService.ts +7 -44
  255. package/src/services/__tests__/OrganisationService.test.ts +26 -8
  256. package/src/services/base/BaseService.ts +0 -3
  257. package/src/styles/core.css +4 -0
  258. package/src/types/database.generated.ts +4733 -3809
  259. package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
  260. package/src/utils/context/organisationContext.test.ts +13 -28
  261. package/src/utils/context/organisationContext.ts +21 -52
  262. package/src/utils/dynamic/dynamicUtils.ts +1 -1
  263. package/src/utils/file-reference/index.ts +39 -15
  264. package/src/utils/formatting/formatDateTime.test.ts +3 -2
  265. package/src/utils/formatting/formatTime.test.ts +3 -2
  266. package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
  267. package/src/utils/index.ts +4 -1
  268. package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
  269. package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
  270. package/src/utils/persistence/keyDerivation.ts +304 -0
  271. package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
  272. package/src/utils/security/secureStorage.ts +5 -5
  273. package/src/utils/storage/helpers.ts +3 -3
  274. package/src/utils/supabase/createBaseClient.ts +147 -0
  275. package/src/utils/timezone/timezone.test.ts +1 -2
  276. package/src/utils/timezone/timezone.ts +1 -1
  277. package/src/utils/validation/csrf.ts +4 -4
  278. package/cursor-rules/CHANGELOG.md +0 -119
  279. package/cursor-rules/README.md +0 -192
  280. package/dist/DataTable-E7YQZD7D.js +0 -175
  281. package/dist/DataTable-E7YQZD7D.js.map +0 -1
  282. package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
  283. package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
  284. package/dist/api-6LVZTHDS.js +0 -52
  285. package/dist/api-6LVZTHDS.js.map +0 -1
  286. package/dist/audit-V53FV5AG.js +0 -17
  287. package/dist/audit-V53FV5AG.js.map +0 -1
  288. package/dist/chunk-36LVWXB2.js +0 -227
  289. package/dist/chunk-36LVWXB2.js.map +0 -1
  290. package/dist/chunk-3LPHPB62.js.map +0 -1
  291. package/dist/chunk-5DRSZLL2.js.map +0 -1
  292. package/dist/chunk-5EC5MEWX.js.map +0 -1
  293. package/dist/chunk-63FOKYGO.js.map +0 -1
  294. package/dist/chunk-6SOIHG6Z.js.map +0 -1
  295. package/dist/chunk-7JPAB3T5.js.map +0 -1
  296. package/dist/chunk-ATKZM7RX.js +0 -2053
  297. package/dist/chunk-ATKZM7RX.js.map +0 -1
  298. package/dist/chunk-AVMLPIM7.js.map +0 -1
  299. package/dist/chunk-DGUM43GV.js.map +0 -1
  300. package/dist/chunk-E66EQZE6.js.map +0 -1
  301. package/dist/chunk-FFQEQTNW.js.map +0 -1
  302. package/dist/chunk-FMUCXFII.js.map +0 -1
  303. package/dist/chunk-G37KK66H.js.map +0 -1
  304. package/dist/chunk-I6DAQMWX.js.map +0 -1
  305. package/dist/chunk-J36DSWQK.js.map +0 -1
  306. package/dist/chunk-KQCRWDSA.js +0 -1
  307. package/dist/chunk-KQCRWDSA.js.map +0 -1
  308. package/dist/chunk-L4OXEN46.js.map +0 -1
  309. package/dist/chunk-LMC26NLJ.js +0 -84
  310. package/dist/chunk-LMC26NLJ.js.map +0 -1
  311. package/dist/chunk-M43Y4SSO.js.map +0 -1
  312. package/dist/chunk-M7MPQISP.js.map +0 -1
  313. package/dist/chunk-NN6WWZ5U.js.map +0 -1
  314. package/dist/chunk-OEWDTMG7.js.map +0 -1
  315. package/dist/chunk-PWLANIRT.js.map +0 -1
  316. package/dist/chunk-QXHPKYJV.js.map +0 -1
  317. package/dist/chunk-VBXEHIUJ.js.map +0 -1
  318. package/dist/chunk-YKRAFF5K.js.map +0 -1
  319. package/dist/chunk-ZSAAAMVR.js.map +0 -1
  320. package/dist/components.js.map +0 -1
  321. package/dist/contextValidator-OOPCLPZW.js +0 -9
  322. package/dist/contextValidator-OOPCLPZW.js.map +0 -1
  323. package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
  324. package/dist/hooks.js.map +0 -1
  325. package/dist/index.js.map +0 -1
  326. package/dist/providers.js.map +0 -1
  327. package/dist/rbac/eslint-rules.js.map +0 -1
  328. package/dist/rbac/index.js.map +0 -1
  329. package/dist/styles/index.js.map +0 -1
  330. package/dist/theming/runtime.js.map +0 -1
  331. package/dist/types.js.map +0 -1
  332. package/dist/utils.js.map +0 -1
  333. package/docs/standards/01-architecture-standard.md +0 -44
  334. package/docs/standards/02-api-and-rpc-standard.md +0 -39
  335. package/docs/standards/03-component-standard.md +0 -32
  336. package/docs/standards/05-security-standard.md +0 -44
  337. package/docs/standards/06-testing-and-docs-standard.md +0 -29
  338. package/docs/standards/pace-core-compliance.md +0 -432
  339. package/scripts/audit/core/checks/accessibility.cjs +0 -197
  340. package/scripts/audit/core/checks/api-usage.cjs +0 -191
  341. package/scripts/audit/core/checks/bundle.cjs +0 -142
  342. package/scripts/audit/core/checks/compliance.cjs +0 -2706
  343. package/scripts/audit/core/checks/config.cjs +0 -54
  344. package/scripts/audit/core/checks/coverage.cjs +0 -84
  345. package/scripts/audit/core/checks/dependencies.cjs +0 -994
  346. package/scripts/audit/core/checks/documentation.cjs +0 -268
  347. package/scripts/audit/core/checks/environment.cjs +0 -116
  348. package/scripts/audit/core/checks/error-handling.cjs +0 -340
  349. package/scripts/audit/core/checks/forms.cjs +0 -172
  350. package/scripts/audit/core/checks/heuristics.cjs +0 -68
  351. package/scripts/audit/core/checks/hooks.cjs +0 -334
  352. package/scripts/audit/core/checks/imports.cjs +0 -244
  353. package/scripts/audit/core/checks/performance.cjs +0 -325
  354. package/scripts/audit/core/checks/routes.cjs +0 -117
  355. package/scripts/audit/core/checks/state.cjs +0 -130
  356. package/scripts/audit/core/checks/structure.cjs +0 -65
  357. package/scripts/audit/core/checks/style.cjs +0 -584
  358. package/scripts/audit/core/checks/testing.cjs +0 -122
  359. package/scripts/audit/core/checks/typescript.cjs +0 -61
  360. package/scripts/audit/core/scanner.cjs +0 -199
  361. package/scripts/audit/core/utils.cjs +0 -137
  362. package/scripts/audit/reporters/console.cjs +0 -151
  363. package/scripts/audit/reporters/json.cjs +0 -54
  364. package/scripts/audit/reporters/markdown.cjs +0 -124
  365. package/scripts/audit-consuming-app.cjs +0 -86
  366. package/src/eslint-rules/pace-core-compliance.cjs +0 -510
  367. package/src/eslint-rules/pace-core-compliance.js +0 -638
  368. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
  369. package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
  370. package/src/rbac/components/NavigationProvider.test.tsx +0 -481
  371. package/src/rbac/components/NavigationProvider.tsx +0 -345
  372. package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
  373. package/src/rbac/components/PagePermissionProvider.tsx +0 -279
  374. package/src/rbac/components/PermissionEnforcer.tsx +0 -312
  375. package/src/rbac/components/RoleBasedRouter.tsx +0 -440
  376. package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
  377. package/src/rbac/components/SecureDataProvider.tsx +0 -339
  378. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
  379. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
  380. package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
  381. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
  382. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
  383. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
  384. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
  385. package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
  386. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
  387. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
@@ -3,10 +3,10 @@ export type {
3
3
  DialogProps,
4
4
  DialogTriggerProps,
5
5
  DialogContentProps,
6
- DialogOverlayProps,
6
+ DialogPortalProps,
7
+ DialogCloseProps,
7
8
  DialogHeaderProps,
8
9
  DialogFooterProps,
9
10
  DialogBodyProps,
10
- DialogTitleProps,
11
- DialogDescriptionProps
11
+ DialogSize
12
12
  } from './Dialog';
@@ -84,6 +84,17 @@ const baseProps = {
84
84
  describe('[component] FileDisplay', () => {
85
85
  beforeEach(async () => {
86
86
  vi.clearAllMocks();
87
+
88
+ // Mock showModal for dialog elements (needed for test environments)
89
+ HTMLDialogElement.prototype.showModal = vi.fn(function(this: HTMLDialogElement) {
90
+ this.setAttribute('open', '');
91
+ this.dispatchEvent(new Event('show', { bubbles: true }));
92
+ });
93
+ HTMLDialogElement.prototype.close = vi.fn(function(this: HTMLDialogElement) {
94
+ this.removeAttribute('open');
95
+ this.dispatchEvent(new Event('close', { bubbles: true }));
96
+ });
97
+
87
98
  // Set up default mock for useFileDisplay
88
99
  const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
89
100
  useFileDisplay.mockReturnValue({
@@ -195,9 +206,25 @@ describe('[component] FileDisplay', () => {
195
206
  const deleteBtn = screen.getByRole('button', { name: /Delete file/i });
196
207
  await userEvent.click(deleteBtn);
197
208
 
209
+ // Wait for dialog to open and be accessible
210
+ // In test environments (jsdom), dialog.open may not be set even when dialog is rendered
211
+ await waitFor(async () => {
212
+ try {
213
+ const dialog = screen.getByRole('dialog');
214
+ expect(dialog).toBeInTheDocument();
215
+ } catch (e) {
216
+ // Fallback for test environments - just check that dialog exists in DOM
217
+ const dialog = document.querySelector('dialog[role="dialog"]');
218
+ if (!dialog) {
219
+ throw new Error('Dialog not found in DOM');
220
+ }
221
+ }
222
+ }, { timeout: 3000 });
223
+
198
224
  // Dialog should open
199
225
  expect(await screen.findByText('Confirm Delete')).toBeInTheDocument();
200
- const confirmDeleteBtn = screen.getByRole('button', { name: /^Delete$/i });
226
+ // Button has aria-label="Delete file", so accessible name is "Delete file"
227
+ const confirmDeleteBtn = await screen.findByRole('button', { name: 'Delete file' }, { timeout: 2000 });
201
228
  await userEvent.click(confirmDeleteBtn);
202
229
 
203
230
  confirmSpy.mockRestore();
@@ -376,9 +403,25 @@ describe('[component] FileDisplay', () => {
376
403
  const deleteBtn = screen.getByRole('button', { name: /Delete file/i });
377
404
  await userEvent.click(deleteBtn);
378
405
 
406
+ // Wait for dialog to open and be accessible
407
+ // In test environments (jsdom), dialog.open may not be set even when dialog is rendered
408
+ await waitFor(async () => {
409
+ try {
410
+ const dialog = screen.getByRole('dialog');
411
+ expect(dialog).toBeInTheDocument();
412
+ } catch (e) {
413
+ // Fallback for test environments - just check that dialog exists in DOM
414
+ const dialog = document.querySelector('dialog[role="dialog"]');
415
+ if (!dialog) {
416
+ throw new Error('Dialog not found in DOM');
417
+ }
418
+ }
419
+ }, { timeout: 3000 });
420
+
379
421
  // Dialog should open
380
422
  expect(await screen.findByText('Confirm Delete')).toBeInTheDocument();
381
- const confirmDeleteBtn = screen.getByRole('button', { name: /^Delete$/i });
423
+ // Button has aria-label="Delete file", so accessible name is "Delete file"
424
+ const confirmDeleteBtn = await screen.findByRole('button', { name: 'Delete file' }, { timeout: 2000 });
382
425
  await userEvent.click(confirmDeleteBtn);
383
426
  });
384
427
 
@@ -6,7 +6,7 @@ import { useFileDisplay } from '../../hooks/useFileDisplay';
6
6
  import { useFileUrl } from '../../hooks/useFileUrl';
7
7
  import { PublicPageContext, useIsPublicPage } from '../PublicLayout/PublicPageProvider';
8
8
  import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
9
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooter } from '../Dialog/Dialog';
9
+ import { Dialog, DialogContent, DialogHeader, DialogBody, DialogFooter } from '../Dialog/Dialog';
10
10
  import { Button } from '../Button/Button';
11
11
  import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner';
12
12
  import { logger } from '../../utils/core/logger';
@@ -448,9 +448,9 @@ const FileDisplayContent = React.memo(function FileDisplayContent({
448
448
  ×
449
449
  </Button>
450
450
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
451
- <DialogContent size="sm">
451
+ <DialogContent size="sm" title="Confirm Delete">
452
452
  <DialogHeader>
453
- <DialogTitle>Confirm Delete</DialogTitle>
453
+ <h2>Confirm Delete</h2>
454
454
  </DialogHeader>
455
455
  <DialogBody>
456
456
  <p>Are you sure you want to delete this file? This action cannot be undone.</p>
@@ -520,9 +520,9 @@ const FileDisplayContent = React.memo(function FileDisplayContent({
520
520
  ×
521
521
  </Button>
522
522
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
523
- <DialogContent size="sm">
523
+ <DialogContent size="sm" title="Confirm Delete">
524
524
  <DialogHeader>
525
- <DialogTitle>Confirm Delete</DialogTitle>
525
+ <h2>Confirm Delete</h2>
526
526
  </DialogHeader>
527
527
  <DialogBody>
528
528
  <p>Are you sure you want to delete this file? This action cannot be undone.</p>
@@ -654,9 +654,32 @@ function FileDisplayPublic({
654
654
  enableChildren,
655
655
  showMetadata
656
656
  }: FileDisplayProps) {
657
+ // Call all hooks unconditionally at the top level
658
+ // Hooks must be called in the same order on every render
657
659
  const publicPageContext = useContext(PublicPageContext);
658
660
  const supabase = publicPageContext?.supabase ?? null;
659
661
 
662
+ // Call hook unconditionally - if supabase is null, the hook will handle it
663
+ // Use a dummy supabase client if null to satisfy type requirements
664
+ // The hook should handle null gracefully, but TypeScript requires a valid type
665
+ const {
666
+ fileUrl,
667
+ fileReference,
668
+ fileReferences,
669
+ fileUrls,
670
+ fileCount,
671
+ isLoading,
672
+ error,
673
+ refetch
674
+ } = usePublicFileDisplay(
675
+ table_name,
676
+ record_id,
677
+ organisation_id,
678
+ category,
679
+ { supabase: supabase as any } // Type assertion needed due to type mismatch between contexts
680
+ );
681
+
682
+ // Early return after all hooks have been called
660
683
  if (!supabase) {
661
684
  // If fallback is enabled, show fallback UI instead of error
662
685
  if (showFallback) {
@@ -700,23 +723,6 @@ function FileDisplayPublic({
700
723
  );
701
724
  }
702
725
 
703
- const {
704
- fileUrl,
705
- fileReference,
706
- fileReferences,
707
- fileUrls,
708
- fileCount,
709
- isLoading,
710
- error,
711
- refetch
712
- } = usePublicFileDisplay(
713
- table_name,
714
- record_id,
715
- organisation_id,
716
- category,
717
- { supabase }
718
- );
719
-
720
726
  // Log errors for debugging public file display issues
721
727
  if (error) {
722
728
  logger.error('FileDisplayPublic', 'Error fetching file', {
@@ -170,7 +170,7 @@ describe('Form Component', () => {
170
170
  await user.type(screen.getByLabelText('Name'), 'John Doe');
171
171
  await user.click(screen.getByRole('button', { name: 'Submit' }));
172
172
 
173
- expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' }, expect.any(Object));
173
+ expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' });
174
174
  });
175
175
 
176
176
  it('calls onError when form has validation errors', async () => {
@@ -194,8 +194,7 @@ describe('Form Component', () => {
194
194
  name: expect.objectContaining({
195
195
  message: 'Required'
196
196
  })
197
- }),
198
- expect.any(Object)
197
+ })
199
198
  );
200
199
  expect(onSubmit).not.toHaveBeenCalled();
201
200
  });
@@ -213,7 +212,7 @@ describe('Form Component', () => {
213
212
  await user.type(screen.getByLabelText('Name'), 'John Doe');
214
213
  await user.click(screen.getByRole('button', { name: 'Submit' }));
215
214
 
216
- expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' }, expect.any(Object));
215
+ expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' });
217
216
  });
218
217
  });
219
218
 
@@ -387,10 +386,9 @@ describe('Form Component', () => {
387
386
  email: 'john@example.com'
388
387
  },
389
388
  preferences: {
390
- theme: 'dark',
391
- notifications: undefined
389
+ theme: 'dark'
392
390
  }
393
- }, expect.any(Object));
391
+ });
394
392
  });
395
393
  });
396
394
  });
@@ -469,8 +467,9 @@ describe('FormField Component', () => {
469
467
  </Form>
470
468
  );
471
469
 
472
- const fieldContainer = screen.getByLabelText('Test Field').closest('div');
473
- expect(fieldContainer).toHaveClass('custom-field');
470
+ // FormField applies className to the label element, not a div container
471
+ const label = screen.getByLabelText('Test Field').closest('label');
472
+ expect(label).toHaveClass('custom-field');
474
473
  });
475
474
 
476
475
  it('renders with test ID', () => {
@@ -709,7 +708,7 @@ describe('Integration', () => {
709
708
  name: 'John Doe',
710
709
  email: 'john@example.com',
711
710
  age: 25
712
- }, expect.any(Object));
711
+ });
713
712
  });
714
713
  expect(onError).not.toHaveBeenCalled();
715
714
  });
@@ -71,12 +71,16 @@
71
71
  * - React 19+ - Hooks and context
72
72
  */
73
73
 
74
- import React from 'react';
75
- import { useForm, FormProvider, UseFormReturn, FieldValues, DefaultValues, SubmitHandler, SubmitErrorHandler, useFormContext, Controller, FieldPath, ControllerRenderProps, ControllerFieldState, UseFormStateReturn } from 'react-hook-form';
74
+ import React, { useEffect, useMemo, useRef } from 'react';
75
+ import { useForm, FormProvider, UseFormReturn, FieldValues, DefaultValues, SubmitHandler, SubmitErrorHandler, useFormContext, Controller, FieldPath, ControllerRenderProps, ControllerFieldState, UseFormStateReturn, useWatch } from 'react-hook-form';
76
76
  import { zodResolver } from '@hookform/resolvers/zod';
77
77
  import { z } from 'zod';
78
+ import { useLocation } from 'react-router-dom';
79
+ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
78
80
  import { cn } from '../../utils/core/cn';
79
- import { Label } from '../Label';
81
+ import { useSessionDraft } from '../../hooks/useSessionDraft';
82
+ import { deriveFormKey } from '../../utils/persistence/keyDerivation';
83
+ import { filterSensitiveFields, isSensitiveField } from '../../utils/persistence/sensitiveFieldDetection';
80
84
 
81
85
  /**
82
86
  * Props for the Form component
@@ -149,14 +153,370 @@ export function Form<TFieldValues extends FieldValues = FieldValues>({
149
153
  children,
150
154
  className,
151
155
  }: FormProps<TFieldValues>) {
156
+ // Call all hooks unconditionally at the top level
157
+ // Hooks must be called in the same order on every render
158
+ // If providers are missing, these hooks will throw - errors should be handled by error boundaries
159
+ const location = useLocation();
160
+ const auth = useUnifiedAuth();
161
+ const userId = auth.user?.id || null;
162
+
163
+ // Extract field names from schema or defaultValues for key derivation and sensitive field filtering
164
+ const fieldNames = useMemo(() => {
165
+ if (schema && 'shape' in schema && typeof schema.shape === 'object') {
166
+ return Object.keys(schema.shape as Record<string, unknown>);
167
+ }
168
+ if (defaultValues) {
169
+ return Object.keys(defaultValues);
170
+ }
171
+ return [];
172
+ }, [schema, defaultValues]);
173
+
174
+ // Derive persistence key (scoped by user ID)
175
+ const persistenceKey = useMemo(() => {
176
+ return deriveFormKey(
177
+ {
178
+ fieldNames,
179
+ },
180
+ null, // Parent context (Dialog) - not available yet, can be enhanced later
181
+ location,
182
+ userId
183
+ );
184
+ }, [fieldNames, location, userId]);
185
+
186
+ // Get field types for sensitive field detection
187
+ // Extract from schema if available, otherwise infer from defaultValues
188
+ const fieldTypes = useMemo(() => {
189
+ const types: Record<string, string> = {};
190
+
191
+ // Try to extract types from schema
192
+ if (schema && 'shape' in schema && typeof schema.shape === 'object') {
193
+ const shape = schema.shape as Record<string, any>;
194
+ for (const [key, value] of Object.entries(shape)) {
195
+ // Zod schema type detection (simplified)
196
+ if (value && typeof value === 'object' && '_def' in value) {
197
+ const def = (value as any)._def;
198
+ if (def.typeName === 'ZodString') {
199
+ types[key] = 'text';
200
+ } else if (def.typeName === 'ZodNumber') {
201
+ types[key] = 'number';
202
+ } else if (def.typeName === 'ZodBoolean') {
203
+ types[key] = 'checkbox';
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ return types;
210
+ }, [schema]);
211
+
212
+ // Use session draft for persistence
213
+ const { state: persistedValues, setState: setPersistedValues, clearDraft } = useSessionDraft<Partial<TFieldValues>>(
214
+ persistenceKey || 'form:no-key',
215
+ {} as Partial<TFieldValues>,
216
+ {
217
+ enabled: Boolean(persistenceKey),
218
+ debounceMs: 300,
219
+ }
220
+ );
221
+
222
+ // Merge persisted values with defaultValues (persisted takes precedence)
223
+ const mergedDefaultValues = useMemo(() => {
224
+ if (!persistenceKey || !persistedValues || Object.keys(persistedValues).length === 0) {
225
+ return defaultValues;
226
+ }
227
+
228
+ // Filter sensitive fields from persisted values
229
+ const filteredPersisted = filterSensitiveFields(
230
+ persistedValues,
231
+ fieldNames,
232
+ fieldTypes
233
+ );
234
+
235
+ return {
236
+ ...defaultValues,
237
+ ...filteredPersisted,
238
+ } as DefaultValues<TFieldValues>;
239
+ }, [defaultValues, persistedValues, persistenceKey, fieldNames, fieldTypes]);
240
+
152
241
  const methods = useForm<TFieldValues>({
153
242
  resolver: schema ? zodResolver(schema) : undefined,
154
- defaultValues,
243
+ defaultValues: mergedDefaultValues,
155
244
  mode,
156
245
  shouldUnregister: false,
157
246
  });
158
247
 
159
- const handleSubmit = methods.handleSubmit(onSubmit, onError);
248
+ // Track if we've already restored persisted values to prevent infinite loops
249
+ const hasRestoredRef = useRef(false);
250
+ const isRestoringRef = useRef(false);
251
+ const restoreTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
252
+ const lastRestoredRef = useRef<string | null>(null);
253
+
254
+ // Restore persisted values after form initialization
255
+ // CRITICAL: Must run when persistedValues changes (e.g., when dialog auto-opens)
256
+ useEffect(() => {
257
+ // Clear any pending restore
258
+ if (restoreTimeoutRef.current) {
259
+ clearTimeout(restoreTimeoutRef.current);
260
+ }
261
+
262
+ // Skip if already restored or currently restoring
263
+ if (isRestoringRef.current) {
264
+ return;
265
+ }
266
+
267
+ if (!persistenceKey || !persistedValues || Object.keys(persistedValues).length === 0) {
268
+ // Mark as restored even if no persisted values (prevents re-running)
269
+ if (!hasRestoredRef.current) {
270
+ hasRestoredRef.current = true;
271
+ }
272
+ return;
273
+ }
274
+
275
+ // Skip if we've already restored these exact values
276
+ const persistedValuesStr = JSON.stringify(persistedValues);
277
+ if (lastRestoredRef.current === persistedValuesStr && hasRestoredRef.current) {
278
+ return;
279
+ }
280
+
281
+ isRestoringRef.current = true;
282
+
283
+ // Defer restoration to prevent blocking and allow form to initialize
284
+ restoreTimeoutRef.current = setTimeout(() => {
285
+ // CRITICAL: Handle both numeric keys (from old array-based persistence) and field names
286
+ // If persistedValues has numeric keys, map them to fieldNames
287
+ const persistedKeys = Object.keys(persistedValues as Record<string, any>);
288
+ const hasNumericKeys = persistedKeys.length > 0 && persistedKeys.every(key => /^\d+$/.test(key));
289
+
290
+ let valuesToRestore: Record<string, any>;
291
+ if (hasNumericKeys && fieldNames.length === persistedKeys.length) {
292
+ // Map numeric keys to field names
293
+ valuesToRestore = {};
294
+ for (let i = 0; i < fieldNames.length; i++) {
295
+ const fieldName = fieldNames[i];
296
+ const numericKey = String(i);
297
+ if (numericKey in persistedValues) {
298
+ valuesToRestore[fieldName] = (persistedValues as Record<string, any>)[numericKey];
299
+ }
300
+ }
301
+ console.log('[Form Persistence] Mapped numeric keys to field names:', {
302
+ numericKeys: persistedKeys,
303
+ fieldNames,
304
+ mappedValues: valuesToRestore,
305
+ });
306
+ } else {
307
+ // Use persistedValues as-is (should have field names as keys)
308
+ valuesToRestore = persistedValues as Record<string, any>;
309
+ }
310
+
311
+ // Filter sensitive fields
312
+ const restoreKeys = Object.keys(valuesToRestore);
313
+ const filteredPersisted = filterSensitiveFields(
314
+ valuesToRestore,
315
+ restoreKeys.length > 0 ? restoreKeys : fieldNames,
316
+ fieldTypes
317
+ );
318
+
319
+ // Debug: Check which fields are being filtered
320
+ const sensitiveFields = restoreKeys.filter(name => {
321
+ const type = fieldTypes?.[name];
322
+ return isSensitiveField(name, type);
323
+ });
324
+
325
+ console.log('[Form Persistence] ✅ Restoring persisted values:', {
326
+ persistenceKey,
327
+ persistedValuesKeys: persistedKeys,
328
+ persistedValuesString: JSON.stringify(persistedValues),
329
+ hasNumericKeys,
330
+ valuesToRestoreKeys: Object.keys(valuesToRestore),
331
+ filteredPersistedKeys: Object.keys(filteredPersisted),
332
+ fieldNames,
333
+ sensitiveFields,
334
+ filteredCount: Object.keys(filteredPersisted).length,
335
+ persistedCount: persistedKeys.length,
336
+ timestamp: new Date().toISOString(),
337
+ });
338
+
339
+ // Set values that might not have been in defaultValues
340
+ const valuesToSet: Partial<TFieldValues> = {};
341
+ let hasValuesToSet = false;
342
+
343
+ for (const [key, value] of Object.entries(filteredPersisted)) {
344
+ const currentValue = methods.getValues(key as any);
345
+ // Only set if different from current value (to avoid unnecessary updates)
346
+ if (currentValue !== value) {
347
+ valuesToSet[key as keyof TFieldValues] = value as any;
348
+ hasValuesToSet = true;
349
+ }
350
+ }
351
+
352
+ if (hasValuesToSet) {
353
+ console.log('[Form Persistence] 🔄 Setting form values via reset():', {
354
+ persistenceKey,
355
+ valuesToSetKeys: Object.keys(valuesToSet),
356
+ valuesToSet,
357
+ timestamp: new Date().toISOString(),
358
+ });
359
+ // Use reset to update all values at once
360
+ methods.reset({
361
+ ...methods.getValues(),
362
+ ...valuesToSet,
363
+ } as TFieldValues);
364
+ console.log('[Form Persistence] ✅ Form values set successfully', {
365
+ persistenceKey,
366
+ currentValues: methods.getValues(),
367
+ });
368
+ } else {
369
+ console.log('[Form Persistence] ⏭️ No values to set (all values already match)', {
370
+ persistenceKey,
371
+ });
372
+ }
373
+
374
+ lastRestoredRef.current = persistedValuesStr;
375
+ hasRestoredRef.current = true;
376
+ isRestoringRef.current = false;
377
+ restoreTimeoutRef.current = null;
378
+ }, 100); // Small delay to ensure form is ready
379
+
380
+ return () => {
381
+ if (restoreTimeoutRef.current) {
382
+ clearTimeout(restoreTimeoutRef.current);
383
+ restoreTimeoutRef.current = null;
384
+ }
385
+ };
386
+ // eslint-disable-next-line react-hooks/exhaustive-deps
387
+ }, [persistedValues, persistenceKey]); // Run when persistedValues changes (e.g., dialog auto-opens)
388
+
389
+ // Log component mount
390
+
391
+ // Watch form values for persistence
392
+ // CRITICAL: Don't pass name parameter - useWatch without name returns all values as an object
393
+ // If we pass fieldNames array, it returns an array with numeric indices, not an object with field names
394
+ const watchedValues = useWatch({
395
+ control: methods.control,
396
+ // Don't pass name - we want all values as an object, not an array
397
+ }) as Partial<TFieldValues>;
398
+
399
+ // Track previous values to prevent unnecessary persistence updates
400
+ const previousValuesRef = useRef<string | null>(null);
401
+ const persistTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
402
+
403
+ // Persist form values (filtered for sensitive fields)
404
+ useEffect(() => {
405
+ if (!persistenceKey || !watchedValues || isRestoringRef.current) {
406
+ return;
407
+ }
408
+
409
+ // Skip if values haven't actually changed
410
+ const currentValuesStr = JSON.stringify(watchedValues);
411
+ if (currentValuesStr === previousValuesRef.current) {
412
+ return;
413
+ }
414
+
415
+ previousValuesRef.current = currentValuesStr;
416
+
417
+ // Clear any pending persistence
418
+ if (persistTimeoutRef.current) {
419
+ clearTimeout(persistTimeoutRef.current);
420
+ }
421
+
422
+ // Debounce persistence to prevent excessive updates while user is typing
423
+ persistTimeoutRef.current = setTimeout(() => {
424
+ // Filter sensitive fields before persisting
425
+ // CRITICAL: Use all keys from watchedValues to ensure we capture all values
426
+ const allFieldNames = Object.keys(watchedValues as Record<string, any>);
427
+ const filteredValues = filterSensitiveFields(
428
+ watchedValues as Record<string, any>,
429
+ allFieldNames.length > 0 ? allFieldNames : fieldNames,
430
+ fieldTypes
431
+ );
432
+
433
+ // Debug: Check which fields are being filtered
434
+ const sensitiveFields = allFieldNames.filter(name => {
435
+ const type = fieldTypes?.[name];
436
+ return isSensitiveField(name, type);
437
+ });
438
+
439
+ console.log('[Form Persistence] 💾 Persisting form values:', {
440
+ persistenceKey,
441
+ filteredValuesKeys: Object.keys(filteredValues),
442
+ originalValuesKeys: Object.keys(watchedValues as Record<string, any>),
443
+ allFieldNames,
444
+ sensitiveFields,
445
+ filteredCount: Object.keys(filteredValues).length,
446
+ originalCount: allFieldNames.length,
447
+ timestamp: new Date().toISOString(),
448
+ });
449
+
450
+ setPersistedValues(filteredValues as Partial<TFieldValues>);
451
+
452
+ // Log sessionStorage after setting (with delay to allow write)
453
+ if (persistenceKey) {
454
+ setTimeout(() => {
455
+ const storageKey = `pace-core:draft:${persistenceKey}`;
456
+ const stored = sessionStorage.getItem(storageKey);
457
+ console.log('[Form Persistence] 📦 SessionStorage AFTER setPersistedValues:', {
458
+ persistenceKey,
459
+ storageKey,
460
+ stored: stored ? JSON.parse(stored) : null,
461
+ });
462
+ }, 100);
463
+ }
464
+ persistTimeoutRef.current = null;
465
+ }, 300); // Debounce for 300ms
466
+
467
+ return () => {
468
+ if (persistTimeoutRef.current) {
469
+ clearTimeout(persistTimeoutRef.current);
470
+ persistTimeoutRef.current = null;
471
+ }
472
+ };
473
+ // eslint-disable-next-line react-hooks/exhaustive-deps
474
+ }, [watchedValues, persistenceKey]); // CRITICAL: Only depend on watchedValues and persistenceKey to prevent infinite loops
475
+
476
+ // Enhanced submit handler that clears draft on success
477
+ const handleSubmit = methods.handleSubmit(
478
+ async (data) => {
479
+ console.log('[Form Lifecycle] 📤 Form submit started', {
480
+ persistenceKey,
481
+ dataKeys: Object.keys(data),
482
+ timestamp: new Date().toISOString(),
483
+ });
484
+
485
+ await onSubmit(data);
486
+
487
+ console.log('[Form Lifecycle] ✅ Form submit successful', {
488
+ persistenceKey,
489
+ timestamp: new Date().toISOString(),
490
+ });
491
+
492
+ // Clear draft after successful submit
493
+ if (persistenceKey && clearDraft) {
494
+ console.log('[Form Persistence] 🗑️ Clearing draft after successful submit', {
495
+ persistenceKey,
496
+ });
497
+ clearDraft();
498
+
499
+ // Log sessionStorage after clearing
500
+ setTimeout(() => {
501
+ const storageKey = `pace-core:draft:${persistenceKey}`;
502
+ const stored = sessionStorage.getItem(storageKey);
503
+ console.log('[Form Persistence] 📦 SessionStorage AFTER clearDraft (submit):', {
504
+ persistenceKey,
505
+ storageKey,
506
+ stored: stored ? JSON.parse(stored) : null,
507
+ });
508
+ }, 100);
509
+ }
510
+ },
511
+ (errors) => {
512
+ console.log('[Form Lifecycle] ❌ Form submit failed with errors', {
513
+ persistenceKey,
514
+ errors,
515
+ timestamp: new Date().toISOString(),
516
+ });
517
+ onError?.(errors);
518
+ }
519
+ );
160
520
 
161
521
  return (
162
522
  <FormProvider {...methods}>
@@ -322,16 +682,16 @@ export function FormField<
322
682
  const { control } = useFormContext<TFieldValues>();
323
683
 
324
684
  return (
325
- <div className={cn("space-y-2", className)}>
685
+ <label className={cn("space-y-2", className)}>
326
686
  {label && (
327
- <Label htmlFor={name}>
687
+ <>
328
688
  {label}
329
689
  {validation?.required && (
330
690
  <span className="text-destructive ml-1" aria-label="required">
331
691
  *
332
692
  </span>
333
693
  )}
334
- </Label>
694
+ </>
335
695
  )}
336
696
 
337
697
  <Controller
@@ -375,6 +735,6 @@ export function FormField<
375
735
  );
376
736
  }}
377
737
  />
378
- </div>
738
+ </label>
379
739
  );
380
740
  }