@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
@@ -116,17 +116,19 @@ describe('[component] FileUpload', () => {
116
116
  describe('Drag and Drop', () => {
117
117
  it('shows drag state message on dragover', () => {
118
118
  renderWithProviders(<FileUpload {...baseProps} />);
119
- const dropArea = screen.getByText(/Click to upload/i).closest('div') as HTMLElement;
119
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
120
120
 
121
121
  fireEvent.dragOver(dropArea);
122
+ // Component shows "Drop files here..." (with ellipsis)
122
123
  expect(screen.getByText(/Drop files here/i)).toBeInTheDocument();
123
124
  });
124
125
 
125
126
  it('resets drag state on dragleave', () => {
126
127
  renderWithProviders(<FileUpload {...baseProps} />);
127
- const dropArea = screen.getByText(/Click to upload/i).closest('div') as HTMLElement;
128
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
128
129
 
129
130
  fireEvent.dragOver(dropArea);
131
+ // Component shows "Drop files here..." (with ellipsis)
130
132
  expect(screen.getByText(/Drop files here/i)).toBeInTheDocument();
131
133
 
132
134
  fireEvent.dragLeave(dropArea);
@@ -135,24 +137,41 @@ describe('[component] FileUpload', () => {
135
137
 
136
138
  it('handles file drop', async () => {
137
139
  const onUploadSuccess = vi.fn();
140
+ // Ensure uploadFile mock resolves immediately
141
+ const mockUpload = vi.fn(async () => createMockUploadResult());
142
+ mockUseFileReference.mockReturnValue({
143
+ uploadFile: mockUpload,
144
+ isLoading: false,
145
+ error: null,
146
+ });
147
+
138
148
  renderWithProviders(
139
149
  <FileUpload {...baseProps} onUploadSuccess={onUploadSuccess} showProgress />
140
150
  );
141
151
 
142
- const dropArea = screen.getByText(/Click to upload/i).closest('div') as HTMLElement;
152
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
143
153
  const file = createTestFile('test.png', 'image/png');
144
154
 
155
+ // Use fireEvent.drop with proper dataTransfer mock
145
156
  await act(async () => {
146
157
  fireEvent.drop(dropArea, {
147
158
  dataTransfer: {
148
159
  files: [file],
149
- },
160
+ items: [{
161
+ kind: 'file',
162
+ type: file.type,
163
+ getAsFile: () => file
164
+ }],
165
+ types: ['Files']
166
+ }
150
167
  });
151
168
  });
152
169
 
170
+ // Wait for upload to complete - the uploadFile is called asynchronously
153
171
  await waitFor(() => {
172
+ expect(mockUpload).toHaveBeenCalled();
154
173
  expect(onUploadSuccess).toHaveBeenCalled();
155
- });
174
+ }, { timeout: 3000 });
156
175
  });
157
176
 
158
177
  it('does not handle drop when disabled', () => {
@@ -204,7 +223,7 @@ describe('[component] FileUpload', () => {
204
223
 
205
224
  it('triggers file input click when drop area is clicked', () => {
206
225
  renderWithProviders(<FileUpload {...baseProps} />);
207
- const dropArea = screen.getByText(/Click to upload/i).closest('div') as HTMLElement;
226
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
208
227
  const input = screen.getByTestId('file-input') as HTMLInputElement;
209
228
  const clickSpy = vi.spyOn(input, 'click');
210
229
 
@@ -398,12 +417,18 @@ describe('[component] FileUpload', () => {
398
417
  fireEvent.change(input, { target: { files: [file] } });
399
418
  });
400
419
 
420
+ // Wait for upload to complete and file card to be rendered with File icon
421
+ // The File icon is rendered as an SVG inside CardHeader (header) inside Card (article)
422
+ // Since showPreview is false and it's a PDF, the File icon should be rendered
423
+ // In tests, lucide-react icons are mocked with data-testid="lucide-{name}"
401
424
  await waitFor(() => {
425
+ // First verify the file name is present
402
426
  expect(screen.getByText('test.pdf')).toBeInTheDocument();
403
- // File icon should be shown (emoji or icon)
404
- const fileIcon = document.querySelector('.w-12.h-12');
427
+
428
+ // The File icon is mocked with data-testid="lucide-file" in tests
429
+ const fileIcon = screen.getByTestId('lucide-file');
405
430
  expect(fileIcon).toBeInTheDocument();
406
- });
431
+ }, { timeout: 3000 });
407
432
  });
408
433
 
409
434
  it('shows loading spinner when showProgress is false', async () => {
@@ -429,14 +454,27 @@ describe('[component] FileUpload', () => {
429
454
  });
430
455
 
431
456
  // Wait for spinner to appear
457
+ // The LoadingSpinner has role="status" but no accessible name
458
+ // When showProgress is false and isUploading is true, spinner appears in CardHeader
459
+ // The upload starts immediately when file is selected, and status changes to 'uploading'
460
+ // Wait for the upload state to be set to 'uploading' which triggers isUploading
461
+ // The spinner appears in the CardHeader (the drop area) when isUploading && !showProgress
432
462
  await waitFor(() => {
433
- const spinnerContainer = screen.getByRole('status', { name: 'Uploading file' });
434
- const spinner = spinnerContainer.querySelector('.animate-spin');
463
+ // The spinner should appear when isUploading is true and showProgress is false
464
+ // isUploading is true when uploadStates has entries with status 'uploading' or 'processing'
465
+ // The spinner is in the CardHeader (the drop area)
466
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
467
+ const spinner = dropArea.querySelector('[role="status"]');
435
468
  expect(spinner).toBeInTheDocument();
469
+ // LoadingSpinner renders a canvas element with animate-spin class directly on it
470
+ // So we check if the spinner element itself has the class
471
+ expect(spinner).toHaveClass('animate-spin');
472
+ }, { timeout: 2000 });
473
+
474
+ // Now resolve the upload to complete it
475
+ await act(async () => {
476
+ resolveUpload!(createMockUploadResult());
436
477
  });
437
-
438
- // Resolve upload to clean up
439
- resolveUpload!(createMockUploadResult());
440
478
  });
441
479
  });
442
480
 
@@ -9,11 +9,15 @@
9
9
 
10
10
  import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
11
11
  import { SupabaseClient } from '@supabase/supabase-js';
12
+ import { Check, X, File } from 'lucide-react';
12
13
  import { FileCategory, FileUploadResult, UploadProgress } from '../../types/file-reference';
13
14
  import { useFileReference } from '../../hooks/useFileReference';
14
15
  import { getCurrentAppName } from '../../utils/app/appNameResolver';
15
16
  import { getAppId } from '../../utils/app/appIdResolver';
16
17
  import { assertAppId } from '../../types/core';
18
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../Card';
19
+ import { Progress } from '../Progress';
20
+ import { LoadingSpinner } from '../LoadingSpinner';
17
21
 
18
22
  /**
19
23
  * Props for the FileUpload component.
@@ -138,7 +142,7 @@ export function FileUpload({
138
142
 
139
143
  // Calculate isUploading and isDisabled early so they can be used in callbacks
140
144
  const isUploading = useMemo(() => {
141
- return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
145
+ return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
142
146
  state.progress.status === 'uploading' || state.progress.status === 'processing'
143
147
  );
144
148
  }, [uploadStates]);
@@ -154,7 +158,7 @@ export function FileUpload({
154
158
  resolve(null);
155
159
  return;
156
160
  }
157
-
161
+
158
162
  const reader = new FileReader();
159
163
  reader.onload = (e) => {
160
164
  resolve(e.target?.result as string || null);
@@ -203,7 +207,7 @@ export function FileUpload({
203
207
  if (!files || files.length === 0) return;
204
208
 
205
209
  const fileArray = Array.from(files);
206
-
210
+
207
211
  // Validate all files first
208
212
  const validationErrors: string[] = [];
209
213
  const validFiles: File[] = [];
@@ -224,11 +228,11 @@ export function FileUpload({
224
228
 
225
229
  // Initialize upload states
226
230
  const newUploadStates = new Map<string, FileUploadState>();
227
-
231
+
228
232
  for (const file of validFiles) {
229
233
  const fileId = `${file.name}-${file.size}-${Date.now()}`;
230
234
  const preview = showPreview ? (await generatePreview(file)) || undefined : undefined;
231
-
235
+
232
236
  const progress: UploadProgress = {
233
237
  loaded: 0,
234
238
  total: file.size,
@@ -383,7 +387,7 @@ export function FileUpload({
383
387
  }
384
388
  } catch (err) {
385
389
  const errorMessage = err instanceof Error ? err.message : 'Upload failed';
386
-
390
+
387
391
  setUploadStates(prev => {
388
392
  const updated = new Map(prev);
389
393
  const state = updated.get(fileId);
@@ -432,9 +436,9 @@ export function FileUpload({
432
436
  e.preventDefault();
433
437
  e.stopPropagation();
434
438
  setIsDragging(false);
435
-
439
+
436
440
  if (isDisabled) return;
437
-
441
+
438
442
  const files = e.dataTransfer.files;
439
443
  handleFileSelect(files);
440
444
  }, [isDisabled, handleFileSelect]);
@@ -465,8 +469,8 @@ export function FileUpload({
465
469
  const disabledClasses = isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-sec-50';
466
470
 
467
471
  return (
468
- <div className={`space-y-4 ${className}`}>
469
- <div
472
+ <Card className={className}>
473
+ <CardHeader
470
474
  role="button"
471
475
  tabIndex={isDisabled ? -1 : 0}
472
476
  aria-label="File upload area"
@@ -484,7 +488,7 @@ export function FileUpload({
484
488
  } : undefined}
485
489
  >
486
490
  {children || (
487
- <div className="space-y-2">
491
+ <>
488
492
  <input
489
493
  ref={fileInputRef}
490
494
  type="file"
@@ -496,146 +500,124 @@ export function FileUpload({
496
500
  data-testid="file-input"
497
501
  aria-label={accept ? `Upload file${multiple ? 's' : ''} (${accept})` : `Upload file${multiple ? 's' : ''}`}
498
502
  />
499
- <div className="text-sec-600">
503
+ <p className="text-sec-600">
500
504
  {isResolvingAppId ? (
501
505
  'Resolving app configuration...'
502
506
  ) : isDragging ? (
503
507
  'Drop files here...'
504
508
  ) : (
505
509
  <>
506
- <span className="font-medium">Click to upload</span>
510
+ Click to upload
507
511
  {' '}or drag and drop
508
512
  </>
509
513
  )}
510
- </div>
511
- <div className="text-sm text-sec-500">
514
+ </p>
515
+ <p className="text-sm text-sec-500">
512
516
  {!isResolvingAppId && accept !== '*/*' && `Accepted formats: ${accept}`}
513
517
  {!isResolvingAppId && maxSize && ` • Max size: ${Math.round(maxSize / 1024 / 1024)}MB`}
514
518
  {!isResolvingAppId && multiple && ' • Multiple files allowed'}
515
- </div>
516
- </div>
519
+ </p>
520
+ </>
517
521
  )}
518
-
522
+
519
523
  {isUploading && !showProgress && (
520
- <div
521
- className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center"
522
- role="status"
523
- aria-live="polite"
524
- aria-label="Uploading file"
525
- >
526
- <div className="animate-spin rounded-full size-8 border-b-2 border-main-500" aria-hidden="true"></div>
527
- </div>
524
+ <LoadingSpinner size="lg" className="text-main-500" />
528
525
  )}
529
- </div>
526
+ </CardHeader>
530
527
 
531
528
  {/* Upload Progress List */}
532
529
  {showProgress && uploadStates.size > 0 && (
533
- <div className="space-y-2">
534
- {Array.from(uploadStates.entries()).map(([fileId, uploadState]) => {
535
- const { file, progress, preview, result } = uploadState;
536
- const isError = progress.status === 'error';
537
- const isCompleted = progress.status === 'completed';
538
- const isUploading = progress.status === 'uploading' || progress.status === 'processing';
539
-
540
- return (
541
- <div
542
- key={fileId}
543
- className={`flex items-center space-x-3 p-3 rounded-lg border ${
544
- isError
545
- ? 'bg-acc-50 border-acc-200'
546
- : isCompleted
547
- ? 'bg-success-50 border-success-200'
548
- : 'bg-sec-50 border-sec-200'
549
- }`}
550
- >
551
- {/* Preview/Icon */}
552
- <div className="flex-shrink-0">
553
- {preview ? (
554
- <img
555
- src={preview}
556
- alt={file.name}
557
- className="w-12 h-12 object-cover rounded"
558
- />
559
- ) : (
560
- <div className="w-12 h-12 flex items-center justify-center bg-sec-200 rounded">
561
- <span className="text-2xl">📄</span>
562
- </div>
563
- )}
564
- </div>
565
-
566
- {/* File Info */}
567
- <div className="flex-1 min-w-0">
568
- <div className="font-medium text-sec-900 truncate">
569
- {file.name}
570
- </div>
571
- <div className="text-sm text-sec-500">
572
- {formatFileSize(file.size)}
573
- {isCompleted && result && ' Uploaded'}
574
- {isError && progress.error && ` • ${progress.error}`}
575
- </div>
576
-
577
- {/* Progress Bar */}
578
- {showProgress && (isUploading || isError) && (
579
- <div className="mt-2">
580
- <div className="w-full bg-sec-200 rounded-full h-2">
581
- <div
582
- className={`h-2 rounded-full transition-all duration-300 ${
583
- isError ? 'bg-acc-500' : 'bg-main-500'
584
- }`}
585
- style={{ width: `${progress.percentage}%` }}
530
+ <CardContent>
531
+ {Array.from(uploadStates.entries()).map(([fileId, uploadState]) => {
532
+ const { file, progress, preview, result } = uploadState;
533
+ const isError = progress.status === 'error';
534
+ const isCompleted = progress.status === 'completed';
535
+ const isUploading = progress.status === 'uploading' || progress.status === 'processing';
536
+
537
+ return (
538
+ <Card
539
+ key={fileId}
540
+ className={`grid grid-cols-[auto_1fr_auto] items-center gap-3 ${isError
541
+ ? 'bg-acc-50 border-acc-200'
542
+ : isCompleted
543
+ ? 'bg-success-50 border-success-200'
544
+ : 'bg-sec-50 border-sec-200'
545
+ }`}
546
+ >
547
+ <CardHeader className="p-0">
548
+ {preview ? (
549
+ <img
550
+ src={preview}
551
+ alt={file.name}
552
+ className="size-12 object-cover rounded"
553
+ />
554
+ ) : (
555
+ <File className="size-12 text-sec-600" />
556
+ )}
557
+ </CardHeader>
558
+
559
+ <CardContent className="p-0 min-w-0">
560
+ <CardTitle className="text-base truncate">
561
+ {file.name}
562
+ </CardTitle>
563
+ <CardDescription>
564
+ {formatFileSize(file.size)}
565
+ {isCompleted && result && ' • Uploaded'}
566
+ {isError && progress.error && ` • ${progress.error}`}
567
+ </CardDescription>
568
+
569
+ {/* Progress Bar */}
570
+ {showProgress && (isUploading || isError) && (
571
+ <>
572
+ <Progress
573
+ value={progress.percentage}
574
+ max={100}
575
+ style={{
576
+ accentColor: isError ? 'var(--color-acc-500)' : 'var(--color-main-500)'
577
+ }}
586
578
  />
587
- </div>
588
- {isUploading && (
589
- <div className="text-xs text-sec-500 mt-1">
590
- {progress.percentage}% • {formatFileSize(progress.loaded)} / {formatFileSize(progress.total)}
591
- </div>
592
- )}
593
- </div>
594
- )}
595
- </div>
596
-
597
- {/* Status Icon */}
598
- <div className="flex-shrink-0">
599
- {isCompleted && (
600
- <span className="text-success-500 text-xl">✓</span>
601
- )}
602
- {isError && (
603
- <span className="text-acc-500 text-xl">✕</span>
604
- )}
605
- {isUploading && (
606
- <div
607
- className="animate-spin rounded-full size-5 border-b-2 border-main-500"
608
- role="status"
609
- aria-label="Uploading"
610
- aria-hidden="true"
611
- ></div>
612
- )}
613
- </div>
614
- </div>
615
- );
616
- })}
617
- </div>
618
- )}
619
-
620
- {appIdError && (
621
- <div
622
- className="p-3 bg-acc-50 border border-acc-200 rounded-lg text-sm text-acc-600"
623
- role="alert"
624
- aria-live="assertive"
625
- >
626
- {appIdError}
627
- </div>
579
+ {isUploading && (
580
+ <p>
581
+ {progress.percentage}% {formatFileSize(progress.loaded)} / {formatFileSize(progress.total)}
582
+ </p>
583
+ )}
584
+ </>
585
+ )}
586
+ </CardContent>
587
+
588
+ <CardFooter className="p-0">
589
+ {isCompleted && (
590
+ <Check className="text-success-500 size-5" />
591
+ )}
592
+ {isError && (
593
+ <X className="text-acc-500 size-5" />
594
+ )}
595
+ {isUploading && (
596
+ <LoadingSpinner size="sm" className="text-main-500" />
597
+ )}
598
+ </CardFooter>
599
+ </Card>
600
+ );
601
+ })}
602
+
603
+ </CardContent>
628
604
  )}
629
- {error && (
630
- <div
631
- className="p-3 bg-acc-50 border border-acc-200 rounded-lg text-sm text-acc-600"
632
- role="alert"
633
- aria-live="assertive"
634
- >
635
- {error}
636
- </div>
605
+
606
+ {(appIdError || error) && (
607
+ <CardFooter>
608
+ {appIdError && (
609
+ <p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
610
+ {appIdError}
611
+ </p>
612
+ )}
613
+ {error && (
614
+ <p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
615
+ {error}
616
+ </p>
617
+ )}
618
+ </CardFooter>
637
619
  )}
638
- </div>
620
+ </Card>
639
621
  );
640
622
  }
641
623
 
@@ -97,10 +97,8 @@ const Progress = React.forwardRef<
97
97
  <progress
98
98
  ref={ref}
99
99
  className={cn(
100
- 'appearance-none border-0 h-2 w-full rounded-full overflow-hidden transition-all accent-primary',
101
- isIndeterminate
102
- ? 'bg-gradient-to-r from-primary/10 via-primary/90 to-primary/10'
103
- : 'bg-primary/20',
100
+ 'appearance-none border-0 h-2 w-full rounded-full overflow-hidden transition-all accent-main-800',
101
+ !isIndeterminate && 'bg-sec-600/50',
104
102
  className
105
103
  )}
106
104
  {...(isIndeterminate ? {} : { value })}
@@ -260,9 +260,9 @@ export function ProtectedRoute({
260
260
  // Use isLoading (combined loading state) for consistency with simpler implementations
261
261
  if (isLoading && !sessionRestoration.hasTimedOut) {
262
262
  return loadingFallback || (
263
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
263
+ <main className="grid place-items-center size-full">
264
264
  <LoadingSpinner />
265
- </div>
265
+ </main>
266
266
  );
267
267
  }
268
268
 
@@ -294,9 +294,9 @@ export function ProtectedRoute({
294
294
  const isTabVisible = typeof document !== 'undefined' && !document.hidden;
295
295
  if (tabJustBecameVisibleRef.current || (isTabVisible && wasAuthenticatedRef.current && isLoading)) {
296
296
  return loadingFallback || (
297
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
297
+ <main className="grid place-items-center size-full">
298
298
  <LoadingSpinner />
299
- </div>
299
+ </main>
300
300
  );
301
301
  }
302
302
 
@@ -309,9 +309,9 @@ export function ProtectedRoute({
309
309
  // Show loading state while we wait for session refresh (unless we're not loading)
310
310
  if (isLoading) {
311
311
  return loadingFallback || (
312
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
312
+ <main className="grid place-items-center size-full">
313
313
  <LoadingSpinner />
314
- </div>
314
+ </main>
315
315
  );
316
316
  }
317
317
 
@@ -333,14 +333,14 @@ export function ProtectedRoute({
333
333
  // If no events are available, show error message
334
334
  if (!events || events.length === 0) {
335
335
  return noEventsFallback || (
336
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', padding: '2rem' }}>
336
+ <main className="grid place-items-center text-center min-h-screen p-8">
337
337
  <Alert variant="destructive" className="max-w-md">
338
338
  <AlertTitle>No Events Available</AlertTitle>
339
339
  <AlertDescription>
340
340
  You don't have access to any events. Please contact your administrator if you believe this is an error.
341
341
  </AlertDescription>
342
342
  </Alert>
343
- </div>
343
+ </main>
344
344
  );
345
345
  }
346
346