@jmruthers/pace-core 0.6.5 → 0.6.7

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 (473) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +5 -403
  3. package/audit-tool/00-dependencies.cjs +394 -0
  4. package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
  5. package/audit-tool/audits/02-project-structure.cjs +255 -0
  6. package/audit-tool/audits/03-architecture.cjs +196 -0
  7. package/audit-tool/audits/04-code-quality.cjs +149 -0
  8. package/audit-tool/audits/05-styling.cjs +224 -0
  9. package/audit-tool/audits/06-security-rbac.cjs +544 -0
  10. package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
  11. package/audit-tool/audits/08-testing-documentation.cjs +202 -0
  12. package/audit-tool/audits/09-operations.cjs +208 -0
  13. package/audit-tool/index.cjs +291 -0
  14. package/audit-tool/utils/code-utils.cjs +218 -0
  15. package/audit-tool/utils/file-utils.cjs +230 -0
  16. package/audit-tool/utils/report-utils.cjs +241 -0
  17. package/core-usage-manifest.json +93 -0
  18. package/cursor-rules/00-standards-overview.mdc +156 -0
  19. package/cursor-rules/01-pace-core-compliance.mdc +586 -0
  20. package/cursor-rules/02-project-structure.mdc +42 -4
  21. package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +126 -10
  22. package/cursor-rules/04-code-quality.mdc +419 -0
  23. package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +104 -34
  24. package/cursor-rules/06-security-rbac.mdc +518 -0
  25. package/cursor-rules/07-api-tech-stack.mdc +377 -0
  26. package/cursor-rules/08-testing-documentation.mdc +324 -0
  27. package/cursor-rules/09-operations.mdc +365 -0
  28. package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
  29. package/dist/DataTable-7PMH7XN7.js +15 -0
  30. package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
  31. package/dist/{PublicPageProvider-QTFVrL-Z.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +33 -72
  32. package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
  33. package/dist/api-Y4MQWOFW.js +4 -0
  34. package/dist/audit-MYQXYZFU.js +3 -0
  35. package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
  36. package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
  37. package/dist/{chunk-UPPMRMYG.js → chunk-5X4QLXRG.js} +73 -151
  38. package/dist/chunk-6F3IILHI.js +62 -0
  39. package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
  40. package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
  41. package/dist/{chunk-FMUCXFII.js → chunk-7ILTDCL2.js} +9 -5
  42. package/dist/{chunk-M43Y4SSO.js → chunk-A3W6LW53.js} +15 -13
  43. package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
  44. package/dist/{chunk-HU2C6SSC.js → chunk-BM4CQ5P3.js} +606 -559
  45. package/dist/chunk-C7NSAPTL.js +1 -0
  46. package/dist/{chunk-J36DSWQK.js → chunk-FEJLJNWA.js} +7 -41
  47. package/dist/{chunk-IHB5DR3H.js → chunk-FTCRZOG2.js} +188 -387
  48. package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
  49. package/dist/chunk-GHYHJTYV.js +994 -0
  50. package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
  51. package/dist/{chunk-FFQEQTNW.js → chunk-IUBRCBSY.js} +134 -45
  52. package/dist/{chunk-6COVEUS7.js → chunk-JGWDVX64.js} +983 -1034
  53. package/dist/{chunk-RGAWHO7N.js → chunk-L4XMVJKY.js} +77 -222
  54. package/dist/chunk-MBADTM7L.js +64 -0
  55. package/dist/{chunk-M7MPQISP.js → chunk-OJ4SKRSV.js} +3 -16
  56. package/dist/{chunk-IVOFDYWT.js → chunk-Q7Q7V5NV.js} +2109 -1604
  57. package/dist/{chunk-JGRYX5UX.js → chunk-S7DKJPLT.js} +29 -58
  58. package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
  59. package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
  60. package/dist/{chunk-NTM7ZSB6.js → chunk-VBCS3DUA.js} +261 -168
  61. package/dist/{chunk-EFN2EIMK.js → chunk-ZFYPMX46.js} +271 -87
  62. package/dist/{chunk-L4OXEN46.js → chunk-ZKAWKYT4.js} +10 -24
  63. package/dist/components.d.ts +7 -5
  64. package/dist/components.js +46 -257
  65. package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
  66. package/dist/eslint-rules/index.cjs +35 -0
  67. package/{src/eslint-rules/pace-core-compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +234 -235
  68. package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
  69. package/dist/eslint-rules/rules/05-styling.cjs +61 -0
  70. package/dist/eslint-rules/rules/06-security-rbac.cjs +806 -0
  71. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
  72. package/dist/eslint-rules/rules/08-testing.cjs +94 -0
  73. package/dist/eslint-rules/utils/helpers.cjs +42 -0
  74. package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
  75. package/dist/hooks.d.ts +6 -6
  76. package/dist/hooks.js +62 -172
  77. package/dist/icons/index.d.ts +1 -0
  78. package/dist/icons/index.js +1 -0
  79. package/dist/index.d.ts +12 -11
  80. package/dist/index.js +67 -660
  81. package/dist/providers.d.ts +2 -2
  82. package/dist/providers.js +8 -35
  83. package/dist/rbac/eslint-rules.d.ts +46 -44
  84. package/dist/rbac/eslint-rules.js +7 -4
  85. package/dist/rbac/index.d.ts +109 -586
  86. package/dist/rbac/index.js +14 -207
  87. package/dist/styles/index.js +2 -12
  88. package/dist/theming/runtime.d.ts +14 -1
  89. package/dist/theming/runtime.js +3 -19
  90. package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
  91. package/dist/{types-CkbwOr4Y.d.ts → types-DXstZpNI.d.ts} +4 -17
  92. package/dist/types-t9H8qKRw.d.ts +55 -0
  93. package/dist/types.d.ts +1 -1
  94. package/dist/types.js +7 -94
  95. package/dist/{usePublicRouteParams-ClnV4tnv.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +20 -20
  96. package/dist/utils.d.ts +24 -117
  97. package/dist/utils.js +54 -392
  98. package/docs/README.md +17 -7
  99. package/docs/api/README.md +4 -402
  100. package/docs/api/modules.md +301 -871
  101. package/docs/api-reference/components.md +21 -21
  102. package/docs/api-reference/deprecated.md +31 -6
  103. package/docs/api-reference/hooks.md +80 -80
  104. package/docs/api-reference/rpc-functions.md +78 -3
  105. package/docs/api-reference/types.md +1 -1
  106. package/docs/api-reference/utilities.md +1 -1
  107. package/docs/architecture/README.md +1 -1
  108. package/docs/core-concepts/events.md +3 -3
  109. package/docs/core-concepts/organisations.md +6 -6
  110. package/docs/core-concepts/permissions.md +6 -6
  111. package/docs/documentation-index.md +12 -18
  112. package/docs/getting-started/cursor-rules.md +3 -23
  113. package/docs/getting-started/dependencies.md +650 -0
  114. package/docs/getting-started/documentation-index.md +1 -1
  115. package/docs/getting-started/examples/README.md +4 -4
  116. package/docs/getting-started/examples/full-featured-app.md +1 -1
  117. package/docs/getting-started/faq.md +2 -2
  118. package/docs/getting-started/installation-guide.md +20 -7
  119. package/docs/getting-started/quick-reference.md +4 -4
  120. package/docs/getting-started/quick-start.md +23 -12
  121. package/docs/implementation-guides/authentication.md +15 -15
  122. package/docs/implementation-guides/component-styling.md +1 -1
  123. package/docs/implementation-guides/data-tables.md +126 -33
  124. package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
  125. package/docs/implementation-guides/dynamic-colors.md +3 -3
  126. package/docs/implementation-guides/file-upload-storage.md +2 -2
  127. package/docs/implementation-guides/hierarchical-datatable.md +40 -60
  128. package/docs/implementation-guides/inactivity-tracking.md +3 -3
  129. package/docs/implementation-guides/large-datasets.md +3 -2
  130. package/docs/implementation-guides/organisation-security.md +2 -2
  131. package/docs/implementation-guides/performance.md +2 -2
  132. package/docs/implementation-guides/permission-enforcement.md +5 -1
  133. package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
  134. package/docs/migration/V0.4.0_rbac-migration.md +6 -6
  135. package/docs/rbac/MIGRATION_GUIDE.md +819 -0
  136. package/docs/rbac/RBAC_CONTRACT.md +724 -0
  137. package/docs/rbac/README.md +17 -8
  138. package/docs/rbac/advanced-patterns.md +6 -6
  139. package/docs/rbac/api-reference.md +20 -20
  140. package/docs/rbac/edge-functions-guide.md +376 -0
  141. package/docs/rbac/event-based-apps.md +3 -3
  142. package/docs/rbac/examples.md +41 -41
  143. package/docs/rbac/getting-started.md +37 -37
  144. package/docs/rbac/performance.md +1 -1
  145. package/docs/rbac/quick-start.md +52 -52
  146. package/docs/rbac/secure-client-protection.md +1 -35
  147. package/docs/rbac/troubleshooting.md +1 -1
  148. package/docs/security/README.md +5 -5
  149. package/docs/standards/0-standards-overview.md +220 -0
  150. package/docs/standards/1-pace-core-compliance-standards.md +986 -0
  151. package/docs/standards/2-project-structure-standards.md +949 -0
  152. package/docs/standards/3-architecture-standards.md +606 -0
  153. package/docs/standards/4-code-quality-standards.md +728 -0
  154. package/docs/standards/5-styling-standards.md +348 -0
  155. package/docs/standards/{07-rbac-and-rls-standard.md → 6-security-rbac-standards.md} +269 -66
  156. package/docs/standards/7-api-tech-stack-standards.md +662 -0
  157. package/docs/standards/8-testing-documentation-standards.md +401 -0
  158. package/docs/standards/9-operations-standards.md +1102 -0
  159. package/docs/standards/README.md +185 -57
  160. package/docs/troubleshooting/README.md +4 -4
  161. package/docs/troubleshooting/common-issues.md +2 -2
  162. package/docs/troubleshooting/debugging.md +9 -9
  163. package/docs/troubleshooting/migration.md +4 -4
  164. package/docs/troubleshooting/organisation-context-setup.md +42 -19
  165. package/eslint-config-pace-core.cjs +33 -6
  166. package/package.json +35 -23
  167. package/scripts/install-cursor-rules.cjs +25 -6
  168. package/scripts/install-eslint-config.cjs +284 -0
  169. package/src/__tests__/fixtures/supabase.ts +1 -1
  170. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +3 -3
  171. package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
  172. package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
  173. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
  174. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +13 -13
  175. package/src/__tests__/helpers/component-test-utils.tsx +1 -1
  176. package/src/__tests__/helpers/supabaseMock.ts +2 -2
  177. package/src/__tests__/integration/UserProfile.test.tsx +14 -14
  178. package/src/__tests__/public-recipe-view.test.ts +38 -9
  179. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
  180. package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
  181. package/src/__tests__/templates/component.test.template.tsx +18 -15
  182. package/src/components/Button/Button.tsx +5 -1
  183. package/src/components/Calendar/Calendar.tsx +201 -47
  184. package/src/components/ContextSelector/ContextSelector.tsx +106 -119
  185. package/src/components/DataTable/AUDIT_REPORT.md +293 -0
  186. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
  187. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
  188. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
  189. package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
  190. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
  191. package/src/components/DataTable/components/DataTableCore.tsx +186 -13
  192. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
  193. package/src/components/DataTable/components/DataTableLayout.tsx +35 -21
  194. package/src/components/DataTable/components/EditFields.tsx +23 -3
  195. package/src/components/DataTable/components/EditableRow.tsx +12 -9
  196. package/src/components/DataTable/components/EmptyState.tsx +10 -9
  197. package/src/components/DataTable/components/FilterRow.tsx +2 -4
  198. package/src/components/DataTable/components/ImportModal.tsx +124 -126
  199. package/src/components/DataTable/components/LoadingState.tsx +5 -6
  200. package/src/components/DataTable/components/RowComponent.tsx +12 -0
  201. package/src/components/DataTable/components/SortIndicator.tsx +50 -0
  202. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
  203. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
  204. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
  205. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
  206. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
  207. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
  208. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
  209. package/src/components/DataTable/components/index.ts +2 -1
  210. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
  211. package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
  212. package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
  213. package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
  214. package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
  215. package/src/components/DataTable/types.ts +5 -18
  216. package/src/components/DataTable/utils/a11yUtils.ts +17 -0
  217. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
  218. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
  219. package/src/components/DateTimeField/DateTimeField.tsx +10 -9
  220. package/src/components/Dialog/Dialog.test.tsx +128 -104
  221. package/src/components/Dialog/Dialog.tsx +742 -24
  222. package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
  223. package/src/components/FileDisplay/FileDisplay.test.tsx +4 -2
  224. package/src/components/FileDisplay/FileDisplay.tsx +23 -17
  225. package/src/components/FileUpload/FileUpload.test.tsx +52 -14
  226. package/src/components/FileUpload/FileUpload.tsx +112 -130
  227. package/src/components/Form/Form.test.tsx +6 -8
  228. package/src/components/Form/Form.tsx +365 -4
  229. package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
  230. package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
  231. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
  232. package/src/components/PaceAppLayout/PaceAppLayout.tsx +11 -15
  233. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
  234. package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
  235. package/src/components/Progress/Progress.tsx +2 -4
  236. package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
  237. package/src/components/Select/Select.tsx +109 -98
  238. package/src/components/Select/types.ts +4 -1
  239. package/src/components/UserMenu/UserMenu.tsx +9 -6
  240. package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
  241. package/src/hooks/__tests__/hooks.integration.test.tsx +55 -57
  242. package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
  243. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
  244. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
  245. package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
  246. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
  247. package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
  248. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
  249. package/src/hooks/public/usePublicEvent.ts +67 -195
  250. package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
  251. package/src/hooks/public/usePublicEventLogo.ts +24 -14
  252. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  253. package/src/hooks/public/usePublicRouteParams.ts +5 -5
  254. package/src/hooks/useAppConfig.ts +28 -26
  255. package/src/hooks/useEventTheme.test.ts +217 -239
  256. package/src/hooks/useEventTheme.ts +16 -28
  257. package/src/hooks/useFileDisplay.ts +2 -2
  258. package/src/hooks/useOrganisationPermissions.ts +5 -7
  259. package/src/hooks/useQueryCache.ts +0 -1
  260. package/src/hooks/useSessionDraft.ts +380 -0
  261. package/src/hooks/useSessionRestoration.ts +3 -1
  262. package/src/icons/index.ts +27 -0
  263. package/src/index.ts +5 -0
  264. package/src/providers/OrganisationProvider.tsx +23 -14
  265. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
  266. package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
  267. package/src/providers/__tests__/EventProvider.test.tsx +61 -61
  268. package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
  269. package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
  270. package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
  271. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
  272. package/src/providers/services/EventServiceProvider.tsx +1 -24
  273. package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
  274. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
  275. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +13 -10
  276. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
  277. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
  278. package/src/rbac/adapters.tsx +7 -295
  279. package/src/rbac/api.test.ts +44 -56
  280. package/src/rbac/api.ts +10 -17
  281. package/src/rbac/cache-invalidation.ts +0 -1
  282. package/src/rbac/compliance/index.ts +10 -0
  283. package/src/rbac/compliance/pattern-detector.ts +553 -0
  284. package/src/rbac/compliance/runtime-compliance.ts +22 -0
  285. package/src/rbac/components/AccessDenied.tsx +150 -0
  286. package/src/rbac/components/NavigationGuard.tsx +12 -20
  287. package/src/rbac/components/PagePermissionGuard.tsx +4 -24
  288. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
  289. package/src/rbac/components/index.ts +3 -41
  290. package/src/rbac/eslint-rules.js +1 -1
  291. package/src/rbac/hooks/index.ts +0 -3
  292. package/src/rbac/hooks/permissions/index.ts +0 -3
  293. package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
  294. package/src/rbac/hooks/usePermissions.ts +0 -3
  295. package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
  296. package/src/rbac/hooks/useResolvedScope.ts +58 -140
  297. package/src/rbac/hooks/useResourcePermissions.test.ts +124 -38
  298. package/src/rbac/hooks/useResourcePermissions.ts +139 -48
  299. package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
  300. package/src/rbac/hooks/useRoleManagement.ts +147 -19
  301. package/src/rbac/hooks/useSecureSupabase.ts +4 -8
  302. package/src/rbac/index.ts +7 -9
  303. package/src/rbac/utils/contextValidator.ts +9 -7
  304. package/src/services/AuthService.ts +130 -18
  305. package/src/services/EventService.ts +4 -97
  306. package/src/services/InactivityService.ts +16 -0
  307. package/src/services/OrganisationService.ts +7 -44
  308. package/src/services/__tests__/OrganisationService.test.ts +26 -8
  309. package/src/services/base/BaseService.ts +0 -3
  310. package/src/styles/core.css +7 -0
  311. package/src/theming/__tests__/parseEventColours.test.ts +9 -3
  312. package/src/theming/parseEventColours.ts +22 -10
  313. package/src/types/database.generated.ts +4733 -3809
  314. package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
  315. package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
  316. package/src/utils/context/organisationContext.test.ts +13 -28
  317. package/src/utils/context/organisationContext.ts +21 -52
  318. package/src/utils/dynamic/dynamicUtils.ts +1 -1
  319. package/src/utils/file-reference/index.ts +39 -15
  320. package/src/utils/formatting/formatDateTime.test.ts +3 -2
  321. package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
  322. package/src/utils/index.ts +4 -1
  323. package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
  324. package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
  325. package/src/utils/persistence/keyDerivation.ts +304 -0
  326. package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
  327. package/src/utils/security/secureStorage.ts +5 -5
  328. package/src/utils/storage/README.md +1 -1
  329. package/src/utils/storage/helpers.ts +3 -3
  330. package/src/utils/supabase/createBaseClient.ts +147 -0
  331. package/src/utils/timezone/timezone.test.ts +1 -2
  332. package/src/utils/timezone/timezone.ts +1 -1
  333. package/src/utils/validation/csrf.ts +4 -4
  334. package/cursor-rules/00-pace-core-compliance.mdc +0 -331
  335. package/cursor-rules/01-standards-compliance.mdc +0 -244
  336. package/cursor-rules/04-testing-standards.mdc +0 -268
  337. package/cursor-rules/05-bug-reports-and-features.mdc +0 -246
  338. package/cursor-rules/06-code-quality.mdc +0 -309
  339. package/cursor-rules/07-tech-stack-compliance.mdc +0 -214
  340. package/cursor-rules/CHANGELOG.md +0 -119
  341. package/cursor-rules/README.md +0 -192
  342. package/dist/DataTable-AOVNCPTX.js +0 -175
  343. package/dist/DataTable-AOVNCPTX.js.map +0 -1
  344. package/dist/UnifiedAuthProvider-4SBX4LU5.js +0 -18
  345. package/dist/UnifiedAuthProvider-4SBX4LU5.js.map +0 -1
  346. package/dist/api-O6HTBX5Y.js +0 -52
  347. package/dist/api-O6HTBX5Y.js.map +0 -1
  348. package/dist/audit-V53FV5AG.js +0 -17
  349. package/dist/audit-V53FV5AG.js.map +0 -1
  350. package/dist/chunk-5DRSZLL2.js.map +0 -1
  351. package/dist/chunk-63FOKYGO.js.map +0 -1
  352. package/dist/chunk-6COVEUS7.js.map +0 -1
  353. package/dist/chunk-AFVQODI2.js +0 -263
  354. package/dist/chunk-AFVQODI2.js.map +0 -1
  355. package/dist/chunk-DGUM43GV.js.map +0 -1
  356. package/dist/chunk-E66EQZE6.js.map +0 -1
  357. package/dist/chunk-EFN2EIMK.js.map +0 -1
  358. package/dist/chunk-FFQEQTNW.js.map +0 -1
  359. package/dist/chunk-FMUCXFII.js.map +0 -1
  360. package/dist/chunk-G37KK66H.js.map +0 -1
  361. package/dist/chunk-G7QEZTYQ.js +0 -2053
  362. package/dist/chunk-G7QEZTYQ.js.map +0 -1
  363. package/dist/chunk-HU2C6SSC.js.map +0 -1
  364. package/dist/chunk-IHB5DR3H.js.map +0 -1
  365. package/dist/chunk-IVOFDYWT.js.map +0 -1
  366. package/dist/chunk-J36DSWQK.js.map +0 -1
  367. package/dist/chunk-JGRYX5UX.js.map +0 -1
  368. package/dist/chunk-KQCRWDSA.js +0 -1
  369. package/dist/chunk-KQCRWDSA.js.map +0 -1
  370. package/dist/chunk-L4OXEN46.js.map +0 -1
  371. package/dist/chunk-LMC26NLJ.js +0 -84
  372. package/dist/chunk-LMC26NLJ.js.map +0 -1
  373. package/dist/chunk-M43Y4SSO.js.map +0 -1
  374. package/dist/chunk-M7MPQISP.js.map +0 -1
  375. package/dist/chunk-NTM7ZSB6.js.map +0 -1
  376. package/dist/chunk-PWLANIRT.js.map +0 -1
  377. package/dist/chunk-QXHPKYJV.js.map +0 -1
  378. package/dist/chunk-RGAWHO7N.js.map +0 -1
  379. package/dist/chunk-UPPMRMYG.js.map +0 -1
  380. package/dist/chunk-VBXEHIUJ.js.map +0 -1
  381. package/dist/chunk-ZSAAAMVR.js.map +0 -1
  382. package/dist/components.js.map +0 -1
  383. package/dist/contextValidator-5OGXSPKS.js +0 -9
  384. package/dist/contextValidator-5OGXSPKS.js.map +0 -1
  385. package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
  386. package/dist/hooks.js.map +0 -1
  387. package/dist/index.js.map +0 -1
  388. package/dist/providers.js.map +0 -1
  389. package/dist/rbac/eslint-rules.js.map +0 -1
  390. package/dist/rbac/index.js.map +0 -1
  391. package/dist/styles/index.js.map +0 -1
  392. package/dist/theming/runtime.js.map +0 -1
  393. package/dist/types.js.map +0 -1
  394. package/dist/utils.js.map +0 -1
  395. package/docs/best-practices/README.md +0 -472
  396. package/docs/best-practices/accessibility.md +0 -601
  397. package/docs/best-practices/common-patterns.md +0 -516
  398. package/docs/best-practices/deployment.md +0 -1103
  399. package/docs/best-practices/performance.md +0 -1328
  400. package/docs/best-practices/security.md +0 -940
  401. package/docs/best-practices/testing.md +0 -1034
  402. package/docs/rbac/compliance/compliance-guide.md +0 -544
  403. package/docs/standards/01-architecture-standard.md +0 -44
  404. package/docs/standards/02-api-and-rpc-standard.md +0 -39
  405. package/docs/standards/03-component-standard.md +0 -32
  406. package/docs/standards/04-code-style-standard.md +0 -32
  407. package/docs/standards/05-security-standard.md +0 -44
  408. package/docs/standards/06-testing-and-docs-standard.md +0 -29
  409. package/docs/standards/pace-core-compliance.md +0 -432
  410. package/scripts/audit/core/checks/accessibility.cjs +0 -197
  411. package/scripts/audit/core/checks/api-usage.cjs +0 -191
  412. package/scripts/audit/core/checks/bundle.cjs +0 -142
  413. package/scripts/audit/core/checks/compliance.cjs +0 -2706
  414. package/scripts/audit/core/checks/config.cjs +0 -54
  415. package/scripts/audit/core/checks/coverage.cjs +0 -84
  416. package/scripts/audit/core/checks/dependencies.cjs +0 -994
  417. package/scripts/audit/core/checks/documentation.cjs +0 -268
  418. package/scripts/audit/core/checks/environment.cjs +0 -116
  419. package/scripts/audit/core/checks/error-handling.cjs +0 -340
  420. package/scripts/audit/core/checks/forms.cjs +0 -172
  421. package/scripts/audit/core/checks/heuristics.cjs +0 -68
  422. package/scripts/audit/core/checks/hooks.cjs +0 -334
  423. package/scripts/audit/core/checks/imports.cjs +0 -244
  424. package/scripts/audit/core/checks/performance.cjs +0 -325
  425. package/scripts/audit/core/checks/routes.cjs +0 -117
  426. package/scripts/audit/core/checks/state.cjs +0 -130
  427. package/scripts/audit/core/checks/structure.cjs +0 -65
  428. package/scripts/audit/core/checks/style.cjs +0 -584
  429. package/scripts/audit/core/checks/testing.cjs +0 -122
  430. package/scripts/audit/core/checks/typescript.cjs +0 -61
  431. package/scripts/audit/core/scanner.cjs +0 -199
  432. package/scripts/audit/core/utils.cjs +0 -137
  433. package/scripts/audit/index.cjs +0 -223
  434. package/scripts/audit/reporters/console.cjs +0 -151
  435. package/scripts/audit/reporters/json.cjs +0 -54
  436. package/scripts/audit/reporters/markdown.cjs +0 -124
  437. package/scripts/audit-consuming-app.cjs +0 -86
  438. package/src/components/DataTable/components/DataTableBody.tsx +0 -454
  439. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
  440. package/src/components/DataTable/components/ExpandButton.tsx +0 -113
  441. package/src/components/DataTable/components/GroupHeader.tsx +0 -54
  442. package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
  443. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
  444. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
  445. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
  446. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
  447. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
  448. package/src/components/DataTable/core/DataTableContext.tsx +0 -216
  449. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
  450. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
  451. package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
  452. package/src/components/DataTable/utils/debugTools.ts +0 -514
  453. package/src/eslint-rules/pace-core-compliance.js +0 -638
  454. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
  455. package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
  456. package/src/rbac/components/NavigationProvider.test.tsx +0 -481
  457. package/src/rbac/components/NavigationProvider.tsx +0 -345
  458. package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
  459. package/src/rbac/components/PagePermissionProvider.tsx +0 -279
  460. package/src/rbac/components/PermissionEnforcer.tsx +0 -312
  461. package/src/rbac/components/RoleBasedRouter.tsx +0 -440
  462. package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
  463. package/src/rbac/components/SecureDataProvider.tsx +0 -339
  464. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
  465. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
  466. package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
  467. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
  468. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
  469. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
  470. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
  471. package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
  472. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
  473. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
@@ -90,10 +90,14 @@
90
90
  import * as React from 'react';
91
91
  import { createPortal } from 'react-dom';
92
92
  import { X } from 'lucide-react';
93
+ import { useLocation } from 'react-router-dom';
94
+ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
93
95
  import { cn } from '../../utils/core/cn';
94
96
  import { renderSafeHtml } from '../../utils/validation/htmlSanitization';
95
- import { useState, useEffect, useRef, useCallback, useId } from 'react';
97
+ import { useState, useEffect, useRef, useCallback, useId, useMemo } from 'react';
96
98
  import { useFocusTrap } from '../../hooks/useFocusTrap';
99
+ import { useSessionDraft } from '../../hooks/useSessionDraft';
100
+ import { deriveDialogKey } from '../../utils/persistence/keyDerivation';
97
101
 
98
102
  /**
99
103
  * Simple debounce function that matches lodash debounce API
@@ -139,6 +143,8 @@ interface DialogContextValue {
139
143
  dialogRef: React.RefObject<HTMLDialogElement | null>;
140
144
  titleId: string;
141
145
  descriptionId: string;
146
+ dialogTitle?: string; // For persistence key derivation
147
+ markClosedByUser?: () => void; // Callback to mark dialog as closed by user (for Cancel buttons, etc.)
142
148
  }
143
149
 
144
150
  const DialogContext = React.createContext<DialogContextValue | null>(null);
@@ -204,10 +210,12 @@ export interface DialogContentProps extends React.HTMLAttributes<HTMLDialogEleme
204
210
  minHeight?: string;
205
211
  /** Minimum width in CSS units */
206
212
  minWidth?: string;
207
- /** Dialog title for accessibility (sets native title attribute) */
213
+ /** Dialog title - sets native title attribute and aria-labelledby */
208
214
  title?: string;
209
- /** Dialog description for accessibility (sets aria-description attribute) */
215
+ /** Dialog description - sets native aria-description attribute */
210
216
  description?: string;
217
+ /** Whether to persist open state across tab switches */
218
+ persistOpenState?: boolean;
211
219
  }
212
220
 
213
221
  /**
@@ -311,6 +319,8 @@ const Dialog = React.memo<DialogProps>(function Dialog({
311
319
  const dialogRef = useRef<HTMLDialogElement | null>(null);
312
320
  const titleId = useId();
313
321
  const descriptionId = useId();
322
+ const dialogTitleRef = useRef<string | undefined>(undefined);
323
+ const [markClosedByUser, setMarkClosedByUserState] = useState<(() => void) | undefined>(undefined);
314
324
 
315
325
  const isControlled = controlledOpen !== undefined;
316
326
  const open = isControlled ? controlledOpen : internalOpen;
@@ -328,14 +338,39 @@ const Dialog = React.memo<DialogProps>(function Dialog({
328
338
  dialogRef,
329
339
  titleId,
330
340
  descriptionId,
331
- }), [open, handleOpenChange, titleId, descriptionId]);
341
+ dialogTitle: dialogTitleRef.current,
342
+ markClosedByUser, // Set by DialogContent
343
+ }), [open, handleOpenChange, titleId, descriptionId, markClosedByUser]);
344
+
345
+ // Expose function to set dialog title (called by DialogContent)
346
+ const setDialogTitle = useCallback((title: string | undefined) => {
347
+ dialogTitleRef.current = title;
348
+ }, []);
349
+
350
+ // Expose function to set markClosedByUser callback (called by DialogContent)
351
+ const setMarkClosedByUser = useCallback((callback: (() => void) | undefined) => {
352
+ setMarkClosedByUserState(callback);
353
+ }, []);
332
354
 
333
355
  return (
334
356
  <DialogContext.Provider value={contextValue}>
335
- {children}
357
+ <DialogTitleContext.Provider value={setDialogTitle}>
358
+ <DialogMarkClosedContext.Provider value={setMarkClosedByUser}>
359
+ {children}
360
+ </DialogMarkClosedContext.Provider>
361
+ </DialogTitleContext.Provider>
336
362
  </DialogContext.Provider>
337
363
  );
338
364
  });
365
+
366
+ // Context for setting dialog title from DialogContent
367
+ const DialogTitleContext = React.createContext<((title: string | undefined) => void) | null>(null);
368
+
369
+ // Context for setting markClosedByUser callback from DialogContent
370
+ const DialogMarkClosedContext = React.createContext<((callback: (() => void) | undefined) => void) | null>(null);
371
+
372
+ // Context for marking dialog as closed by user (from DialogContent to DialogClose)
373
+ const DialogCloseContext = React.createContext<(() => void) | null>(null);
339
374
  Dialog.displayName = 'Dialog';
340
375
 
341
376
  /**
@@ -512,6 +547,89 @@ const useSmartDimensions = ({
512
547
  return result;
513
548
  };
514
549
 
550
+ /**
551
+ * Global lock to ensure only one dialog is open at a time
552
+ * Uses sessionStorage for persistence across page reloads
553
+ */
554
+ const DIALOG_LOCK_KEY = 'pace-core:dialog:lock';
555
+
556
+ function acquireDialogLock(persistenceKey: string | null): boolean {
557
+ if (!persistenceKey) {
558
+ return true; // Non-persisted dialogs can always open
559
+ }
560
+
561
+ try {
562
+ const lock = sessionStorage.getItem(DIALOG_LOCK_KEY);
563
+ if (lock) {
564
+ const lockData = JSON.parse(lock);
565
+ // If lock is held by this dialog, allow it
566
+ if (lockData.key === persistenceKey) {
567
+ return true;
568
+ }
569
+ // If lock is held by another dialog, check if that dialog is still open
570
+ const lockDialog = document.querySelector(`dialog[data-persistence-key="${lockData.key}"]`) as HTMLDialogElement;
571
+ if (lockDialog && lockDialog.open) {
572
+ return false; // Another dialog is still open
573
+ }
574
+ // Lock is stale, clear it
575
+ sessionStorage.removeItem(DIALOG_LOCK_KEY);
576
+ }
577
+ // Acquire the lock
578
+ sessionStorage.setItem(DIALOG_LOCK_KEY, JSON.stringify({
579
+ key: persistenceKey,
580
+ timestamp: Date.now(),
581
+ }));
582
+ return true;
583
+ } catch {
584
+ // If sessionStorage fails, allow opening (graceful degradation)
585
+ return true;
586
+ }
587
+ }
588
+
589
+ function releaseDialogLock(persistenceKey: string | null): void {
590
+ if (!persistenceKey) {
591
+ return;
592
+ }
593
+
594
+ try {
595
+ const lock = sessionStorage.getItem(DIALOG_LOCK_KEY);
596
+ if (lock) {
597
+ const lockData = JSON.parse(lock);
598
+ if (lockData.key === persistenceKey) {
599
+ sessionStorage.removeItem(DIALOG_LOCK_KEY);
600
+ }
601
+ }
602
+ } catch {
603
+ // Ignore errors
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Check if any other dialog (besides the current one) has persisted open state
609
+ * This helps determine if another dialog should be allowed to auto-open
610
+ */
611
+ function checkOtherDialogsHavePersistedState(currentPersistenceKey: string | null): boolean {
612
+ if (!currentPersistenceKey) {
613
+ return false;
614
+ }
615
+
616
+ try {
617
+ const lock = sessionStorage.getItem(DIALOG_LOCK_KEY);
618
+ if (lock) {
619
+ const lockData = JSON.parse(lock);
620
+ if (lockData.key !== currentPersistenceKey) {
621
+ // Another dialog holds the lock
622
+ return true;
623
+ }
624
+ }
625
+ } catch {
626
+ // Error accessing sessionStorage - assume no other dialogs
627
+ return false;
628
+ }
629
+
630
+ return false;
631
+ }
632
+
515
633
  /**
516
634
  * DialogContent component
517
635
  * The main content container using semantic HTML <dialog> element with enhanced features
@@ -538,16 +656,425 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
538
656
  title,
539
657
  description,
540
658
  style,
659
+ persistOpenState = true,
541
660
  ...props
542
661
  }, ref) => {
662
+ // Call all hooks unconditionally at the top level
663
+ // Hooks must be called in the same order on every render
543
664
  const { open, onOpenChange, dialogRef, titleId, descriptionId } = useDialogContext();
665
+ const setDialogTitle = React.useContext(DialogTitleContext);
666
+ const setMarkClosedByUser = React.useContext(DialogMarkClosedContext);
667
+
668
+ // Component mount/unmount tracking removed for performance
669
+
670
+ // Call hooks unconditionally - if providers are missing, they will throw
671
+ // Errors should be handled by error boundaries at a higher level
672
+ const location = useLocation();
673
+ const auth = useUnifiedAuth();
674
+ const userId = auth.user?.id || null;
675
+
676
+ // Set dialog title in context for persistence
677
+ useEffect(() => {
678
+ if (setDialogTitle) {
679
+ setDialogTitle(title);
680
+ }
681
+ }, [title, setDialogTitle]);
682
+
683
+ // Derive persistence key (scoped by user ID)
684
+ // CRITICAL: Only enable persistence if we have a valid userId to prevent data leakage
685
+ const persistenceKey = useMemo(() => {
686
+ if (!persistOpenState) {
687
+ return null;
688
+ }
689
+ // Don't create persistence key if userId is not available
690
+ // This prevents unscoped persistence that could leak between users
691
+ if (!userId) {
692
+ return null;
693
+ }
694
+ return deriveDialogKey(
695
+ {
696
+ title,
697
+ description,
698
+ },
699
+ location,
700
+ userId
701
+ );
702
+ }, [title, description, location, userId, persistOpenState]);
703
+
704
+ // Use session draft for open state persistence
705
+ // Only enabled when we have a valid persistenceKey (which requires userId)
706
+ const { state: persistedOpen, setState: setPersistedOpen, clearDraft, wasRestored } = useSessionDraft<boolean>(
707
+ persistenceKey ? `${persistenceKey}:open` : 'dialog:no-key:open',
708
+ false,
709
+ {
710
+ enabled: Boolean(persistenceKey && persistOpenState && userId),
711
+ debounceMs: 300,
712
+ }
713
+ );
714
+
715
+ // Track if we've attempted auto-open to prevent multiple attempts
716
+ const hasAutoOpenedRef = useRef(false);
717
+ const hasInitializedRef = useRef(false);
718
+ // Track if dialog was closed by user action (to clear persistence)
719
+ const wasClosedByUserRef = useRef(false);
720
+ // Track if dialog was manually opened (to prevent auto-open from interfering)
721
+ const wasManuallyOpenedRef = useRef(false);
722
+
723
+ // Callback to mark dialog as closed by user (exposed via context for DialogClose and Cancel buttons)
724
+ const markClosedByUser = useCallback(() => {
725
+ if (hasInitializedRef.current) {
726
+ wasClosedByUserRef.current = true;
727
+ }
728
+ }, []);
729
+
730
+ // Register markClosedByUser with parent Dialog component so it's available via DialogContext
731
+ useEffect(() => {
732
+ if (setMarkClosedByUser) {
733
+ setMarkClosedByUser(markClosedByUser);
734
+ }
735
+ return () => {
736
+ // Cleanup: unregister when DialogContent unmounts
737
+ if (setMarkClosedByUser) {
738
+ setMarkClosedByUser(undefined);
739
+ }
740
+ };
741
+ }, [setMarkClosedByUser, markClosedByUser]);
742
+ // Track if we've cleaned up other dialog states (to prevent multiple cleanup runs)
743
+ const hasCleanedUpOtherDialogsRef = useRef(false);
744
+
745
+ // Auto-open on mount if dialog was open when tab closed
746
+ useEffect(() => {
747
+ if (!persistenceKey || !persistOpenState) {
748
+ hasInitializedRef.current = true;
749
+ return;
750
+ }
751
+
752
+ // Only attempt auto-open once
753
+ // This prevents auto-open from running after manual opens
754
+ if (hasAutoOpenedRef.current) {
755
+ return;
756
+ }
757
+
758
+ // CRITICAL: Don't auto-open if dialog was manually opened
759
+ // This prevents auto-open from interfering with manual opens
760
+ if (wasManuallyOpenedRef.current) {
761
+ console.log('[Dialog Persistence] ⏭️ Skipping auto-open - dialog was manually opened', {
762
+ persistenceKey,
763
+ currentOpen: open,
764
+ });
765
+ return;
766
+ }
767
+
768
+ // Mark as initialized after first check
769
+ if (!hasInitializedRef.current) {
770
+ hasInitializedRef.current = true;
771
+ }
772
+
773
+ // Auto-open check (logging removed for performance)
774
+
775
+ // CRITICAL: Don't auto-open if dialog is already open (user-initiated)
776
+ if (open === true) {
777
+ hasAutoOpenedRef.current = true;
778
+ return;
779
+ }
780
+
781
+ // CRITICAL: Don't auto-open if userId is not available (prevents unscoped persistence from being restored)
782
+ if (!userId) {
783
+ hasAutoOpenedRef.current = true;
784
+ return;
785
+ }
786
+
787
+ // Only auto-open if conditions are met
788
+ if (persistedOpen === true && open === false && wasRestored && !hasAutoOpenedRef.current && !wasManuallyOpenedRef.current) {
789
+ const AUTO_OPEN_LOCK_KEY = 'pace-core:dialog:auto-open-lock';
790
+ const lockTimestamp = sessionStorage.getItem(AUTO_OPEN_LOCK_KEY);
791
+ const now = Date.now();
792
+
793
+ // Check if another dialog is already auto-opening (lock exists and is recent)
794
+ if (lockTimestamp) {
795
+ const lockAge = now - parseInt(lockTimestamp, 10);
796
+ if (lockAge < 1000) {
797
+ // Lock is recent - another dialog is auto-opening
798
+ // Check if there's already an open dialog with this persistence key
799
+ const existingOpenDialog = document.querySelector(`dialog[data-persistence-key="${persistenceKey}"][open]`);
800
+ if (existingOpenDialog) {
801
+ // Another instance is already open - skip
802
+ hasAutoOpenedRef.current = true;
803
+ return;
804
+ }
805
+ // Check if other dialog has persisted state
806
+ const otherDialogHasPersistedState = checkOtherDialogsHavePersistedState(persistenceKey);
807
+ if (otherDialogHasPersistedState) {
808
+ // Another dialog with persisted state is auto-opening - skip this one
809
+ clearDraft();
810
+ hasAutoOpenedRef.current = true;
811
+ return;
812
+ }
813
+ }
814
+ // Clear stale locks
815
+ if (lockAge > 2000) {
816
+ sessionStorage.removeItem(AUTO_OPEN_LOCK_KEY);
817
+ }
818
+ }
819
+
820
+ // Check if dialog with same key is already open in DOM (synchronous check)
821
+ const existingDialog = document.querySelector(`dialog[data-persistence-key="${persistenceKey}"][open]`);
822
+ if (existingDialog && existingDialog !== dialogRef.current && existingDialog !== internalRef.current) {
823
+ hasAutoOpenedRef.current = true;
824
+ return;
825
+ }
826
+
827
+ // Set lock and mark as auto-opened BEFORE calling onOpenChange
828
+ sessionStorage.setItem(AUTO_OPEN_LOCK_KEY, String(now));
829
+ hasAutoOpenedRef.current = true;
830
+ wasManuallyOpenedRef.current = false;
831
+
832
+ console.log('[Dialog] 🔄 AUTO-OPEN', { persistenceKey });
833
+
834
+ // Use small delay to prevent visual flash
835
+ const timeoutId = setTimeout(() => {
836
+ // Double-check: if dialog is still closed and no other instance opened it
837
+ const stillClosed = !open;
838
+ const noOtherInstance = !document.querySelector(`dialog[data-persistence-key="${persistenceKey}"][open]`);
839
+
840
+ if (stillClosed && noOtherInstance) {
841
+ sessionStorage.removeItem(AUTO_OPEN_LOCK_KEY);
842
+ onOpenChange(true);
843
+ } else {
844
+ sessionStorage.removeItem(AUTO_OPEN_LOCK_KEY);
845
+ }
846
+ }, 75);
847
+
848
+ return () => {
849
+ clearTimeout(timeoutId);
850
+ sessionStorage.removeItem(AUTO_OPEN_LOCK_KEY);
851
+ };
852
+ }
853
+ }, [persistenceKey, persistOpenState, persistedOpen, open, onOpenChange, wasRestored, clearDraft]);
854
+
855
+ // When this dialog auto-opens, clear persisted state of all other dialogs
856
+ // This prevents multiple dialogs from being restored simultaneously
857
+ useEffect(() => {
858
+ if (!persistenceKey || !persistOpenState || !open || !hasAutoOpenedRef.current) {
859
+ return;
860
+ }
861
+
862
+ // Only run once when dialog first auto-opens
863
+ if (hasCleanedUpOtherDialogsRef.current) {
864
+ return;
865
+ }
866
+
867
+ // Clear all other dialog persisted states from sessionStorage
868
+ // AND close any other dialogs that are currently open in the DOM
869
+ // This ensures only the first auto-opened dialog remains open
870
+ try {
871
+ const keysToRemove: string[] = [];
872
+ for (let i = 0; i < sessionStorage.length; i++) {
873
+ const key = sessionStorage.key(i);
874
+ if (key && key.startsWith('pace-core:draft:dialog:') && key.endsWith(':open')) {
875
+ // Don't clear this dialog's own state
876
+ if (key !== `pace-core:draft:${persistenceKey}:open`) {
877
+ keysToRemove.push(key);
878
+ }
879
+ }
880
+ }
881
+
882
+ if (keysToRemove.length > 0) {
883
+ console.log('[Dialog Persistence] Clearing other dialog persisted states:', keysToRemove);
884
+ keysToRemove.forEach(key => sessionStorage.removeItem(key));
885
+ }
886
+
887
+ // Also close any other dialogs that are currently open in the DOM AND have persisted state
888
+ // This prevents dialogs with persisted state from being hidden behind this one
889
+ // We only close dialogs that have persisted state, not dialogs opened by app code
890
+ // We identify dialogs with persistence using a data attribute
891
+ const timeoutId = setTimeout(() => {
892
+ // Guard against test environment teardown
893
+ if (typeof document === 'undefined' || typeof sessionStorage === 'undefined') {
894
+ return;
895
+ }
896
+
897
+ const otherOpenDialogs = document.querySelectorAll('dialog[open][role="dialog"]');
898
+ const currentDialog = dialogRef.current || internalRef.current;
899
+ if (otherOpenDialogs.length > 0 && currentDialog) {
900
+ let closedCount = 0;
901
+ otherOpenDialogs.forEach((dialog) => {
902
+ // Don't close this dialog
903
+ const dialogElement = dialog as HTMLDialogElement;
904
+ if (dialogElement !== currentDialog) {
905
+ // Check if this dialog has a data-persistence-key attribute
906
+ // This indicates it was auto-opened from persistence
907
+ const dialogPersistenceKey = dialogElement.getAttribute('data-persistence-key');
908
+ if (dialogPersistenceKey && dialogPersistenceKey !== persistenceKey) {
909
+ // Check if this dialog's persisted state is true
910
+ let hasPersistedState = false;
911
+ try {
912
+ const key = `pace-core:draft:${dialogPersistenceKey}:open`;
913
+ const stored = sessionStorage.getItem(key);
914
+ if (stored) {
915
+ const parsed = JSON.parse(stored);
916
+ if (parsed && parsed.data === true) {
917
+ hasPersistedState = true;
918
+ }
919
+ }
920
+ } catch {
921
+ // Invalid data - skip
922
+ }
923
+
924
+ // Only close if this dialog has persisted state (was auto-opened)
925
+ if (hasPersistedState) {
926
+ console.log('[Dialog Persistence] Closing other dialog with persisted state:', dialogPersistenceKey);
927
+ dialogElement.close();
928
+ closedCount++;
929
+ }
930
+ }
931
+ // If dialog doesn't have data-persistence-key, it was opened by app code - don't close it
932
+ }
933
+ });
934
+ if (closedCount > 0) {
935
+ console.log('[Dialog Persistence] Closed', closedCount, 'other dialog(s) with persisted state');
936
+ }
937
+ }
938
+ }, 100);
939
+
940
+ hasCleanedUpOtherDialogsRef.current = true;
941
+
942
+ // Also clear the auto-open lock
943
+ if (typeof sessionStorage !== 'undefined') {
944
+ sessionStorage.removeItem('pace-core:dialog:auto-open-lock');
945
+ }
946
+
947
+ // Cleanup timeout on unmount or dependency change
948
+ return () => {
949
+ clearTimeout(timeoutId);
950
+ };
951
+ } catch (error) {
952
+ console.warn('[Dialog Persistence] Failed to clear other dialog states:', error);
953
+ }
954
+ }, [open, persistenceKey, persistOpenState]);
955
+
956
+ // When dialog closes (user action), immediately clear persisted state
957
+ // This prevents the dialog from auto-opening again after user explicitly closed it
958
+ useEffect(() => {
959
+ if (!persistenceKey || !persistOpenState) {
960
+ return;
961
+ }
962
+
963
+ if (!hasInitializedRef.current) {
964
+ return;
965
+ }
966
+
967
+ // If dialog is closed and user closed it, clear persisted state immediately
968
+ if (!open && wasClosedByUserRef.current) {
969
+ clearDraft();
970
+ wasClosedByUserRef.current = false;
971
+ }
972
+ }, [open, persistenceKey, persistOpenState, clearDraft]);
973
+
974
+ // Check lock BEFORE allowing dialog to open (synchronous check)
975
+ // This prevents React from even trying to open if another dialog is open
976
+ useEffect(() => {
977
+ if (!open) {
978
+ return;
979
+ }
980
+
981
+ // Synchronously check if we can acquire the lock
982
+ const lockAcquired = acquireDialogLock(persistenceKey);
983
+ if (!lockAcquired) {
984
+ // Another dialog is open - prevent this one from opening
985
+ console.warn('[Dialog] ⚠️ Cannot open - another dialog holds the lock', {
986
+ persistenceKey,
987
+ });
988
+ // Immediately close this dialog's React state
989
+ onOpenChange?.(false);
990
+ return;
991
+ }
992
+
993
+ // Lock acquired successfully - dialog can proceed to open
994
+ }, [open, persistenceKey, onOpenChange]);
995
+
996
+ // Track when dialog closes via onOpenChange to mark as closed by user
997
+ // This handles Cancel buttons and other programmatic closes
998
+ const previousOpenRef = useRef(open);
999
+ useEffect(() => {
1000
+ // If dialog was open and is now closed, and it wasn't auto-opened, mark as closed by user
1001
+ if (previousOpenRef.current === true && open === false && hasInitializedRef.current) {
1002
+ // Only mark as closed by user if it wasn't an auto-open scenario
1003
+ // Auto-open sets hasAutoOpenedRef before calling onOpenChange, so we can detect it
1004
+ if (!hasAutoOpenedRef.current || wasManuallyOpenedRef.current) {
1005
+ // Dialog was manually opened and then closed - mark as closed by user
1006
+ wasClosedByUserRef.current = true;
1007
+ }
1008
+ }
1009
+ previousOpenRef.current = open;
1010
+ }, [open]);
1011
+
1012
+ // Persist open state changes
1013
+ useEffect(() => {
1014
+ if (!persistenceKey || !persistOpenState) {
1015
+ return;
1016
+ }
1017
+
1018
+ // Only persist after initial mount check is complete
1019
+ // This prevents overwriting the persisted state before auto-open can read it
1020
+ if (!hasInitializedRef.current) {
1021
+ return;
1022
+ }
1023
+
1024
+ // Persisting open state (logging removed for performance)
1025
+
1026
+ let logTimeoutId: ReturnType<typeof setTimeout> | null = null;
1027
+
1028
+ // Only persist when dialog is open
1029
+ if (open) {
1030
+ // Reset the flag when opening
1031
+ wasClosedByUserRef.current = false;
1032
+ // If dialog is manually opened (not via auto-open), mark it so auto-open doesn't interfere
1033
+ // This prevents auto-open from trying to open an already-open dialog
1034
+ // We check if hasAutoOpenedRef is false to determine if this is a manual open
1035
+ // (auto-open sets hasAutoOpenedRef to true before calling onOpenChange)
1036
+ if (!hasAutoOpenedRef.current) {
1037
+ // Mark as manually opened to prevent auto-open from interfering
1038
+ wasManuallyOpenedRef.current = true;
1039
+ // Also mark as "opened" to prevent auto-open from trying to open it again
1040
+ hasAutoOpenedRef.current = true;
1041
+ }
1042
+ setPersistedOpen(true);
1043
+ } else {
1044
+ // Only clear draft if user explicitly closed (not if it was never opened or auto-opened then closed)
1045
+ if (wasClosedByUserRef.current) {
1046
+ clearDraft();
1047
+ wasClosedByUserRef.current = false;
1048
+ // Reset manual open flag when dialog is closed
1049
+ wasManuallyOpenedRef.current = false;
1050
+ }
1051
+ }
1052
+
1053
+ // Cleanup timeout on unmount or dependency change
1054
+ return () => {
1055
+ if (logTimeoutId) {
1056
+ clearTimeout(logTimeoutId);
1057
+ }
1058
+ };
1059
+ }, [open, persistenceKey, persistOpenState, setPersistedOpen, clearDraft]);
1060
+
1061
+ // Note: We do NOT automatically clear the draft when dialog closes
1062
+ // The draft should only be cleared on explicit user actions (e.g., form submit success)
1063
+ // This allows the dialog to restore its state after tab switches
544
1064
  const internalRef = useRef<HTMLDialogElement>(null);
545
1065
 
546
1066
  // Use the dialogRef from context, or fall back to internal ref or forwarded ref
547
1067
  const actualDialogRef = dialogRef.current ? dialogRef : (ref ? (ref as React.RefObject<HTMLDialogElement>) : internalRef);
548
1068
 
1069
+ // Default to 80% viewport height if no height constraint is provided
1070
+ // This allows the dialog to grow to 80% before enabling scrolling
1071
+ const effectiveMaxHeightPercent = maxHeightPercent ?? (maxHeight ? undefined : 80);
1072
+
1073
+ // Determine if we have a height constraint that requires flex layout
1074
+ const hasHeightConstraint = Boolean(effectiveMaxHeightPercent || maxHeight);
1075
+
549
1076
  const smartDimensions = useSmartDimensions({
550
- maxHeightPercent,
1077
+ maxHeightPercent: effectiveMaxHeightPercent,
551
1078
  maxWidthPercent,
552
1079
  maxHeight,
553
1080
  maxWidth,
@@ -592,30 +1119,105 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
592
1119
  if (!dialog) return;
593
1120
 
594
1121
  if (open) {
1122
+ // Log all dialogs in DOM before opening
1123
+ const allDialogsBefore = document.querySelectorAll('dialog[role="dialog"]');
1124
+ const dialogsBefore = Array.from(allDialogsBefore).map((d) => {
1125
+ const dialogEl = d as HTMLDialogElement;
1126
+ return {
1127
+ persistenceKey: dialogEl.getAttribute('data-persistence-key') || 'NO-KEY',
1128
+ open: dialogEl.open,
1129
+ isCurrent: d === dialog,
1130
+ };
1131
+ });
1132
+ console.log('[Dialog] 🟢 OPENING', {
1133
+ persistenceKey,
1134
+ dialogsInDOM: dialogsBefore,
1135
+ totalDialogs: allDialogsBefore.length,
1136
+ });
1137
+
595
1138
  // Use requestAnimationFrame to ensure DOM is ready
1139
+ // Lock was already checked in the earlier useEffect, so we can proceed
596
1140
  requestAnimationFrame(() => {
597
1141
  if (dialog && open) {
1142
+ // Before opening, close any other open dialogs (safety check)
1143
+ const allDialogs = document.querySelectorAll('dialog[role="dialog"]');
1144
+ allDialogs.forEach((d) => {
1145
+ const dialogEl = d as HTMLDialogElement;
1146
+ if (dialogEl !== dialog && dialogEl.open) {
1147
+ dialogEl.setAttribute('data-duplicate-cleanup', 'true');
1148
+ dialogEl.close();
1149
+ }
1150
+ });
1151
+
1152
+ console.log('[Dialog] ✅ showModal() called', { persistenceKey });
598
1153
  dialog.showModal();
599
1154
  }
600
1155
  });
601
1156
  } else {
602
1157
  // Close dialog before it's removed from DOM
603
1158
  if (dialog.open) {
1159
+ console.log('[Dialog] 🔴 CLOSING', { persistenceKey });
604
1160
  dialog.close();
1161
+ // Release the lock
1162
+ releaseDialogLock(persistenceKey);
1163
+
1164
+ // After closing, check if any other dialogs with persistence are trying to open
1165
+ // Only close dialogs that have persistence (data-persistence-key attribute)
1166
+ // Non-persistent dialogs should be left alone
1167
+ setTimeout(() => {
1168
+ const allDialogs = document.querySelectorAll('dialog[role="dialog"]');
1169
+ allDialogs.forEach((d) => {
1170
+ const dialogEl = d as HTMLDialogElement;
1171
+ if (dialogEl !== dialog && dialogEl.open) {
1172
+ const otherPersistenceKey = dialogEl.getAttribute('data-persistence-key');
1173
+ // Only close dialogs that have persistence (they might auto-open)
1174
+ // Non-persistent dialogs are user-controlled and shouldn't be closed
1175
+ if (otherPersistenceKey) {
1176
+ console.warn('[Dialog] 🗑️ Closing other persisted dialog after lock release:', {
1177
+ persistenceKey,
1178
+ otherPersistenceKey,
1179
+ });
1180
+ dialogEl.setAttribute('data-duplicate-cleanup', 'true');
1181
+ dialogEl.close();
1182
+ }
1183
+ }
1184
+ });
1185
+ }, 50);
605
1186
  }
606
1187
  }
607
- }, [open, dialogRef]);
1188
+ }, [open, persistenceKey, dialogRef]);
608
1189
 
609
1190
  // Handle close event - sync state when dialog is closed externally
1191
+ // Also track when dialog is closed by user action (for persistence clearing)
610
1192
  useEffect(() => {
611
1193
  const dialog = dialogRef.current || internalRef.current;
612
1194
  if (!dialog) return;
613
1195
 
614
1196
  const handleClose = () => {
615
- // Only update state if dialog was closed externally (not via our state change)
616
- // Check if dialog is actually closed and our state says it should be open
1197
+ // Check if this close was initiated by the user (via close button)
1198
+ const wasUserClosed = dialog.hasAttribute('data-user-closed');
1199
+ if (wasUserClosed) {
1200
+ dialog.removeAttribute('data-user-closed');
1201
+ if (hasInitializedRef.current) {
1202
+ wasClosedByUserRef.current = true;
1203
+ }
1204
+ }
1205
+
1206
+ // Ignore duplicate cleanup closes
1207
+ const isDuplicateCleanup = dialog.hasAttribute('data-duplicate-cleanup');
1208
+ if (isDuplicateCleanup) {
1209
+ dialog.removeAttribute('data-duplicate-cleanup');
1210
+ return;
1211
+ }
1212
+
617
1213
  if (!dialog.open && open) {
1214
+ // Mark as closed by user if this wasn't an auto-open scenario
1215
+ if (hasInitializedRef.current && !wasUserClosed) {
1216
+ wasClosedByUserRef.current = true;
1217
+ }
618
1218
  onOpenChange(false);
1219
+ } else if (!dialog.open && !open && hasInitializedRef.current && wasUserClosed) {
1220
+ wasClosedByUserRef.current = true;
619
1221
  }
620
1222
  };
621
1223
 
@@ -635,6 +1237,11 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
635
1237
  e.preventDefault();
636
1238
  return;
637
1239
  }
1240
+ // Mark as closed by user and clear persisted state
1241
+ wasClosedByUserRef.current = true;
1242
+ if (persistenceKey && persistOpenState && clearDraft) {
1243
+ clearDraft();
1244
+ }
638
1245
  onOpenChange(false);
639
1246
  };
640
1247
 
@@ -642,7 +1249,7 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
642
1249
  return () => {
643
1250
  dialog.removeEventListener('cancel', handleCancel);
644
1251
  };
645
- }, [preventCloseOnEscape, preventCloseOnOutsideClick, onOpenChange, dialogRef]);
1252
+ }, [preventCloseOnEscape, preventCloseOnOutsideClick, onOpenChange, dialogRef, persistenceKey, persistOpenState, clearDraft]);
646
1253
 
647
1254
  // Merge smart dimensions with provided style
648
1255
  const mergedStyle = React.useMemo(() => {
@@ -660,9 +1267,49 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
660
1267
  return finalStyle;
661
1268
  }, [smartDimensions, style, maxWidth, maxWidthPercent]);
662
1269
 
1270
+ // Track if lock has been acquired (set by useEffect when open becomes true)
1271
+ const [lockAcquired, setLockAcquired] = React.useState(false);
1272
+
1273
+ // Check lock BEFORE allowing dialog to open (synchronous check)
1274
+ // This prevents React from even trying to open if another dialog is open
1275
+ useEffect(() => {
1276
+ if (!open) {
1277
+ setLockAcquired(false);
1278
+ return;
1279
+ }
1280
+
1281
+ // Synchronously check if we can acquire the lock
1282
+ const acquired = acquireDialogLock(persistenceKey);
1283
+ setLockAcquired(acquired);
1284
+
1285
+ if (!acquired) {
1286
+ // Another dialog is open - prevent this one from opening
1287
+ console.warn('[Dialog] ⚠️ Cannot open - another dialog holds the lock', {
1288
+ persistenceKey,
1289
+ });
1290
+ // Immediately close this dialog's React state
1291
+ onOpenChange?.(false);
1292
+ return;
1293
+ }
1294
+
1295
+ // Lock acquired successfully - dialog can proceed to open
1296
+ }, [open, persistenceKey, onOpenChange]);
1297
+
1298
+ // Synchronously check if we can render (must hold lock if open)
1299
+ const canRender = React.useMemo(() => {
1300
+ if (!open) {
1301
+ return true; // Can always render when closed
1302
+ }
1303
+ if (!persistenceKey) {
1304
+ return true; // Non-persisted dialogs can always render
1305
+ }
1306
+ // Use the lockAcquired state which is set by the effect
1307
+ return lockAcquired;
1308
+ }, [open, persistenceKey, lockAcquired]);
1309
+
663
1310
  return (
664
1311
  <DialogPortal>
665
- {open && (
1312
+ {open && canRender && (
666
1313
  <dialog
667
1314
  ref={mergedRef}
668
1315
  className={cn(
@@ -673,18 +1320,18 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
673
1320
  'm-0 p-0 max-w-none max-h-none w-auto h-auto border-0 bg-transparent outline-none',
674
1321
  // Apply our custom styling
675
1322
  'border bg-background shadow-lg',
676
- // Style native backdrop pseudo-element (Tailwind v4 supports arbitrary variants)
677
- '[&::backdrop]:bg-black/50 [&::backdrop]:animate-in [&::backdrop]:fade-in-0',
1323
+ // Backdrop styling is handled via core.css only
678
1324
  // Only apply size classes if not using smart width
679
1325
  !maxWidth && !maxWidthPercent && sizeClasses[size],
680
1326
  // Auto size gets special handling
681
1327
  size === 'auto' && 'w-fit max-w-[90vw] sm:max-w-[80vw]',
682
- // Layout classes based on scrolling mode
683
- enableScrolling ? 'flex flex-col px-6' : 'grid gap-4 p-6',
1328
+ // Layout classes: use flex when we have height constraints or enableScrolling is true
1329
+ // Flex layout is needed for proper scrolling when height is constrained
1330
+ (enableScrolling || hasHeightConstraint) ? 'flex flex-col px-6' : 'grid gap-4 p-6',
684
1331
  // Full screen handling
685
1332
  size === 'full' && 'sm:left-[50%] sm:top-[50%] sm:translate-x-[-50%] sm:translate-y-[-50%] left-0 top-0 translate-x-0 translate-y-0 h-full rounded-none sm:h-auto sm:rounded-lg',
686
- // Overflow handling for scrolling mode
687
- enableScrolling && 'overflow-hidden',
1333
+ // Overflow handling for scrolling mode or when height is constrained
1334
+ (enableScrolling || hasHeightConstraint) && 'overflow-hidden',
688
1335
  className
689
1336
  )}
690
1337
  style={mergedStyle}
@@ -694,12 +1341,15 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
694
1341
  aria-describedby={descriptionId}
695
1342
  title={title}
696
1343
  aria-description={description}
1344
+ data-persistence-key={persistenceKey && persistOpenState ? persistenceKey : undefined}
697
1345
  {...props}
698
1346
  >
699
- {children}
700
- {showCloseButton && (
701
- <DialogClose />
702
- )}
1347
+ <DialogCloseContext.Provider value={markClosedByUser}>
1348
+ {children}
1349
+ {showCloseButton && (
1350
+ <DialogClose />
1351
+ )}
1352
+ </DialogCloseContext.Provider>
703
1353
  </dialog>
704
1354
  )}
705
1355
  </DialogPortal>
@@ -708,19 +1358,53 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
708
1358
  );
709
1359
  DialogContent.displayName = 'DialogContent';
710
1360
 
1361
+ /**
1362
+ * Props for the DialogClose component
1363
+ * @public
1364
+ */
1365
+ export interface DialogCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
1366
+ /** Whether to merge props with child element instead of rendering a button */
1367
+ asChild?: boolean;
1368
+ }
1369
+
711
1370
  /**
712
1371
  * DialogClose component
713
1372
  * Button to close the dialog
714
1373
  */
715
1374
  const DialogClose = React.forwardRef<HTMLButtonElement, DialogCloseProps>(
716
- ({ className, ...props }, ref) => {
717
- const { onOpenChange } = useDialogContext();
1375
+ ({ className, asChild = false, children, onClick, ...props }, ref) => {
1376
+ // Call all hooks unconditionally at the top level
1377
+ // Hooks must be called in the same order on every render
1378
+ const { onOpenChange, markClosedByUser: contextMarkClosedByUser } = useDialogContext();
1379
+ // Prefer DialogContext markClosedByUser (available to all components), fallback to DialogCloseContext (for backwards compatibility)
1380
+ const dialogCloseContextValue = React.useContext(DialogCloseContext);
1381
+ const markClosedByUser = contextMarkClosedByUser || dialogCloseContextValue;
1382
+
1383
+ const handleClick = useCallback((e: React.MouseEvent<HTMLElement>) => {
1384
+ // Mark dialog as closed by user before calling onOpenChange
1385
+ // This ensures the persisted state is cleared when user clicks close button
1386
+ if (markClosedByUser) {
1387
+ markClosedByUser();
1388
+ }
1389
+
1390
+ onClick?.(e as React.MouseEvent<HTMLButtonElement>);
1391
+ onOpenChange(false);
1392
+ }, [onOpenChange, markClosedByUser, onClick]);
1393
+
1394
+ if (asChild && React.isValidElement(children)) {
1395
+ return React.cloneElement(children as React.ReactElement<any>, {
1396
+ ref,
1397
+ onClick: handleClick,
1398
+ className: cn(className, (children as any).props?.className),
1399
+ ...props,
1400
+ });
1401
+ }
718
1402
 
719
1403
  return (
720
1404
  <button
721
1405
  ref={ref}
722
1406
  type="button"
723
- onClick={() => onOpenChange(false)}
1407
+ onClick={handleClick}
724
1408
  className={cn(
725
1409
  'absolute right-4 top-4 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none',
726
1410
  className
@@ -792,10 +1476,44 @@ const DialogBody = ({
792
1476
 
793
1477
  const hasHtmlContent = Boolean(htmlContent && allowHtml);
794
1478
 
1479
+ // Check if parent dialog has height constraint by checking if it uses flex layout
1480
+ // When dialog has height constraint, it uses flex layout, and DialogBody should
1481
+ // use flex properties to properly participate in the layout and only scroll when constrained
1482
+ const { dialogRef, open } = useDialogContext();
1483
+ const [isInFlexContainer, setIsInFlexContainer] = React.useState(false);
1484
+
1485
+ React.useEffect(() => {
1486
+ if (!open) {
1487
+ setIsInFlexContainer(false);
1488
+ return;
1489
+ }
1490
+
1491
+ // Check if dialog uses flex layout (indicates height constraint)
1492
+ const checkFlexLayout = () => {
1493
+ const dialog = dialogRef?.current;
1494
+ if (!dialog) {
1495
+ setIsInFlexContainer(false);
1496
+ return;
1497
+ }
1498
+ const styles = window.getComputedStyle(dialog);
1499
+ const isFlex = styles.display === 'flex' && styles.flexDirection === 'column';
1500
+ setIsInFlexContainer(isFlex);
1501
+ };
1502
+
1503
+ // Check after a brief delay to ensure styles are applied
1504
+ const timeoutId = setTimeout(checkFlexLayout, 0);
1505
+ checkFlexLayout(); // Also check immediately
1506
+
1507
+ return () => clearTimeout(timeoutId);
1508
+ }, [open, dialogRef]);
1509
+
795
1510
  return (
796
1511
  <main
797
1512
  className={cn(
798
1513
  'overflow-y-auto py-2',
1514
+ // When in a flex container with height constraint, use flex properties
1515
+ // so DialogBody takes up available space and only scrolls when content exceeds it
1516
+ isInFlexContainer && 'flex-1 min-h-0',
799
1517
  className
800
1518
  )}
801
1519
  style={mergedStyle}