@jmruthers/pace-core 0.6.6 → 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/{scripts/audit/audit-dependencies.cjs → audit-tool/00-dependencies.cjs} +12 -13
- 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/cursor-rules/00-standards-overview.mdc +156 -0
- package/cursor-rules/{00-pace-core-compliance.mdc → 01-pace-core-compliance.mdc} +187 -34
- package/cursor-rules/02-project-structure.mdc +37 -5
- package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +125 -11
- package/cursor-rules/04-code-quality.mdc +419 -0
- package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +55 -10
- package/cursor-rules/{09-rbac-compliance.mdc → 06-security-rbac.mdc} +62 -6
- 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/DataTable-7PMH7XN7.js +15 -0
- package/dist/{DataTable-2N_tqbfq.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
- package/dist/{PublicPageProvider-BBH6Vqg7.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +26 -16
- package/dist/{chunk-FENMYN2U.js → chunk-5X4QLXRG.js} +1 -3
- package/dist/{chunk-4T7OBVTU.js → chunk-6F3IILHI.js} +1 -1
- package/dist/{chunk-SD6WQY43.js → chunk-7ILTDCL2.js} +9 -1
- package/dist/{chunk-3QC3KRHK.js → chunk-A3W6LW53.js} +16 -1
- package/dist/{chunk-7TYHROIV.js → chunk-BM4CQ5P3.js} +50 -8
- package/dist/{chunk-2HGJFNAH.js → chunk-FEJLJNWA.js} +1 -15
- package/dist/{chunk-OHIK3MIO.js → chunk-GHYHJTYV.js} +2 -2
- package/dist/{chunk-UIYSCEV7.js → chunk-IUBRCBSY.js} +1 -1
- package/dist/{chunk-LAZMKTTF.js → chunk-JGWDVX64.js} +281 -347
- package/dist/{chunk-MAGBIDNS.js → chunk-L4XMVJKY.js} +2 -2
- package/dist/{chunk-A55DK444.js → chunk-OJ4SKRSV.js} +1 -7
- package/dist/{chunk-ZS5VO5JB.js → chunk-Q7Q7V5NV.js} +406 -451
- package/dist/{chunk-3O3WHILE.js → chunk-VBCS3DUA.js} +236 -60
- package/dist/{chunk-BVP2BCJF.js → chunk-ZKAWKYT4.js} +8 -8
- package/dist/components.d.ts +5 -4
- package/dist/components.js +27 -32
- package/dist/eslint-rules/index.cjs +22 -9
- package/{src/eslint-rules/rules/compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +184 -23
- 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/{rbac.cjs → 06-security-rbac.cjs} +26 -10
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
- package/dist/eslint-rules/rules/08-testing.cjs +94 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +18 -17
- package/dist/rbac/index.js +6 -6
- package/dist/theming/runtime.d.ts +14 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/{types-B-K_5VnO.d.ts → types-DXstZpNI.d.ts} +0 -17
- package/dist/{usePublicRouteParams-COZ28Mvq.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +19 -19
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +8 -8
- package/docs/README.md +1 -1
- package/docs/api/modules.md +47 -31
- package/docs/api-reference/components.md +18 -20
- package/docs/api-reference/hooks.md +80 -80
- 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/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/quick-reference.md +4 -4
- 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 +1 -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/README.md +5 -5
- package/docs/rbac/advanced-patterns.md +6 -6
- package/docs/rbac/api-reference.md +20 -20
- 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 -1
- 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/{00-pace-core-compliance.md → 1-pace-core-compliance-standards.md} +204 -185
- package/docs/standards/{02-project-structure.md → 2-project-structure-standards.md} +11 -47
- package/docs/standards/3-architecture-standards.md +606 -0
- package/docs/standards/4-code-quality-standards.md +728 -0
- package/docs/standards/{08-markup-quality.md → 5-styling-standards.md} +12 -9
- package/docs/standards/{09-rbac-compliance.md → 6-security-rbac-standards.md} +126 -18
- 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 +203 -104
- 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/eslint-config-pace-core.cjs +21 -10
- package/package.json +6 -5
- package/scripts/install-cursor-rules.cjs +11 -243
- package/scripts/install-eslint-config.cjs +284 -0
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +10 -10
- package/src/__tests__/integration/UserProfile.test.tsx +14 -14
- 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/Calendar/Calendar.tsx +201 -47
- package/src/components/ContextSelector/ContextSelector.tsx +137 -153
- 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/DataTableErrorBoundary.tsx +9 -11
- package/src/components/DataTable/components/DataTableLayout.tsx +5 -16
- package/src/components/DataTable/components/EditableRow.tsx +5 -7
- 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/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/index.ts +2 -1
- package/src/components/DataTable/types.ts +0 -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 +7 -8
- package/src/components/Dialog/Dialog.test.tsx +1 -0
- package/src/components/Dialog/Dialog.tsx +25 -8
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
- package/src/components/FileUpload/FileUpload.test.tsx +52 -14
- package/src/components/FileUpload/FileUpload.tsx +112 -130
- package/src/components/Progress/Progress.tsx +2 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
- package/src/components/Select/Select.tsx +86 -77
- package/src/components/Select/types.ts +3 -0
- package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
- package/src/hooks/__tests__/hooks.integration.test.tsx +49 -49
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
- package/src/hooks/public/usePublicEvent.ts +5 -5
- package/src/hooks/public/usePublicEventLogo.ts +5 -5
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/public/usePublicRouteParams.ts +5 -5
- package/src/hooks/useAppConfig.ts +2 -2
- package/src/hooks/useEventTheme.test.ts +7 -7
- package/src/hooks/useEventTheme.ts +1 -4
- package/src/hooks/useFileDisplay.ts +2 -2
- 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/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +10 -10
- 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/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
- package/src/utils/storage/README.md +1 -1
- package/cursor-rules/01-standards-compliance.mdc +0 -285
- package/cursor-rules/04-testing-standards.mdc +0 -270
- package/cursor-rules/05-bug-reports-and-features.mdc +0 -248
- package/cursor-rules/06-code-quality.mdc +0 -311
- package/cursor-rules/07-tech-stack-compliance.mdc +0 -216
- package/cursor-rules/10-error-handling-patterns.mdc +0 -179
- package/cursor-rules/11-performance-optimization.mdc +0 -169
- package/cursor-rules/12-ci-cd-integration.mdc +0 -150
- package/dist/DataTable-LRJL4IRV.js +0 -15
- package/dist/eslint-rules/rules/compliance.cjs +0 -348
- package/dist/eslint-rules/rules/components.cjs +0 -113
- package/dist/eslint-rules/rules/imports.cjs +0 -102
- package/docs/best-practices/README.md +0 -472
- package/docs/best-practices/accessibility.md +0 -604
- 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-standards-compliance.md +0 -188
- package/docs/standards/03-solid-principles.md +0 -39
- package/docs/standards/04-testing-standards.md +0 -36
- package/docs/standards/05-bug-reports-and-features.md +0 -27
- package/docs/standards/06-code-quality.md +0 -34
- package/docs/standards/07-tech-stack-compliance.md +0 -30
- package/docs/standards/10-error-handling-patterns.md +0 -401
- package/docs/standards/11-performance-optimization.md +0 -348
- package/docs/standards/12-ci-cd-integration.md +0 -370
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +0 -192
- package/scripts/audit/audit-compliance.cjs +0 -1295
- package/scripts/audit/audit-components.cjs +0 -260
- package/scripts/audit/audit-rbac.cjs +0 -954
- package/scripts/audit/audit-standards.cjs +0 -1268
- package/scripts/audit/index.cjs +0 -1927
- package/src/components/DataTable/components/DataTableBody.tsx +0 -478
- 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/index.cjs +0 -22
- package/src/eslint-rules/rules/components.cjs +0 -113
- package/src/eslint-rules/rules/imports.cjs +0 -102
- package/src/eslint-rules/rules/rbac.cjs +0 -790
- package/src/eslint-rules/utils/helpers.cjs +0 -42
- package/src/eslint-rules/utils/manifest-loader.cjs +0 -75
|
@@ -123,7 +123,7 @@ describe('[component] FilterRow', () => {
|
|
|
123
123
|
expect(filters.length).toBeGreaterThan(0);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
it('displays
|
|
126
|
+
it('displays non-breaking space for non-filterable columns', () => {
|
|
127
127
|
const columns = [
|
|
128
128
|
columnHelper.accessor('name', {
|
|
129
129
|
header: 'Name',
|
|
@@ -133,9 +133,17 @@ describe('[component] FilterRow', () => {
|
|
|
133
133
|
const table = createTable(columns);
|
|
134
134
|
const visibleColumns = table.getHeaderGroups()[0]?.headers || [];
|
|
135
135
|
|
|
136
|
-
render(<FilterRow table={table} visibleColumns={visibleColumns} />);
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const { container } = render(<FilterRow table={table} visibleColumns={visibleColumns} />);
|
|
137
|
+
|
|
138
|
+
// Check that the cell exists and does not contain a filter component
|
|
139
|
+
const cell = container.querySelector('td');
|
|
140
|
+
expect(cell).toBeInTheDocument();
|
|
141
|
+
// The cell should not contain a ColumnFilter component
|
|
142
|
+
expect(cell?.querySelector('[data-testid="column-filter"]')).not.toBeInTheDocument();
|
|
143
|
+
// The cell should be empty or contain only whitespace (non-breaking space from React Fragment)
|
|
144
|
+
// React Fragment with renders as a non-breaking space, but textContent may normalize it
|
|
145
|
+
// The important thing is that no filter component is rendered
|
|
146
|
+
expect(cell?.children.length).toBe(0);
|
|
139
147
|
});
|
|
140
148
|
|
|
141
149
|
it('renders filter with correct placeholder', () => {
|
|
@@ -18,37 +18,45 @@ describe('[component] LoadingState', () => {
|
|
|
18
18
|
it('renders loading spinner and text', () => {
|
|
19
19
|
render(<LoadingState />);
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// There are two "Loading..." texts (sr-only and visible), use getAllByText
|
|
22
|
+
const loadingTexts = screen.getAllByText('Loading...');
|
|
23
|
+
expect(loadingTexts.length).toBeGreaterThan(0);
|
|
24
|
+
// Check that the visible text is present
|
|
25
|
+
expect(screen.getByText('Loading...', { selector: 'strong' })).toBeInTheDocument();
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
it('renders with centered layout', () => {
|
|
25
29
|
render(<LoadingState />);
|
|
26
30
|
|
|
27
|
-
// Find the
|
|
28
|
-
const
|
|
31
|
+
// Find the container using getByRole for the spinner
|
|
32
|
+
const spinner = screen.getByRole('status');
|
|
33
|
+
const container = spinner.closest('p');
|
|
29
34
|
expect(container).toHaveClass('text-center', 'p-8');
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
it('renders with padding', () => {
|
|
33
38
|
render(<LoadingState />);
|
|
34
39
|
|
|
35
|
-
// Find the
|
|
36
|
-
const
|
|
40
|
+
// Find the container using getByRole for the spinner
|
|
41
|
+
const spinner = screen.getByRole('status');
|
|
42
|
+
const container = spinner.closest('p');
|
|
37
43
|
expect(container).toHaveClass('p-8');
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
it('renders spinner with animation class', () => {
|
|
41
47
|
render(<LoadingState />);
|
|
42
48
|
|
|
43
|
-
const spinner = screen.
|
|
49
|
+
const spinner = screen.getByRole('status');
|
|
44
50
|
expect(spinner).toHaveClass('animate-spin');
|
|
45
51
|
});
|
|
46
52
|
|
|
47
|
-
it('renders
|
|
53
|
+
it('renders grid container with items centered', () => {
|
|
48
54
|
render(<LoadingState />);
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
// Container uses grid, not flex
|
|
57
|
+
const spinner = screen.getByRole('status');
|
|
58
|
+
const container = spinner.closest('p');
|
|
59
|
+
expect(container).toHaveClass('grid', 'place-items-center');
|
|
52
60
|
});
|
|
53
61
|
});
|
|
54
62
|
|
|
@@ -56,15 +64,17 @@ describe('[component] LoadingState', () => {
|
|
|
56
64
|
it('provides aria-live region for loading state', () => {
|
|
57
65
|
render(<LoadingState />);
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
// The spinner has role="status" which provides the aria-live region
|
|
68
|
+
const spinner = screen.getByRole('status');
|
|
69
|
+
expect(spinner).toBeInTheDocument();
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
it('announces loading state to screen readers', () => {
|
|
64
73
|
render(<LoadingState />);
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
// Check that the sr-only text is present for screen readers
|
|
76
|
+
const srOnlyText = screen.getByText('Loading...', { selector: 'span.sr-only' });
|
|
77
|
+
expect(srOnlyText).toBeInTheDocument();
|
|
68
78
|
});
|
|
69
79
|
});
|
|
70
80
|
|
|
@@ -72,49 +82,53 @@ describe('[component] LoadingState', () => {
|
|
|
72
82
|
it('renders spinner before loading text', () => {
|
|
73
83
|
render(<LoadingState />);
|
|
74
84
|
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const text = container?.
|
|
85
|
+
const spinner = screen.getByRole('status');
|
|
86
|
+
const container = spinner.closest('p');
|
|
87
|
+
const text = container?.querySelector('strong');
|
|
78
88
|
|
|
79
89
|
expect(spinner).toBeInTheDocument();
|
|
80
90
|
expect(text).toHaveTextContent('Loading...');
|
|
81
91
|
});
|
|
82
92
|
|
|
83
|
-
it('applies
|
|
93
|
+
it('applies grid layout for centering', () => {
|
|
84
94
|
render(<LoadingState />);
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
// Container uses grid place-items-center, not space-x-2
|
|
97
|
+
const spinner = screen.getByRole('status');
|
|
98
|
+
const container = spinner.closest('p');
|
|
99
|
+
expect(container).toHaveClass('grid', 'place-items-center');
|
|
88
100
|
});
|
|
89
101
|
});
|
|
90
102
|
|
|
91
103
|
describe('Styling', () => {
|
|
92
|
-
it('
|
|
104
|
+
it('renders visible loading text', () => {
|
|
93
105
|
render(<LoadingState />);
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
// Check that the visible text (in strong) is present
|
|
108
|
+
const text = screen.getByText('Loading...', { selector: 'strong' });
|
|
109
|
+
expect(text).toBeInTheDocument();
|
|
97
110
|
});
|
|
98
111
|
|
|
99
112
|
it('spinner has rounded-full class', () => {
|
|
100
113
|
render(<LoadingState />);
|
|
101
114
|
|
|
102
|
-
const spinner = screen.
|
|
115
|
+
const spinner = screen.getByRole('status');
|
|
103
116
|
expect(spinner).toHaveClass('rounded-full');
|
|
104
117
|
});
|
|
105
118
|
|
|
106
119
|
it('spinner has border styling', () => {
|
|
107
120
|
render(<LoadingState />);
|
|
108
121
|
|
|
109
|
-
const spinner = screen.
|
|
110
|
-
|
|
122
|
+
const spinner = screen.getByRole('status');
|
|
123
|
+
// Spinner uses border-2 border-solid border-current border-r-transparent
|
|
124
|
+
expect(spinner).toHaveClass('border-2', 'border-solid', 'border-current', 'border-r-transparent');
|
|
111
125
|
});
|
|
112
126
|
|
|
113
127
|
it('spinner has appropriate size', () => {
|
|
114
128
|
render(<LoadingState />);
|
|
115
129
|
|
|
116
|
-
const spinner = screen.
|
|
117
|
-
// LoadingState spinner uses Tailwind v4 size-* utility
|
|
130
|
+
const spinner = screen.getByRole('status');
|
|
131
|
+
// LoadingState spinner uses Tailwind v4 size-* utility
|
|
118
132
|
expect(spinner).toHaveClass('size-6');
|
|
119
133
|
});
|
|
120
134
|
});
|
|
@@ -7,9 +7,10 @@ export { DataTableToolbar } from './DataTableToolbar';
|
|
|
7
7
|
export { DataTableModals } from './DataTableModals';
|
|
8
8
|
export { ImportModal } from './ImportModal';
|
|
9
9
|
export type { ImportModalConfig } from './ImportModal';
|
|
10
|
-
export { GroupHeader } from './GroupHeader';
|
|
11
10
|
export { GroupingDropdown } from './GroupingDropdown';
|
|
12
11
|
export { DataTableErrorBoundary } from './DataTableErrorBoundary';
|
|
13
12
|
export { PaginationControls } from './PaginationControls';
|
|
14
13
|
export { LoadingState } from './LoadingState';
|
|
15
14
|
export { EmptyState } from './EmptyState';
|
|
15
|
+
export { SortIndicator } from './SortIndicator';
|
|
16
|
+
export type { SortIndicatorProps } from './SortIndicator';
|
|
@@ -86,8 +86,6 @@ export interface HierarchicalConfig {
|
|
|
86
86
|
defaultExpanded?: boolean | string[];
|
|
87
87
|
/** Callback when expanded state changes */
|
|
88
88
|
onExpandedChange?: (expandedIds: string[]) => void;
|
|
89
|
-
/** Custom expand/collapse button component */
|
|
90
|
-
expandButton?: React.ComponentType<ExpandButtonProps>;
|
|
91
89
|
/** Visual indentation for child rows (in pixels) */
|
|
92
90
|
indentSize?: number;
|
|
93
91
|
/** Custom styling for parent rows */
|
|
@@ -96,22 +94,6 @@ export interface HierarchicalConfig {
|
|
|
96
94
|
childRowClassName?: string;
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
/**
|
|
100
|
-
* Props for the expand/collapse button component
|
|
101
|
-
*/
|
|
102
|
-
export interface ExpandButtonProps {
|
|
103
|
-
/** Row ID */
|
|
104
|
-
rowId: string;
|
|
105
|
-
/** Whether the row is currently expanded */
|
|
106
|
-
isExpanded: boolean;
|
|
107
|
-
/** Whether this row has children */
|
|
108
|
-
hasChildren: boolean;
|
|
109
|
-
/** Click handler */
|
|
110
|
-
onClick: () => void;
|
|
111
|
-
/** Additional CSS classes */
|
|
112
|
-
className?: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
97
|
// ============================================================================
|
|
116
98
|
// PERFORMANCE & PAGINATION TYPES
|
|
117
99
|
// ============================================================================
|
|
@@ -224,6 +224,23 @@ export function getAriaSortValue(sortDirection: 'asc' | 'desc' | false): 'ascend
|
|
|
224
224
|
return 'none';
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Get ARIA sort state for a column header.
|
|
229
|
+
* Returns the appropriate aria-sort value or undefined if column is not sortable.
|
|
230
|
+
*
|
|
231
|
+
* @param column - TanStack Table column instance
|
|
232
|
+
* @returns ARIA sort value ('ascending', 'descending', 'none') or undefined if not sortable
|
|
233
|
+
*/
|
|
234
|
+
export function getAriaSortState<TData>(column: { getCanSort?: () => boolean; getIsSorted?: () => 'asc' | 'desc' | false }): 'ascending' | 'descending' | 'none' | undefined {
|
|
235
|
+
const isSortable = column.getCanSort ? column.getCanSort() : false;
|
|
236
|
+
if (!isSortable) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const sortState = column.getIsSorted ? column.getIsSorted() : false;
|
|
241
|
+
return getAriaSortValue(sortState);
|
|
242
|
+
}
|
|
243
|
+
|
|
227
244
|
/**
|
|
228
245
|
* Get accessible row description for screen readers
|
|
229
246
|
* @param rowIndex - The row index (0-based)
|
|
@@ -306,7 +306,8 @@ describe('DatePickerWithTimezone Component', () => {
|
|
|
306
306
|
expect(callArgs.captionLayout).toBe('dropdown');
|
|
307
307
|
expect(callArgs.startMonth).toEqual(new Date(1900, 0));
|
|
308
308
|
expect(callArgs.endMonth).toEqual(new Date(2100, 11));
|
|
309
|
-
|
|
309
|
+
// Calendar component may add additional classes like 'col-span-full'
|
|
310
|
+
expect(callArgs.className).toContain('p-0');
|
|
310
311
|
});
|
|
311
312
|
});
|
|
312
313
|
});
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* Provides a calendar interface with timezone context for date selection.
|
|
9
9
|
*
|
|
10
10
|
* Features:
|
|
11
|
-
* - Calendar date selection
|
|
11
|
+
* - Calendar date selection with dropdown month/year navigation
|
|
12
12
|
* - Timezone display (shows "Local" when matches user timezone)
|
|
13
13
|
* - Optional "Done" button
|
|
14
14
|
* - Accessible date selection
|
|
15
15
|
* - Keyboard navigation support
|
|
16
|
+
* - Wide date range support (1900-2100) via dropdown selectors
|
|
16
17
|
*
|
|
17
18
|
* @example
|
|
18
19
|
* ```tsx
|
|
@@ -93,8 +94,7 @@ export function DatePickerWithTimezone({
|
|
|
93
94
|
const timezoneDisplay = displayTimezone === userTimezone ? 'Local' : displayTimezone;
|
|
94
95
|
|
|
95
96
|
return (
|
|
96
|
-
|
|
97
|
-
<div className="p-3">
|
|
97
|
+
<form className={cn('grid grid-cols-[1fr_auto] gap-2', className)}>
|
|
98
98
|
<Calendar
|
|
99
99
|
mode="single"
|
|
100
100
|
selected={selected}
|
|
@@ -103,24 +103,20 @@ export function DatePickerWithTimezone({
|
|
|
103
103
|
captionLayout="dropdown"
|
|
104
104
|
startMonth={new Date(1900, 0)}
|
|
105
105
|
endMonth={new Date(2100, 11)}
|
|
106
|
-
className="p-0"
|
|
106
|
+
className="p-0 col-span-full"
|
|
107
107
|
/>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
112
|
-
<Clock className="size-4" aria-hidden="true" />
|
|
113
|
-
<span>
|
|
108
|
+
|
|
109
|
+
<label htmlFor="timezone">
|
|
110
|
+
<Clock className="size-4 inline-block mr-2" aria-hidden="true" />
|
|
114
111
|
Timezone: <span aria-label={`Timezone ${timezoneDisplay}`}>{timezoneDisplay}</span>
|
|
115
|
-
|
|
116
|
-
</div>
|
|
112
|
+
</label>
|
|
117
113
|
{onDone && (
|
|
118
|
-
<Button onClick={onDone} size="sm" className="h-8">
|
|
114
|
+
<Button onClick={onDone} size="sm" className="ml-auto h-8">
|
|
119
115
|
Done
|
|
120
116
|
</Button>
|
|
121
117
|
)}
|
|
122
|
-
|
|
123
|
-
</
|
|
118
|
+
|
|
119
|
+
</form>
|
|
124
120
|
);
|
|
125
121
|
}
|
|
126
122
|
|
|
@@ -206,10 +206,9 @@ export function DateTimeField({
|
|
|
206
206
|
const timezoneDisplay = getTimezoneDisplay();
|
|
207
207
|
|
|
208
208
|
return (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</Label>
|
|
209
|
+
|
|
210
|
+
<Label className={cn('space-y-2', className)} htmlFor={fieldId} required={required} helperText={helperText} error={error}>
|
|
211
|
+
{label}
|
|
213
212
|
<Input
|
|
214
213
|
ref={inputRef}
|
|
215
214
|
id={fieldId}
|
|
@@ -221,12 +220,12 @@ export function DateTimeField({
|
|
|
221
220
|
error={!!error}
|
|
222
221
|
className="w-full"
|
|
223
222
|
/>
|
|
224
|
-
{timezoneDisplay &&
|
|
225
|
-
<
|
|
223
|
+
{timezoneDisplay && (
|
|
224
|
+
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none">
|
|
226
225
|
{timezoneDisplay}
|
|
227
|
-
</
|
|
226
|
+
</span>
|
|
228
227
|
)}
|
|
229
|
-
</
|
|
228
|
+
</Label>
|
|
230
229
|
);
|
|
231
230
|
}
|
|
232
231
|
|
|
@@ -13,6 +13,7 @@ import React from 'react';
|
|
|
13
13
|
import { screen, waitFor } from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
16
|
+
import '@testing-library/jest-dom/vitest';
|
|
16
17
|
import {
|
|
17
18
|
Dialog,
|
|
18
19
|
DialogTrigger,
|
|
@@ -210,9 +210,9 @@ export interface DialogContentProps extends React.HTMLAttributes<HTMLDialogEleme
|
|
|
210
210
|
minHeight?: string;
|
|
211
211
|
/** Minimum width in CSS units */
|
|
212
212
|
minWidth?: string;
|
|
213
|
-
/** Dialog title
|
|
213
|
+
/** Dialog title - sets native title attribute and aria-labelledby */
|
|
214
214
|
title?: string;
|
|
215
|
-
/** Dialog description
|
|
215
|
+
/** Dialog description - sets native aria-description attribute */
|
|
216
216
|
description?: string;
|
|
217
217
|
/** Whether to persist open state across tab switches */
|
|
218
218
|
persistOpenState?: boolean;
|
|
@@ -1320,8 +1320,7 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
|
|
|
1320
1320
|
'm-0 p-0 max-w-none max-h-none w-auto h-auto border-0 bg-transparent outline-none',
|
|
1321
1321
|
// Apply our custom styling
|
|
1322
1322
|
'border bg-background shadow-lg',
|
|
1323
|
-
//
|
|
1324
|
-
'[&::backdrop]:bg-black/50 [&::backdrop]:animate-in [&::backdrop]:fade-in-0',
|
|
1323
|
+
// Backdrop styling is handled via core.css only
|
|
1325
1324
|
// Only apply size classes if not using smart width
|
|
1326
1325
|
!maxWidth && !maxWidthPercent && sizeClasses[size],
|
|
1327
1326
|
// Auto size gets special handling
|
|
@@ -1359,12 +1358,21 @@ const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
|
|
|
1359
1358
|
);
|
|
1360
1359
|
DialogContent.displayName = 'DialogContent';
|
|
1361
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
|
+
|
|
1362
1370
|
/**
|
|
1363
1371
|
* DialogClose component
|
|
1364
1372
|
* Button to close the dialog
|
|
1365
1373
|
*/
|
|
1366
1374
|
const DialogClose = React.forwardRef<HTMLButtonElement, DialogCloseProps>(
|
|
1367
|
-
({ className, onClick, ...props }, ref) => {
|
|
1375
|
+
({ className, asChild = false, children, onClick, ...props }, ref) => {
|
|
1368
1376
|
// Call all hooks unconditionally at the top level
|
|
1369
1377
|
// Hooks must be called in the same order on every render
|
|
1370
1378
|
const { onOpenChange, markClosedByUser: contextMarkClosedByUser } = useDialogContext();
|
|
@@ -1372,16 +1380,25 @@ const DialogClose = React.forwardRef<HTMLButtonElement, DialogCloseProps>(
|
|
|
1372
1380
|
const dialogCloseContextValue = React.useContext(DialogCloseContext);
|
|
1373
1381
|
const markClosedByUser = contextMarkClosedByUser || dialogCloseContextValue;
|
|
1374
1382
|
|
|
1375
|
-
const handleClick = (e: React.MouseEvent<
|
|
1383
|
+
const handleClick = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
|
1376
1384
|
// Mark dialog as closed by user before calling onOpenChange
|
|
1377
1385
|
// This ensures the persisted state is cleared when user clicks close button
|
|
1378
1386
|
if (markClosedByUser) {
|
|
1379
1387
|
markClosedByUser();
|
|
1380
1388
|
}
|
|
1381
1389
|
|
|
1382
|
-
onClick?.(e);
|
|
1390
|
+
onClick?.(e as React.MouseEvent<HTMLButtonElement>);
|
|
1383
1391
|
onOpenChange(false);
|
|
1384
|
-
};
|
|
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
|
+
}
|
|
1385
1402
|
|
|
1386
1403
|
return (
|
|
1387
1404
|
<button
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
import React, { Component, ReactNode } from 'react';
|
|
119
119
|
import { performanceBudgetMonitor } from '../../utils/performance/performanceBudgets';
|
|
120
120
|
import { logger } from '../../utils/core/logger';
|
|
121
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../Card/Card';
|
|
121
122
|
|
|
122
123
|
/**
|
|
123
124
|
* State interface for the ErrorBoundary component
|
|
@@ -181,18 +182,18 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
181
182
|
|
|
182
183
|
constructor(props: ErrorBoundaryProps) {
|
|
183
184
|
super(props);
|
|
184
|
-
this.state = {
|
|
185
|
-
hasError: false,
|
|
186
|
-
retryCount: 0
|
|
185
|
+
this.state = {
|
|
186
|
+
hasError: false,
|
|
187
|
+
retryCount: 0
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
191
192
|
const errorId = `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
192
|
-
return {
|
|
193
|
-
hasError: true,
|
|
193
|
+
return {
|
|
194
|
+
hasError: true,
|
|
194
195
|
error,
|
|
195
|
-
errorId
|
|
196
|
+
errorId
|
|
196
197
|
};
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -200,12 +201,12 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
200
201
|
const { componentName = 'Unknown Component', onError, enableReporting = true } = this.props;
|
|
201
202
|
const errorId = this.state.errorId!;
|
|
202
203
|
const componentNameForHandler = componentName || 'Unknown Component';
|
|
203
|
-
|
|
204
|
+
|
|
204
205
|
this.setState({ errorInfo });
|
|
205
|
-
|
|
206
|
+
|
|
206
207
|
// Enhanced logging with component name and error ID
|
|
207
208
|
logger.error('ErrorBoundary', `[${componentNameForHandler}] Caught error ${errorId}:`, error, errorInfo);
|
|
208
|
-
|
|
209
|
+
|
|
209
210
|
// Performance monitoring - track error occurrence
|
|
210
211
|
performanceBudgetMonitor.measure('ERROR_BOUNDARY_TRIGGER', 1, {
|
|
211
212
|
componentName: componentNameForHandler,
|
|
@@ -218,7 +219,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
218
219
|
if (enableReporting) {
|
|
219
220
|
this.reportError(errorId, componentNameForHandler);
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
+
|
|
222
223
|
// Call error handler: prefer prop onError, fall back to global handler from props
|
|
223
224
|
if (onError) {
|
|
224
225
|
onError(error, errorInfo, errorId);
|
|
@@ -242,7 +243,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
242
243
|
|
|
243
244
|
if (retryCount < maxRetries) {
|
|
244
245
|
logger.debug('ErrorBoundary', `Retrying component render (attempt ${retryCount + 1}/${maxRetries})`);
|
|
245
|
-
|
|
246
|
+
|
|
246
247
|
this.setState(prevState => ({
|
|
247
248
|
hasError: false,
|
|
248
249
|
error: undefined,
|
|
@@ -261,11 +262,11 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
261
262
|
|
|
262
263
|
render() {
|
|
263
264
|
if (this.state.hasError) {
|
|
264
|
-
const {
|
|
265
|
-
componentName = 'Component',
|
|
266
|
-
fallback,
|
|
267
|
-
enableRetry = true,
|
|
268
|
-
maxRetries = 3
|
|
265
|
+
const {
|
|
266
|
+
componentName = 'Component',
|
|
267
|
+
fallback,
|
|
268
|
+
enableRetry = true,
|
|
269
|
+
maxRetries = 3
|
|
269
270
|
} = this.props;
|
|
270
271
|
const { retryCount, errorId } = this.state;
|
|
271
272
|
|
|
@@ -276,73 +277,70 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
276
277
|
|
|
277
278
|
// Enhanced error UI with retry functionality
|
|
278
279
|
return (
|
|
279
|
-
<
|
|
280
|
-
role="alert"
|
|
281
|
-
className="
|
|
280
|
+
<Card
|
|
281
|
+
role="alert"
|
|
282
|
+
className="bg-destructive/10 border-destructive/20"
|
|
282
283
|
data-error-boundary={errorId}
|
|
283
284
|
>
|
|
284
|
-
<
|
|
285
|
-
<
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
</button>
|
|
312
|
-
</div>
|
|
313
|
-
)}
|
|
285
|
+
<CardHeader className="flex items-start gap-3">
|
|
286
|
+
<svg className="w-5 h-5 text-destructive flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
|
|
287
|
+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
288
|
+
</svg>
|
|
289
|
+
<CardTitle className="text-destructive">
|
|
290
|
+
Error in {componentName}
|
|
291
|
+
</CardTitle>
|
|
292
|
+
<CardDescription className="text-destructive/80">
|
|
293
|
+
{this.state.error?.message || 'An unexpected error occurred.'}
|
|
294
|
+
</CardDescription>
|
|
295
|
+
</CardHeader>
|
|
296
|
+
|
|
297
|
+
{import.meta.env.MODE === 'development' && this.state.error && (
|
|
298
|
+
<CardContent>
|
|
299
|
+
<details className="text-sm text-destructive/70">
|
|
300
|
+
<summary className="cursor-pointer font-medium mb-2">
|
|
301
|
+
Error Details (Development)
|
|
302
|
+
</summary>
|
|
303
|
+
<pre>Error ID: {errorId}
|
|
304
|
+
<code className="overflow-auto max-h-32">
|
|
305
|
+
{this.state.error.toString()}
|
|
306
|
+
{this.state.errorInfo?.componentStack}
|
|
307
|
+
</code>
|
|
308
|
+
</pre>
|
|
309
|
+
</details>
|
|
310
|
+
</CardContent>
|
|
311
|
+
)}
|
|
314
312
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
313
|
+
{enableRetry && retryCount < maxRetries && (
|
|
314
|
+
<CardFooter className="flex gap-3">
|
|
315
|
+
<button
|
|
316
|
+
onClick={this.handleRetry}
|
|
317
|
+
className="px-4 py-2 bg-destructive text-destructive-foreground rounded-md hover:bg-destructive/90 transition-colors text-sm font-medium"
|
|
318
|
+
>
|
|
319
|
+
Retry ({retryCount + 1}/{maxRetries})
|
|
320
|
+
</button>
|
|
321
|
+
<button
|
|
322
|
+
onClick={() => window.location.reload()}
|
|
323
|
+
className="px-4 py-2 bg-sec-600 text-main-50 rounded-md hover:bg-sec-700 transition-colors text-sm font-medium"
|
|
324
|
+
>
|
|
325
|
+
Reload Page
|
|
326
|
+
</button>
|
|
327
|
+
</CardFooter>
|
|
328
|
+
)}
|
|
328
329
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
</div>
|
|
330
|
+
{retryCount >= maxRetries && (
|
|
331
|
+
<CardFooter className="flex flex-col gap-3">
|
|
332
|
+
<p className="text-acc-800">
|
|
333
|
+
Maximum retry attempts reached. Please reload the page or contact support.
|
|
334
|
+
</p>
|
|
335
|
+
<button
|
|
336
|
+
onClick={() => window.location.reload()}
|
|
337
|
+
className="px-3 py-1 bg-acc-600 text-main-50 rounded text-sm hover:bg-acc-700"
|
|
338
|
+
>
|
|
339
|
+
Reload Page
|
|
340
|
+
</button>
|
|
341
|
+
</CardFooter>
|
|
342
|
+
)}
|
|
343
|
+
</Card>
|
|
346
344
|
);
|
|
347
345
|
}
|
|
348
346
|
|