@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.
- package/CHANGELOG.md +104 -0
- package/README.md +5 -403
- package/audit-tool/00-dependencies.cjs +394 -0
- package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
- package/audit-tool/audits/02-project-structure.cjs +255 -0
- package/audit-tool/audits/03-architecture.cjs +196 -0
- package/audit-tool/audits/04-code-quality.cjs +149 -0
- package/audit-tool/audits/05-styling.cjs +224 -0
- package/audit-tool/audits/06-security-rbac.cjs +544 -0
- package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
- package/audit-tool/audits/08-testing-documentation.cjs +202 -0
- package/audit-tool/audits/09-operations.cjs +208 -0
- package/audit-tool/index.cjs +291 -0
- package/audit-tool/utils/code-utils.cjs +218 -0
- package/audit-tool/utils/file-utils.cjs +230 -0
- package/audit-tool/utils/report-utils.cjs +241 -0
- package/core-usage-manifest.json +93 -0
- package/cursor-rules/00-standards-overview.mdc +156 -0
- package/cursor-rules/01-pace-core-compliance.mdc +586 -0
- package/cursor-rules/02-project-structure.mdc +42 -4
- package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +126 -10
- package/cursor-rules/04-code-quality.mdc +419 -0
- package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +104 -34
- package/cursor-rules/06-security-rbac.mdc +518 -0
- package/cursor-rules/07-api-tech-stack.mdc +377 -0
- package/cursor-rules/08-testing-documentation.mdc +324 -0
- package/cursor-rules/09-operations.mdc +365 -0
- package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
- package/dist/DataTable-7PMH7XN7.js +15 -0
- package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
- package/dist/{PublicPageProvider-QTFVrL-Z.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +33 -72
- package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
- package/dist/api-Y4MQWOFW.js +4 -0
- package/dist/audit-MYQXYZFU.js +3 -0
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
- package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
- package/dist/{chunk-UPPMRMYG.js → chunk-5X4QLXRG.js} +73 -151
- package/dist/chunk-6F3IILHI.js +62 -0
- package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
- package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
- package/dist/{chunk-FMUCXFII.js → chunk-7ILTDCL2.js} +9 -5
- package/dist/{chunk-M43Y4SSO.js → chunk-A3W6LW53.js} +15 -13
- package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
- package/dist/{chunk-HU2C6SSC.js → chunk-BM4CQ5P3.js} +606 -559
- package/dist/chunk-C7NSAPTL.js +1 -0
- package/dist/{chunk-J36DSWQK.js → chunk-FEJLJNWA.js} +7 -41
- package/dist/{chunk-IHB5DR3H.js → chunk-FTCRZOG2.js} +188 -387
- package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
- package/dist/chunk-GHYHJTYV.js +994 -0
- package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
- package/dist/{chunk-FFQEQTNW.js → chunk-IUBRCBSY.js} +134 -45
- package/dist/{chunk-6COVEUS7.js → chunk-JGWDVX64.js} +983 -1034
- package/dist/{chunk-RGAWHO7N.js → chunk-L4XMVJKY.js} +77 -222
- package/dist/chunk-MBADTM7L.js +64 -0
- package/dist/{chunk-M7MPQISP.js → chunk-OJ4SKRSV.js} +3 -16
- package/dist/{chunk-IVOFDYWT.js → chunk-Q7Q7V5NV.js} +2109 -1604
- package/dist/{chunk-JGRYX5UX.js → chunk-S7DKJPLT.js} +29 -58
- package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
- package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
- package/dist/{chunk-NTM7ZSB6.js → chunk-VBCS3DUA.js} +261 -168
- package/dist/{chunk-EFN2EIMK.js → chunk-ZFYPMX46.js} +271 -87
- package/dist/{chunk-L4OXEN46.js → chunk-ZKAWKYT4.js} +10 -24
- package/dist/components.d.ts +7 -5
- package/dist/components.js +46 -257
- package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
- package/dist/eslint-rules/index.cjs +35 -0
- package/{src/eslint-rules/pace-core-compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +234 -235
- package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
- package/dist/eslint-rules/rules/05-styling.cjs +61 -0
- package/dist/eslint-rules/rules/06-security-rbac.cjs +806 -0
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
- package/dist/eslint-rules/rules/08-testing.cjs +94 -0
- package/dist/eslint-rules/utils/helpers.cjs +42 -0
- package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/dist/hooks.d.ts +6 -6
- package/dist/hooks.js +62 -172
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +12 -11
- package/dist/index.js +67 -660
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +8 -35
- package/dist/rbac/eslint-rules.d.ts +46 -44
- package/dist/rbac/eslint-rules.js +7 -4
- package/dist/rbac/index.d.ts +109 -586
- package/dist/rbac/index.js +14 -207
- package/dist/styles/index.js +2 -12
- package/dist/theming/runtime.d.ts +14 -1
- package/dist/theming/runtime.js +3 -19
- package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
- package/dist/{types-CkbwOr4Y.d.ts → types-DXstZpNI.d.ts} +4 -17
- package/dist/types-t9H8qKRw.d.ts +55 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +7 -94
- package/dist/{usePublicRouteParams-ClnV4tnv.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +20 -20
- package/dist/utils.d.ts +24 -117
- package/dist/utils.js +54 -392
- package/docs/README.md +17 -7
- package/docs/api/README.md +4 -402
- package/docs/api/modules.md +301 -871
- package/docs/api-reference/components.md +21 -21
- package/docs/api-reference/deprecated.md +31 -6
- package/docs/api-reference/hooks.md +80 -80
- package/docs/api-reference/rpc-functions.md +78 -3
- package/docs/api-reference/types.md +1 -1
- package/docs/api-reference/utilities.md +1 -1
- package/docs/architecture/README.md +1 -1
- package/docs/core-concepts/events.md +3 -3
- package/docs/core-concepts/organisations.md +6 -6
- package/docs/core-concepts/permissions.md +6 -6
- package/docs/documentation-index.md +12 -18
- package/docs/getting-started/cursor-rules.md +3 -23
- package/docs/getting-started/dependencies.md +650 -0
- package/docs/getting-started/documentation-index.md +1 -1
- package/docs/getting-started/examples/README.md +4 -4
- package/docs/getting-started/examples/full-featured-app.md +1 -1
- package/docs/getting-started/faq.md +2 -2
- package/docs/getting-started/installation-guide.md +20 -7
- package/docs/getting-started/quick-reference.md +4 -4
- package/docs/getting-started/quick-start.md +23 -12
- package/docs/implementation-guides/authentication.md +15 -15
- package/docs/implementation-guides/component-styling.md +1 -1
- package/docs/implementation-guides/data-tables.md +126 -33
- package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
- package/docs/implementation-guides/dynamic-colors.md +3 -3
- package/docs/implementation-guides/file-upload-storage.md +2 -2
- package/docs/implementation-guides/hierarchical-datatable.md +40 -60
- package/docs/implementation-guides/inactivity-tracking.md +3 -3
- package/docs/implementation-guides/large-datasets.md +3 -2
- package/docs/implementation-guides/organisation-security.md +2 -2
- package/docs/implementation-guides/performance.md +2 -2
- package/docs/implementation-guides/permission-enforcement.md +5 -1
- package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
- package/docs/migration/V0.4.0_rbac-migration.md +6 -6
- package/docs/rbac/MIGRATION_GUIDE.md +819 -0
- package/docs/rbac/RBAC_CONTRACT.md +724 -0
- package/docs/rbac/README.md +17 -8
- package/docs/rbac/advanced-patterns.md +6 -6
- package/docs/rbac/api-reference.md +20 -20
- package/docs/rbac/edge-functions-guide.md +376 -0
- package/docs/rbac/event-based-apps.md +3 -3
- package/docs/rbac/examples.md +41 -41
- package/docs/rbac/getting-started.md +37 -37
- package/docs/rbac/performance.md +1 -1
- package/docs/rbac/quick-start.md +52 -52
- package/docs/rbac/secure-client-protection.md +1 -35
- package/docs/rbac/troubleshooting.md +1 -1
- package/docs/security/README.md +5 -5
- package/docs/standards/0-standards-overview.md +220 -0
- package/docs/standards/1-pace-core-compliance-standards.md +986 -0
- package/docs/standards/2-project-structure-standards.md +949 -0
- package/docs/standards/3-architecture-standards.md +606 -0
- package/docs/standards/4-code-quality-standards.md +728 -0
- package/docs/standards/5-styling-standards.md +348 -0
- package/docs/standards/{07-rbac-and-rls-standard.md → 6-security-rbac-standards.md} +269 -66
- package/docs/standards/7-api-tech-stack-standards.md +662 -0
- package/docs/standards/8-testing-documentation-standards.md +401 -0
- package/docs/standards/9-operations-standards.md +1102 -0
- package/docs/standards/README.md +185 -57
- package/docs/troubleshooting/README.md +4 -4
- package/docs/troubleshooting/common-issues.md +2 -2
- package/docs/troubleshooting/debugging.md +9 -9
- package/docs/troubleshooting/migration.md +4 -4
- package/docs/troubleshooting/organisation-context-setup.md +42 -19
- package/eslint-config-pace-core.cjs +33 -6
- package/package.json +35 -23
- package/scripts/install-cursor-rules.cjs +25 -6
- package/scripts/install-eslint-config.cjs +284 -0
- package/src/__tests__/fixtures/supabase.ts +1 -1
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +3 -3
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +13 -13
- package/src/__tests__/helpers/component-test-utils.tsx +1 -1
- package/src/__tests__/helpers/supabaseMock.ts +2 -2
- package/src/__tests__/integration/UserProfile.test.tsx +14 -14
- package/src/__tests__/public-recipe-view.test.ts +38 -9
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
- package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
- package/src/__tests__/templates/component.test.template.tsx +18 -15
- package/src/components/Button/Button.tsx +5 -1
- package/src/components/Calendar/Calendar.tsx +201 -47
- package/src/components/ContextSelector/ContextSelector.tsx +106 -119
- package/src/components/DataTable/AUDIT_REPORT.md +293 -0
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
- package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
- package/src/components/DataTable/components/DataTableCore.tsx +186 -13
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
- package/src/components/DataTable/components/DataTableLayout.tsx +35 -21
- package/src/components/DataTable/components/EditFields.tsx +23 -3
- package/src/components/DataTable/components/EditableRow.tsx +12 -9
- package/src/components/DataTable/components/EmptyState.tsx +10 -9
- package/src/components/DataTable/components/FilterRow.tsx +2 -4
- package/src/components/DataTable/components/ImportModal.tsx +124 -126
- package/src/components/DataTable/components/LoadingState.tsx +5 -6
- package/src/components/DataTable/components/RowComponent.tsx +12 -0
- package/src/components/DataTable/components/SortIndicator.tsx +50 -0
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
- package/src/components/DataTable/components/index.ts +2 -1
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
- package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
- package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
- package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
- package/src/components/DataTable/types.ts +5 -18
- package/src/components/DataTable/utils/a11yUtils.ts +17 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
- package/src/components/DateTimeField/DateTimeField.tsx +10 -9
- package/src/components/Dialog/Dialog.test.tsx +128 -104
- package/src/components/Dialog/Dialog.tsx +742 -24
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
- package/src/components/FileDisplay/FileDisplay.test.tsx +4 -2
- package/src/components/FileDisplay/FileDisplay.tsx +23 -17
- package/src/components/FileUpload/FileUpload.test.tsx +52 -14
- package/src/components/FileUpload/FileUpload.tsx +112 -130
- package/src/components/Form/Form.test.tsx +6 -8
- package/src/components/Form/Form.tsx +365 -4
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
- package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +11 -15
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
- package/src/components/Progress/Progress.tsx +2 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
- package/src/components/Select/Select.tsx +109 -98
- package/src/components/Select/types.ts +4 -1
- package/src/components/UserMenu/UserMenu.tsx +9 -6
- package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
- package/src/hooks/__tests__/hooks.integration.test.tsx +55 -57
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
- package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
- package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
- package/src/hooks/public/usePublicEvent.ts +67 -195
- package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
- package/src/hooks/public/usePublicEventLogo.ts +24 -14
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/public/usePublicRouteParams.ts +5 -5
- package/src/hooks/useAppConfig.ts +28 -26
- package/src/hooks/useEventTheme.test.ts +217 -239
- package/src/hooks/useEventTheme.ts +16 -28
- package/src/hooks/useFileDisplay.ts +2 -2
- package/src/hooks/useOrganisationPermissions.ts +5 -7
- package/src/hooks/useQueryCache.ts +0 -1
- package/src/hooks/useSessionDraft.ts +380 -0
- package/src/hooks/useSessionRestoration.ts +3 -1
- package/src/icons/index.ts +27 -0
- package/src/index.ts +5 -0
- package/src/providers/OrganisationProvider.tsx +23 -14
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
- package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
- package/src/providers/__tests__/EventProvider.test.tsx +61 -61
- package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
- package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
- package/src/providers/services/EventServiceProvider.tsx +1 -24
- package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +13 -10
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
- package/src/rbac/adapters.tsx +7 -295
- package/src/rbac/api.test.ts +44 -56
- package/src/rbac/api.ts +10 -17
- package/src/rbac/cache-invalidation.ts +0 -1
- package/src/rbac/compliance/index.ts +10 -0
- package/src/rbac/compliance/pattern-detector.ts +553 -0
- package/src/rbac/compliance/runtime-compliance.ts +22 -0
- package/src/rbac/components/AccessDenied.tsx +150 -0
- package/src/rbac/components/NavigationGuard.tsx +12 -20
- package/src/rbac/components/PagePermissionGuard.tsx +4 -24
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
- package/src/rbac/components/index.ts +3 -41
- package/src/rbac/eslint-rules.js +1 -1
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/permissions/index.ts +0 -3
- package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
- package/src/rbac/hooks/usePermissions.ts +0 -3
- package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
- package/src/rbac/hooks/useResolvedScope.ts +58 -140
- package/src/rbac/hooks/useResourcePermissions.test.ts +124 -38
- package/src/rbac/hooks/useResourcePermissions.ts +139 -48
- package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
- package/src/rbac/hooks/useRoleManagement.ts +147 -19
- package/src/rbac/hooks/useSecureSupabase.ts +4 -8
- package/src/rbac/index.ts +7 -9
- package/src/rbac/utils/contextValidator.ts +9 -7
- package/src/services/AuthService.ts +130 -18
- package/src/services/EventService.ts +4 -97
- package/src/services/InactivityService.ts +16 -0
- package/src/services/OrganisationService.ts +7 -44
- package/src/services/__tests__/OrganisationService.test.ts +26 -8
- package/src/services/base/BaseService.ts +0 -3
- package/src/styles/core.css +7 -0
- package/src/theming/__tests__/parseEventColours.test.ts +9 -3
- package/src/theming/parseEventColours.ts +22 -10
- package/src/types/database.generated.ts +4733 -3809
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
- package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
- package/src/utils/context/organisationContext.test.ts +13 -28
- package/src/utils/context/organisationContext.ts +21 -52
- package/src/utils/dynamic/dynamicUtils.ts +1 -1
- package/src/utils/file-reference/index.ts +39 -15
- package/src/utils/formatting/formatDateTime.test.ts +3 -2
- package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
- package/src/utils/index.ts +4 -1
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
- package/src/utils/persistence/keyDerivation.ts +304 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
- package/src/utils/security/secureStorage.ts +5 -5
- package/src/utils/storage/README.md +1 -1
- package/src/utils/storage/helpers.ts +3 -3
- package/src/utils/supabase/createBaseClient.ts +147 -0
- package/src/utils/timezone/timezone.test.ts +1 -2
- package/src/utils/timezone/timezone.ts +1 -1
- package/src/utils/validation/csrf.ts +4 -4
- package/cursor-rules/00-pace-core-compliance.mdc +0 -331
- package/cursor-rules/01-standards-compliance.mdc +0 -244
- package/cursor-rules/04-testing-standards.mdc +0 -268
- package/cursor-rules/05-bug-reports-and-features.mdc +0 -246
- package/cursor-rules/06-code-quality.mdc +0 -309
- package/cursor-rules/07-tech-stack-compliance.mdc +0 -214
- package/cursor-rules/CHANGELOG.md +0 -119
- package/cursor-rules/README.md +0 -192
- package/dist/DataTable-AOVNCPTX.js +0 -175
- package/dist/DataTable-AOVNCPTX.js.map +0 -1
- package/dist/UnifiedAuthProvider-4SBX4LU5.js +0 -18
- package/dist/UnifiedAuthProvider-4SBX4LU5.js.map +0 -1
- package/dist/api-O6HTBX5Y.js +0 -52
- package/dist/api-O6HTBX5Y.js.map +0 -1
- package/dist/audit-V53FV5AG.js +0 -17
- package/dist/audit-V53FV5AG.js.map +0 -1
- package/dist/chunk-5DRSZLL2.js.map +0 -1
- package/dist/chunk-63FOKYGO.js.map +0 -1
- package/dist/chunk-6COVEUS7.js.map +0 -1
- package/dist/chunk-AFVQODI2.js +0 -263
- package/dist/chunk-AFVQODI2.js.map +0 -1
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-E66EQZE6.js.map +0 -1
- package/dist/chunk-EFN2EIMK.js.map +0 -1
- package/dist/chunk-FFQEQTNW.js.map +0 -1
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-G37KK66H.js.map +0 -1
- package/dist/chunk-G7QEZTYQ.js +0 -2053
- package/dist/chunk-G7QEZTYQ.js.map +0 -1
- package/dist/chunk-HU2C6SSC.js.map +0 -1
- package/dist/chunk-IHB5DR3H.js.map +0 -1
- package/dist/chunk-IVOFDYWT.js.map +0 -1
- package/dist/chunk-J36DSWQK.js.map +0 -1
- package/dist/chunk-JGRYX5UX.js.map +0 -1
- package/dist/chunk-KQCRWDSA.js +0 -1
- package/dist/chunk-KQCRWDSA.js.map +0 -1
- package/dist/chunk-L4OXEN46.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-M43Y4SSO.js.map +0 -1
- package/dist/chunk-M7MPQISP.js.map +0 -1
- package/dist/chunk-NTM7ZSB6.js.map +0 -1
- package/dist/chunk-PWLANIRT.js.map +0 -1
- package/dist/chunk-QXHPKYJV.js.map +0 -1
- package/dist/chunk-RGAWHO7N.js.map +0 -1
- package/dist/chunk-UPPMRMYG.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js.map +0 -1
- package/dist/chunk-ZSAAAMVR.js.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/contextValidator-5OGXSPKS.js +0 -9
- package/dist/contextValidator-5OGXSPKS.js.map +0 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
- package/dist/hooks.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers.js.map +0 -1
- package/dist/rbac/eslint-rules.js.map +0 -1
- package/dist/rbac/index.js.map +0 -1
- package/dist/styles/index.js.map +0 -1
- package/dist/theming/runtime.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/docs/best-practices/README.md +0 -472
- package/docs/best-practices/accessibility.md +0 -601
- package/docs/best-practices/common-patterns.md +0 -516
- package/docs/best-practices/deployment.md +0 -1103
- package/docs/best-practices/performance.md +0 -1328
- package/docs/best-practices/security.md +0 -940
- package/docs/best-practices/testing.md +0 -1034
- package/docs/rbac/compliance/compliance-guide.md +0 -544
- package/docs/standards/01-architecture-standard.md +0 -44
- package/docs/standards/02-api-and-rpc-standard.md +0 -39
- package/docs/standards/03-component-standard.md +0 -32
- package/docs/standards/04-code-style-standard.md +0 -32
- package/docs/standards/05-security-standard.md +0 -44
- package/docs/standards/06-testing-and-docs-standard.md +0 -29
- package/docs/standards/pace-core-compliance.md +0 -432
- package/scripts/audit/core/checks/accessibility.cjs +0 -197
- package/scripts/audit/core/checks/api-usage.cjs +0 -191
- package/scripts/audit/core/checks/bundle.cjs +0 -142
- package/scripts/audit/core/checks/compliance.cjs +0 -2706
- package/scripts/audit/core/checks/config.cjs +0 -54
- package/scripts/audit/core/checks/coverage.cjs +0 -84
- package/scripts/audit/core/checks/dependencies.cjs +0 -994
- package/scripts/audit/core/checks/documentation.cjs +0 -268
- package/scripts/audit/core/checks/environment.cjs +0 -116
- package/scripts/audit/core/checks/error-handling.cjs +0 -340
- package/scripts/audit/core/checks/forms.cjs +0 -172
- package/scripts/audit/core/checks/heuristics.cjs +0 -68
- package/scripts/audit/core/checks/hooks.cjs +0 -334
- package/scripts/audit/core/checks/imports.cjs +0 -244
- package/scripts/audit/core/checks/performance.cjs +0 -325
- package/scripts/audit/core/checks/routes.cjs +0 -117
- package/scripts/audit/core/checks/state.cjs +0 -130
- package/scripts/audit/core/checks/structure.cjs +0 -65
- package/scripts/audit/core/checks/style.cjs +0 -584
- package/scripts/audit/core/checks/testing.cjs +0 -122
- package/scripts/audit/core/checks/typescript.cjs +0 -61
- package/scripts/audit/core/scanner.cjs +0 -199
- package/scripts/audit/core/utils.cjs +0 -137
- package/scripts/audit/index.cjs +0 -223
- package/scripts/audit/reporters/console.cjs +0 -151
- package/scripts/audit/reporters/json.cjs +0 -54
- package/scripts/audit/reporters/markdown.cjs +0 -124
- package/scripts/audit-consuming-app.cjs +0 -86
- package/src/components/DataTable/components/DataTableBody.tsx +0 -454
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
- package/src/components/DataTable/components/ExpandButton.tsx +0 -113
- package/src/components/DataTable/components/GroupHeader.tsx +0 -54
- package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
- package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
- package/src/components/DataTable/core/DataTableContext.tsx +0 -216
- package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
- package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
- package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
- package/src/components/DataTable/utils/debugTools.ts +0 -514
- package/src/eslint-rules/pace-core-compliance.js +0 -638
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
- package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
- package/src/rbac/components/NavigationProvider.test.tsx +0 -481
- package/src/rbac/components/NavigationProvider.tsx +0 -345
- package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
- package/src/rbac/components/PagePermissionProvider.tsx +0 -279
- package/src/rbac/components/PermissionEnforcer.tsx +0 -312
- package/src/rbac/components/RoleBasedRouter.tsx +0 -440
- package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
- package/src/rbac/components/SecureDataProvider.tsx +0 -339
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
- package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
- 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
|
|
213
|
+
/** Dialog title - sets native title attribute and aria-labelledby */
|
|
208
214
|
title?: string;
|
|
209
|
-
/** Dialog description
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
//
|
|
616
|
-
|
|
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
|
-
//
|
|
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
|
|
683
|
-
|
|
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
|
-
{
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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={
|
|
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}
|