@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.
Files changed (246) hide show
  1. package/{scripts/audit/audit-dependencies.cjs → audit-tool/00-dependencies.cjs} +12 -13
  2. package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
  3. package/audit-tool/audits/02-project-structure.cjs +255 -0
  4. package/audit-tool/audits/03-architecture.cjs +196 -0
  5. package/audit-tool/audits/04-code-quality.cjs +149 -0
  6. package/audit-tool/audits/05-styling.cjs +224 -0
  7. package/audit-tool/audits/06-security-rbac.cjs +544 -0
  8. package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
  9. package/audit-tool/audits/08-testing-documentation.cjs +202 -0
  10. package/audit-tool/audits/09-operations.cjs +208 -0
  11. package/audit-tool/index.cjs +291 -0
  12. package/audit-tool/utils/code-utils.cjs +218 -0
  13. package/audit-tool/utils/file-utils.cjs +230 -0
  14. package/audit-tool/utils/report-utils.cjs +241 -0
  15. package/cursor-rules/00-standards-overview.mdc +156 -0
  16. package/cursor-rules/{00-pace-core-compliance.mdc → 01-pace-core-compliance.mdc} +187 -34
  17. package/cursor-rules/02-project-structure.mdc +37 -5
  18. package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +125 -11
  19. package/cursor-rules/04-code-quality.mdc +419 -0
  20. package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +55 -10
  21. package/cursor-rules/{09-rbac-compliance.mdc → 06-security-rbac.mdc} +62 -6
  22. package/cursor-rules/07-api-tech-stack.mdc +377 -0
  23. package/cursor-rules/08-testing-documentation.mdc +324 -0
  24. package/cursor-rules/09-operations.mdc +365 -0
  25. package/dist/DataTable-7PMH7XN7.js +15 -0
  26. package/dist/{DataTable-2N_tqbfq.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
  27. package/dist/{PublicPageProvider-BBH6Vqg7.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +26 -16
  28. package/dist/{chunk-FENMYN2U.js → chunk-5X4QLXRG.js} +1 -3
  29. package/dist/{chunk-4T7OBVTU.js → chunk-6F3IILHI.js} +1 -1
  30. package/dist/{chunk-SD6WQY43.js → chunk-7ILTDCL2.js} +9 -1
  31. package/dist/{chunk-3QC3KRHK.js → chunk-A3W6LW53.js} +16 -1
  32. package/dist/{chunk-7TYHROIV.js → chunk-BM4CQ5P3.js} +50 -8
  33. package/dist/{chunk-2HGJFNAH.js → chunk-FEJLJNWA.js} +1 -15
  34. package/dist/{chunk-OHIK3MIO.js → chunk-GHYHJTYV.js} +2 -2
  35. package/dist/{chunk-UIYSCEV7.js → chunk-IUBRCBSY.js} +1 -1
  36. package/dist/{chunk-LAZMKTTF.js → chunk-JGWDVX64.js} +281 -347
  37. package/dist/{chunk-MAGBIDNS.js → chunk-L4XMVJKY.js} +2 -2
  38. package/dist/{chunk-A55DK444.js → chunk-OJ4SKRSV.js} +1 -7
  39. package/dist/{chunk-ZS5VO5JB.js → chunk-Q7Q7V5NV.js} +406 -451
  40. package/dist/{chunk-3O3WHILE.js → chunk-VBCS3DUA.js} +236 -60
  41. package/dist/{chunk-BVP2BCJF.js → chunk-ZKAWKYT4.js} +8 -8
  42. package/dist/components.d.ts +5 -4
  43. package/dist/components.js +27 -32
  44. package/dist/eslint-rules/index.cjs +22 -9
  45. package/{src/eslint-rules/rules/compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +184 -23
  46. package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
  47. package/dist/eslint-rules/rules/05-styling.cjs +61 -0
  48. package/dist/eslint-rules/rules/{rbac.cjs → 06-security-rbac.cjs} +26 -10
  49. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
  50. package/dist/eslint-rules/rules/08-testing.cjs +94 -0
  51. package/dist/hooks.d.ts +5 -5
  52. package/dist/hooks.js +6 -6
  53. package/dist/index.d.ts +6 -6
  54. package/dist/index.js +18 -17
  55. package/dist/rbac/index.js +6 -6
  56. package/dist/theming/runtime.d.ts +14 -1
  57. package/dist/theming/runtime.js +1 -1
  58. package/dist/{types-B-K_5VnO.d.ts → types-DXstZpNI.d.ts} +0 -17
  59. package/dist/{usePublicRouteParams-COZ28Mvq.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +19 -19
  60. package/dist/utils.d.ts +2 -2
  61. package/dist/utils.js +8 -8
  62. package/docs/README.md +1 -1
  63. package/docs/api/modules.md +47 -31
  64. package/docs/api-reference/components.md +18 -20
  65. package/docs/api-reference/hooks.md +80 -80
  66. package/docs/api-reference/types.md +1 -1
  67. package/docs/api-reference/utilities.md +1 -1
  68. package/docs/architecture/README.md +1 -1
  69. package/docs/core-concepts/events.md +3 -3
  70. package/docs/core-concepts/organisations.md +6 -6
  71. package/docs/core-concepts/permissions.md +6 -6
  72. package/docs/documentation-index.md +12 -18
  73. package/docs/getting-started/documentation-index.md +1 -1
  74. package/docs/getting-started/examples/README.md +4 -4
  75. package/docs/getting-started/examples/full-featured-app.md +1 -1
  76. package/docs/getting-started/faq.md +2 -2
  77. package/docs/getting-started/quick-reference.md +4 -4
  78. package/docs/implementation-guides/authentication.md +15 -15
  79. package/docs/implementation-guides/component-styling.md +1 -1
  80. package/docs/implementation-guides/data-tables.md +126 -33
  81. package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
  82. package/docs/implementation-guides/dynamic-colors.md +3 -3
  83. package/docs/implementation-guides/file-upload-storage.md +2 -2
  84. package/docs/implementation-guides/hierarchical-datatable.md +40 -60
  85. package/docs/implementation-guides/inactivity-tracking.md +3 -3
  86. package/docs/implementation-guides/large-datasets.md +3 -2
  87. package/docs/implementation-guides/organisation-security.md +2 -2
  88. package/docs/implementation-guides/performance.md +2 -2
  89. package/docs/implementation-guides/permission-enforcement.md +1 -1
  90. package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
  91. package/docs/migration/V0.4.0_rbac-migration.md +6 -6
  92. package/docs/rbac/README.md +5 -5
  93. package/docs/rbac/advanced-patterns.md +6 -6
  94. package/docs/rbac/api-reference.md +20 -20
  95. package/docs/rbac/event-based-apps.md +3 -3
  96. package/docs/rbac/examples.md +41 -41
  97. package/docs/rbac/getting-started.md +37 -37
  98. package/docs/rbac/performance.md +1 -1
  99. package/docs/rbac/quick-start.md +52 -52
  100. package/docs/rbac/secure-client-protection.md +1 -1
  101. package/docs/rbac/troubleshooting.md +1 -1
  102. package/docs/security/README.md +5 -5
  103. package/docs/standards/0-standards-overview.md +220 -0
  104. package/docs/standards/{00-pace-core-compliance.md → 1-pace-core-compliance-standards.md} +204 -185
  105. package/docs/standards/{02-project-structure.md → 2-project-structure-standards.md} +11 -47
  106. package/docs/standards/3-architecture-standards.md +606 -0
  107. package/docs/standards/4-code-quality-standards.md +728 -0
  108. package/docs/standards/{08-markup-quality.md → 5-styling-standards.md} +12 -9
  109. package/docs/standards/{09-rbac-compliance.md → 6-security-rbac-standards.md} +126 -18
  110. package/docs/standards/7-api-tech-stack-standards.md +662 -0
  111. package/docs/standards/8-testing-documentation-standards.md +401 -0
  112. package/docs/standards/9-operations-standards.md +1102 -0
  113. package/docs/standards/README.md +203 -104
  114. package/docs/troubleshooting/README.md +4 -4
  115. package/docs/troubleshooting/common-issues.md +2 -2
  116. package/docs/troubleshooting/debugging.md +9 -9
  117. package/docs/troubleshooting/migration.md +4 -4
  118. package/eslint-config-pace-core.cjs +21 -10
  119. package/package.json +6 -5
  120. package/scripts/install-cursor-rules.cjs +11 -243
  121. package/scripts/install-eslint-config.cjs +284 -0
  122. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +2 -2
  123. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
  124. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +10 -10
  125. package/src/__tests__/integration/UserProfile.test.tsx +14 -14
  126. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
  127. package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
  128. package/src/__tests__/templates/component.test.template.tsx +18 -15
  129. package/src/components/Calendar/Calendar.tsx +201 -47
  130. package/src/components/ContextSelector/ContextSelector.tsx +137 -153
  131. package/src/components/DataTable/AUDIT_REPORT.md +293 -0
  132. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
  133. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
  134. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
  135. package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
  136. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
  137. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
  138. package/src/components/DataTable/components/DataTableLayout.tsx +5 -16
  139. package/src/components/DataTable/components/EditableRow.tsx +5 -7
  140. package/src/components/DataTable/components/EmptyState.tsx +10 -9
  141. package/src/components/DataTable/components/FilterRow.tsx +2 -4
  142. package/src/components/DataTable/components/ImportModal.tsx +124 -126
  143. package/src/components/DataTable/components/LoadingState.tsx +5 -6
  144. package/src/components/DataTable/components/SortIndicator.tsx +50 -0
  145. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
  146. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
  147. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
  148. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
  149. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
  150. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
  151. package/src/components/DataTable/components/index.ts +2 -1
  152. package/src/components/DataTable/types.ts +0 -18
  153. package/src/components/DataTable/utils/a11yUtils.ts +17 -0
  154. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
  155. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
  156. package/src/components/DateTimeField/DateTimeField.tsx +7 -8
  157. package/src/components/Dialog/Dialog.test.tsx +1 -0
  158. package/src/components/Dialog/Dialog.tsx +25 -8
  159. package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
  160. package/src/components/FileUpload/FileUpload.test.tsx +52 -14
  161. package/src/components/FileUpload/FileUpload.tsx +112 -130
  162. package/src/components/Progress/Progress.tsx +2 -4
  163. package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
  164. package/src/components/Select/Select.tsx +86 -77
  165. package/src/components/Select/types.ts +3 -0
  166. package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
  167. package/src/hooks/__tests__/hooks.integration.test.tsx +49 -49
  168. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
  169. package/src/hooks/public/usePublicEvent.ts +5 -5
  170. package/src/hooks/public/usePublicEventLogo.ts +5 -5
  171. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  172. package/src/hooks/public/usePublicRouteParams.ts +5 -5
  173. package/src/hooks/useAppConfig.ts +2 -2
  174. package/src/hooks/useEventTheme.test.ts +7 -7
  175. package/src/hooks/useEventTheme.ts +1 -4
  176. package/src/hooks/useFileDisplay.ts +2 -2
  177. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
  178. package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
  179. package/src/providers/__tests__/EventProvider.test.tsx +61 -61
  180. package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
  181. package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
  182. package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
  183. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
  184. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
  185. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +10 -10
  186. package/src/styles/core.css +7 -0
  187. package/src/theming/__tests__/parseEventColours.test.ts +9 -3
  188. package/src/theming/parseEventColours.ts +22 -10
  189. package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
  190. package/src/utils/storage/README.md +1 -1
  191. package/cursor-rules/01-standards-compliance.mdc +0 -285
  192. package/cursor-rules/04-testing-standards.mdc +0 -270
  193. package/cursor-rules/05-bug-reports-and-features.mdc +0 -248
  194. package/cursor-rules/06-code-quality.mdc +0 -311
  195. package/cursor-rules/07-tech-stack-compliance.mdc +0 -216
  196. package/cursor-rules/10-error-handling-patterns.mdc +0 -179
  197. package/cursor-rules/11-performance-optimization.mdc +0 -169
  198. package/cursor-rules/12-ci-cd-integration.mdc +0 -150
  199. package/dist/DataTable-LRJL4IRV.js +0 -15
  200. package/dist/eslint-rules/rules/compliance.cjs +0 -348
  201. package/dist/eslint-rules/rules/components.cjs +0 -113
  202. package/dist/eslint-rules/rules/imports.cjs +0 -102
  203. package/docs/best-practices/README.md +0 -472
  204. package/docs/best-practices/accessibility.md +0 -604
  205. package/docs/best-practices/common-patterns.md +0 -516
  206. package/docs/best-practices/deployment.md +0 -1103
  207. package/docs/best-practices/performance.md +0 -1328
  208. package/docs/best-practices/security.md +0 -940
  209. package/docs/best-practices/testing.md +0 -1034
  210. package/docs/rbac/compliance/compliance-guide.md +0 -544
  211. package/docs/standards/01-standards-compliance.md +0 -188
  212. package/docs/standards/03-solid-principles.md +0 -39
  213. package/docs/standards/04-testing-standards.md +0 -36
  214. package/docs/standards/05-bug-reports-and-features.md +0 -27
  215. package/docs/standards/06-code-quality.md +0 -34
  216. package/docs/standards/07-tech-stack-compliance.md +0 -30
  217. package/docs/standards/10-error-handling-patterns.md +0 -401
  218. package/docs/standards/11-performance-optimization.md +0 -348
  219. package/docs/standards/12-ci-cd-integration.md +0 -370
  220. package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +0 -192
  221. package/scripts/audit/audit-compliance.cjs +0 -1295
  222. package/scripts/audit/audit-components.cjs +0 -260
  223. package/scripts/audit/audit-rbac.cjs +0 -954
  224. package/scripts/audit/audit-standards.cjs +0 -1268
  225. package/scripts/audit/index.cjs +0 -1927
  226. package/src/components/DataTable/components/DataTableBody.tsx +0 -478
  227. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
  228. package/src/components/DataTable/components/ExpandButton.tsx +0 -113
  229. package/src/components/DataTable/components/GroupHeader.tsx +0 -54
  230. package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
  231. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
  232. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
  233. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
  234. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
  235. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
  236. package/src/components/DataTable/core/DataTableContext.tsx +0 -216
  237. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
  238. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
  239. package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
  240. package/src/components/DataTable/utils/debugTools.ts +0 -514
  241. package/src/eslint-rules/index.cjs +0 -22
  242. package/src/eslint-rules/rules/components.cjs +0 -113
  243. package/src/eslint-rules/rules/imports.cjs +0 -102
  244. package/src/eslint-rules/rules/rbac.cjs +0 -790
  245. package/src/eslint-rules/utils/helpers.cjs +0 -42
  246. 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 "No filter" for non-filterable columns', () => {
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
- expect(screen.getByText('No filter')).toBeInTheDocument();
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 &nbsp; 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
- expect(screen.getByText('Loading...')).toBeInTheDocument();
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 outer container with text-center and p-8 classes
28
- const container = screen.getByText('Loading...').parentElement?.parentElement;
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 outer container with p-8 class
36
- const container = screen.getByText('Loading...').parentElement?.parentElement;
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.getByText('Loading...').previousElementSibling;
49
+ const spinner = screen.getByRole('status');
44
50
  expect(spinner).toHaveClass('animate-spin');
45
51
  });
46
52
 
47
- it('renders flex container with items centered', () => {
53
+ it('renders grid container with items centered', () => {
48
54
  render(<LoadingState />);
49
55
 
50
- const flexContainer = screen.getByText('Loading...').parentElement;
51
- expect(flexContainer).toHaveClass('flex', 'items-center', 'justify-center');
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
- const loadingText = screen.getByText('Loading...');
60
- expect(loadingText).toHaveAttribute('aria-live', 'polite');
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
- const loadingText = screen.getByText('Loading...');
67
- expect(loadingText).toBeInTheDocument();
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 container = screen.getByText('Loading...').parentElement;
76
- const spinner = container?.firstElementChild;
77
- const text = container?.lastElementChild;
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 space between spinner and text', () => {
93
+ it('applies grid layout for centering', () => {
84
94
  render(<LoadingState />);
85
95
 
86
- const container = screen.getByText('Loading...').parentElement;
87
- expect(container).toHaveClass('space-x-2');
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('applies muted foreground color to text', () => {
104
+ it('renders visible loading text', () => {
93
105
  render(<LoadingState />);
94
106
 
95
- const text = screen.getByText('Loading...');
96
- expect(text).toHaveClass('text-muted-foreground');
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.getByText('Loading...').previousElementSibling;
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.getByText('Loading...').previousElementSibling;
110
- expect(spinner).toHaveClass('border-b-2', 'border-primary');
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.getByText('Loading...').previousElementSibling;
117
- // LoadingState spinner uses Tailwind v4 size-* utility instead of h-* w-*
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
- expect(callArgs.className).toBe('p-0');
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
- <div className={cn('flex flex-col', className)}>
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
- </div>
109
-
110
- <div className="flex items-center justify-between border-t border-border px-3 py-2">
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
- </span>
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
- </div>
123
- </div>
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
- <div className={cn('space-y-2', className)}>
210
- <Label htmlFor={fieldId} required={required} helperText={helperText} error={error}>
211
- {label}
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 && !error && (
225
- <p className="text-sm text-muted-foreground">
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
- </p>
226
+ </span>
228
227
  )}
229
- </div>
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 for accessibility (sets native title attribute) */
213
+ /** Dialog title - sets native title attribute and aria-labelledby */
214
214
  title?: string;
215
- /** Dialog description for accessibility (sets aria-description attribute) */
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
- // Style native backdrop pseudo-element (Tailwind v4 supports arbitrary variants)
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<HTMLButtonElement>) => {
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
- <div
280
- role="alert"
281
- className="p-6 bg-destructive/10 border border-destructive/20 rounded-lg"
280
+ <Card
281
+ role="alert"
282
+ className="bg-destructive/10 border-destructive/20"
282
283
  data-error-boundary={errorId}
283
284
  >
284
- <div className="flex items-start gap-3">
285
- <div className="flex-shrink-0">
286
- <svg className="w-5 h-5 text-destructive" 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
- </div>
290
- <div className="flex-1 min-w-0">
291
- <h3 className="text-destructive">
292
- Error in {componentName}
293
- </h3>
294
- <p className="text-destructive/80">
295
- {this.state.error?.message || 'An unexpected error occurred.'}
296
- </p>
297
-
298
- {enableRetry && retryCount < maxRetries && (
299
- <div className="flex gap-3 mb-4">
300
- <button
301
- onClick={this.handleRetry}
302
- className="px-4 py-2 bg-destructive text-destructive-foreground rounded-md hover:bg-destructive/90 transition-colors text-sm font-medium"
303
- >
304
- Retry ({retryCount + 1}/{maxRetries})
305
- </button>
306
- <button
307
- onClick={() => window.location.reload()}
308
- className="px-4 py-2 bg-sec-600 text-main-50 rounded-md hover:bg-sec-700 transition-colors text-sm font-medium"
309
- >
310
- Reload Page
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
- {retryCount >= maxRetries && (
316
- <div className="mb-4 p-3 bg-acc-50 border border-acc-200 rounded-md">
317
- <p className="text-acc-800">
318
- Maximum retry attempts reached. Please reload the page or contact support.
319
- </p>
320
- <button
321
- onClick={() => window.location.reload()}
322
- className="mt-2 px-3 py-1 bg-acc-600 text-main-50 rounded text-sm hover:bg-acc-700"
323
- >
324
- Reload Page
325
- </button>
326
- </div>
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
- {import.meta.env.MODE === 'development' && this.state.error && (
330
- <details className="text-sm text-destructive/70">
331
- <summary className="cursor-pointer font-medium mb-2">
332
- Error Details (Development)
333
- </summary>
334
- <div className="bg-destructive/5 p-3 rounded border">
335
- <p className="font-mono">Error ID: {errorId}</p>
336
- <pre className="whitespace-pre-wrap text-xs overflow-auto max-h-32">
337
- {this.state.error.toString()}
338
- {this.state.errorInfo?.componentStack}
339
- </pre>
340
- </div>
341
- </details>
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