@jmruthers/pace-core 0.6.6 → 0.6.8

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 (292) hide show
  1. package/{scripts/audit/audit-dependencies.cjs → audit-tool/00-dependencies.cjs} +227 -22
  2. package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
  3. package/audit-tool/audits/02-project-structure.cjs +240 -0
  4. package/audit-tool/audits/03-architecture.cjs +224 -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 +554 -0
  8. package/audit-tool/audits/07-api-tech-stack.cjs +355 -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 +295 -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 +380 -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-6RMSCQJ6.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-CIGSujI2.d.ts} +40 -24
  28. package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
  29. package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
  30. package/dist/{chunk-MAGBIDNS.js → chunk-4DDCYDQ3.js} +8 -7
  31. package/dist/{chunk-BVP2BCJF.js → chunk-5W2A3DRC.js} +10 -9
  32. package/dist/{chunk-SD6WQY43.js → chunk-7ILTDCL2.js} +9 -1
  33. package/dist/{chunk-3QC3KRHK.js → chunk-A3W6LW53.js} +16 -1
  34. package/dist/{chunk-3O3WHILE.js → chunk-EF2UGZWY.js} +239 -63
  35. package/dist/{chunk-LAZMKTTF.js → chunk-EURB7QFZ.js} +341 -337
  36. package/dist/{chunk-2HGJFNAH.js → chunk-FEJLJNWA.js} +1 -15
  37. package/dist/{chunk-7TYHROIV.js → chunk-GS5672WG.js} +55 -13
  38. package/dist/{chunk-UIYSCEV7.js → chunk-IUBRCBSY.js} +1 -1
  39. package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
  40. package/dist/{chunk-FENMYN2U.js → chunk-MPBLMWVR.js} +3 -3
  41. package/dist/{chunk-ZS5VO5JB.js → chunk-NKHKXPI4.js} +408 -453
  42. package/dist/{chunk-A55DK444.js → chunk-OJ4SKRSV.js} +1 -7
  43. package/dist/{chunk-4T7OBVTU.js → chunk-S6ZQKDY6.js} +1 -1
  44. package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
  45. package/dist/{chunk-OHIK3MIO.js → chunk-Z2FNRKF3.js} +13 -13
  46. package/dist/components.d.ts +5 -4
  47. package/dist/components.js +29 -34
  48. package/dist/eslint-rules/index.cjs +22 -9
  49. package/{src/eslint-rules/rules/compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +184 -23
  50. package/dist/eslint-rules/rules/04-code-quality.cjs +346 -0
  51. package/dist/eslint-rules/rules/05-styling.cjs +61 -0
  52. package/dist/eslint-rules/rules/{rbac.cjs → 06-security-rbac.cjs} +34 -13
  53. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +385 -0
  54. package/dist/eslint-rules/rules/08-testing.cjs +94 -0
  55. package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
  56. package/dist/hooks.d.ts +5 -5
  57. package/dist/hooks.js +8 -8
  58. package/dist/index.d.ts +7 -7
  59. package/dist/index.js +21 -20
  60. package/dist/providers.js +2 -2
  61. package/dist/rbac/index.d.ts +1 -1
  62. package/dist/rbac/index.js +8 -8
  63. package/dist/theming/runtime.d.ts +61 -1
  64. package/dist/theming/runtime.js +1 -1
  65. package/dist/{types-B-K_5VnO.d.ts → types-DXstZpNI.d.ts} +0 -17
  66. package/dist/types.d.ts +2 -2
  67. package/dist/{usePublicRouteParams-COZ28Mvq.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +19 -19
  68. package/dist/utils.d.ts +2 -2
  69. package/dist/utils.js +8 -8
  70. package/docs/README.md +1 -1
  71. package/docs/api/modules.md +106 -41
  72. package/docs/api-reference/components.md +18 -20
  73. package/docs/api-reference/hooks.md +80 -80
  74. package/docs/api-reference/types.md +1 -1
  75. package/docs/api-reference/utilities.md +1 -1
  76. package/docs/architecture/README.md +1 -1
  77. package/docs/core-concepts/events.md +3 -3
  78. package/docs/core-concepts/organisations.md +6 -6
  79. package/docs/core-concepts/permissions.md +6 -6
  80. package/docs/documentation-index.md +12 -18
  81. package/docs/getting-started/dependencies.md +23 -0
  82. package/docs/getting-started/documentation-index.md +1 -1
  83. package/docs/getting-started/examples/README.md +4 -4
  84. package/docs/getting-started/examples/full-featured-app.md +1 -1
  85. package/docs/getting-started/faq.md +2 -2
  86. package/docs/getting-started/quick-reference.md +4 -4
  87. package/docs/implementation-guides/app-layout.md +1 -1
  88. package/docs/implementation-guides/authentication.md +15 -15
  89. package/docs/implementation-guides/component-styling.md +1 -1
  90. package/docs/implementation-guides/data-tables.md +127 -34
  91. package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
  92. package/docs/implementation-guides/dynamic-colors.md +3 -3
  93. package/docs/implementation-guides/file-upload-storage.md +2 -2
  94. package/docs/implementation-guides/hierarchical-datatable.md +40 -60
  95. package/docs/implementation-guides/inactivity-tracking.md +3 -3
  96. package/docs/implementation-guides/large-datasets.md +3 -2
  97. package/docs/implementation-guides/organisation-security.md +2 -2
  98. package/docs/implementation-guides/performance.md +2 -2
  99. package/docs/implementation-guides/permission-enforcement.md +1 -1
  100. package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
  101. package/docs/migration/V0.4.0_rbac-migration.md +6 -6
  102. package/docs/rbac/README.md +5 -5
  103. package/docs/rbac/advanced-patterns.md +6 -6
  104. package/docs/rbac/api-reference.md +20 -20
  105. package/docs/rbac/event-based-apps.md +3 -3
  106. package/docs/rbac/examples.md +41 -41
  107. package/docs/rbac/getting-started.md +37 -37
  108. package/docs/rbac/performance.md +1 -1
  109. package/docs/rbac/quick-start.md +52 -52
  110. package/docs/rbac/secure-client-protection.md +1 -1
  111. package/docs/rbac/troubleshooting.md +1 -1
  112. package/docs/security/README.md +5 -5
  113. package/docs/standards/0-standards-overview.md +220 -0
  114. package/docs/standards/{00-pace-core-compliance.md → 1-pace-core-compliance-standards.md} +241 -185
  115. package/docs/standards/{02-project-structure.md → 2-project-structure-standards.md} +11 -47
  116. package/docs/standards/3-architecture-standards.md +606 -0
  117. package/docs/standards/4-code-quality-standards.md +728 -0
  118. package/docs/standards/{08-markup-quality.md → 5-styling-standards.md} +12 -9
  119. package/docs/standards/{09-rbac-compliance.md → 6-security-rbac-standards.md} +126 -18
  120. package/docs/standards/7-api-tech-stack-standards.md +662 -0
  121. package/docs/standards/8-testing-documentation-standards.md +401 -0
  122. package/docs/standards/9-operations-standards.md +1102 -0
  123. package/docs/standards/README.md +203 -104
  124. package/docs/troubleshooting/README.md +4 -4
  125. package/docs/troubleshooting/common-issues.md +2 -2
  126. package/docs/troubleshooting/debugging.md +9 -9
  127. package/docs/troubleshooting/migration.md +4 -4
  128. package/eslint-config-pace-core.cjs +50 -20
  129. package/package.json +50 -19
  130. package/scripts/eslint-audit.cjs +123 -0
  131. package/scripts/install-cursor-rules.cjs +11 -243
  132. package/scripts/install-eslint-config.cjs +349 -0
  133. package/scripts/validate-dependencies.cjs +248 -0
  134. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +2 -2
  135. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
  136. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +30 -18
  137. package/src/__tests__/integration/UserProfile.test.tsx +14 -14
  138. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
  139. package/src/__tests__/templates/accessibility.test.template.tsx +10 -9
  140. package/src/__tests__/templates/component.test.template.tsx +18 -15
  141. package/src/components/AddressField/AddressField.tsx +26 -1
  142. package/src/components/Alert/Alert.test.tsx +86 -22
  143. package/src/components/Alert/Alert.tsx +19 -11
  144. package/src/components/Badge/Badge.tsx +1 -1
  145. package/src/components/Calendar/Calendar.tsx +201 -47
  146. package/src/components/Checkbox/Checkbox.test.tsx +2 -1
  147. package/src/components/ContextSelector/ContextSelector.tsx +108 -126
  148. package/src/components/DataTable/AUDIT_REPORT.md +293 -0
  149. package/src/components/DataTable/DataTable.tsx +1 -19
  150. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -2
  151. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +21 -6
  152. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
  153. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
  154. package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
  155. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
  156. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
  157. package/src/components/DataTable/components/DataTableLayout.tsx +5 -16
  158. package/src/components/DataTable/components/EditableRow.tsx +5 -7
  159. package/src/components/DataTable/components/EmptyState.tsx +11 -10
  160. package/src/components/DataTable/components/FilterRow.tsx +2 -4
  161. package/src/components/DataTable/components/ImportModal.tsx +124 -126
  162. package/src/components/DataTable/components/LoadingState.tsx +5 -6
  163. package/src/components/DataTable/components/SortIndicator.tsx +50 -0
  164. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
  165. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
  166. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
  167. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
  168. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
  169. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +45 -27
  170. package/src/components/DataTable/components/index.ts +2 -1
  171. package/src/components/DataTable/types.ts +0 -18
  172. package/src/components/DataTable/utils/a11yUtils.ts +17 -0
  173. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -1
  174. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
  175. package/src/components/DateTimeField/DateTimeField.tsx +7 -8
  176. package/src/components/Dialog/Dialog.test.tsx +1 -0
  177. package/src/components/Dialog/Dialog.tsx +25 -8
  178. package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
  179. package/src/components/FileUpload/FileUpload.test.tsx +45 -16
  180. package/src/components/FileUpload/FileUpload.tsx +141 -130
  181. package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
  182. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
  183. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
  184. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
  185. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
  186. package/src/components/Progress/Progress.tsx +2 -4
  187. package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
  188. package/src/components/Select/Select.tsx +86 -77
  189. package/src/components/Select/types.ts +3 -0
  190. package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
  191. package/src/hooks/__tests__/hooks.integration.test.tsx +49 -49
  192. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
  193. package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
  194. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +99 -99
  195. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
  196. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
  197. package/src/hooks/public/usePublicEvent.ts +5 -5
  198. package/src/hooks/public/usePublicEventLogo.ts +5 -5
  199. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  200. package/src/hooks/public/usePublicRouteParams.ts +13 -9
  201. package/src/hooks/useAddressAutocomplete.test.ts +18 -18
  202. package/src/hooks/useAppConfig.ts +2 -2
  203. package/src/hooks/useEventTheme.test.ts +7 -7
  204. package/src/hooks/useEventTheme.ts +2 -1
  205. package/src/hooks/useFileDisplay.ts +2 -2
  206. package/src/hooks/useFileUrl.ts +52 -8
  207. package/src/hooks/useOrganisationSecurity.test.ts +2 -1
  208. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
  209. package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
  210. package/src/providers/__tests__/EventProvider.test.tsx +61 -61
  211. package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
  212. package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
  213. package/src/providers/__tests__/ProviderLifecycle.test.tsx +38 -38
  214. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
  215. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
  216. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +10 -10
  217. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
  218. package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
  219. package/src/rbac/api.test.ts +104 -0
  220. package/src/rbac/engine.ts +1 -1
  221. package/src/rbac/hooks/useCan.test.ts +2 -2
  222. package/src/rbac/secureClient.ts +1 -1
  223. package/src/rbac/types/functions.ts +1 -1
  224. package/src/styles/core.css +7 -0
  225. package/src/theming/__tests__/parseEventColours.test.ts +118 -3
  226. package/src/theming/parseEventColours.ts +77 -11
  227. package/src/types/supabase.ts +2 -3
  228. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
  229. package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
  230. package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
  231. package/src/utils/formatting/formatDate.test.ts +3 -2
  232. package/src/utils/formatting/formatDateTime.test.ts +2 -2
  233. package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
  234. package/src/utils/storage/README.md +1 -1
  235. package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
  236. package/src/utils/storage/helpers.test.ts +69 -3
  237. package/cursor-rules/01-standards-compliance.mdc +0 -285
  238. package/cursor-rules/04-testing-standards.mdc +0 -270
  239. package/cursor-rules/05-bug-reports-and-features.mdc +0 -248
  240. package/cursor-rules/06-code-quality.mdc +0 -311
  241. package/cursor-rules/07-tech-stack-compliance.mdc +0 -216
  242. package/cursor-rules/10-error-handling-patterns.mdc +0 -179
  243. package/cursor-rules/11-performance-optimization.mdc +0 -169
  244. package/cursor-rules/12-ci-cd-integration.mdc +0 -150
  245. package/dist/DataTable-LRJL4IRV.js +0 -15
  246. package/dist/eslint-rules/rules/compliance.cjs +0 -348
  247. package/dist/eslint-rules/rules/components.cjs +0 -113
  248. package/dist/eslint-rules/rules/imports.cjs +0 -102
  249. package/docs/best-practices/README.md +0 -472
  250. package/docs/best-practices/accessibility.md +0 -604
  251. package/docs/best-practices/common-patterns.md +0 -516
  252. package/docs/best-practices/deployment.md +0 -1103
  253. package/docs/best-practices/performance.md +0 -1328
  254. package/docs/best-practices/security.md +0 -940
  255. package/docs/best-practices/testing.md +0 -1034
  256. package/docs/rbac/compliance/compliance-guide.md +0 -544
  257. package/docs/standards/01-standards-compliance.md +0 -188
  258. package/docs/standards/03-solid-principles.md +0 -39
  259. package/docs/standards/04-testing-standards.md +0 -36
  260. package/docs/standards/05-bug-reports-and-features.md +0 -27
  261. package/docs/standards/06-code-quality.md +0 -34
  262. package/docs/standards/07-tech-stack-compliance.md +0 -30
  263. package/docs/standards/10-error-handling-patterns.md +0 -401
  264. package/docs/standards/11-performance-optimization.md +0 -348
  265. package/docs/standards/12-ci-cd-integration.md +0 -370
  266. package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +0 -192
  267. package/scripts/audit/audit-compliance.cjs +0 -1295
  268. package/scripts/audit/audit-components.cjs +0 -260
  269. package/scripts/audit/audit-rbac.cjs +0 -954
  270. package/scripts/audit/audit-standards.cjs +0 -1268
  271. package/scripts/audit/index.cjs +0 -1927
  272. package/src/components/DataTable/components/DataTableBody.tsx +0 -478
  273. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
  274. package/src/components/DataTable/components/ExpandButton.tsx +0 -113
  275. package/src/components/DataTable/components/GroupHeader.tsx +0 -54
  276. package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
  277. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
  278. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
  279. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
  280. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
  281. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
  282. package/src/components/DataTable/core/DataTableContext.tsx +0 -216
  283. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
  284. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
  285. package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
  286. package/src/components/DataTable/utils/debugTools.ts +0 -514
  287. package/src/eslint-rules/index.cjs +0 -22
  288. package/src/eslint-rules/rules/components.cjs +0 -113
  289. package/src/eslint-rules/rules/imports.cjs +0 -102
  290. package/src/eslint-rules/rules/rbac.cjs +0 -790
  291. package/src/eslint-rules/utils/helpers.cjs +0 -42
  292. package/src/eslint-rules/utils/manifest-loader.cjs +0 -75
@@ -116,18 +116,22 @@ 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
+ // The drop area is the CardHeader with role="button", not a div
120
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
120
121
 
121
122
  fireEvent.dragOver(dropArea);
122
- expect(screen.getByText(/Drop files here/i)).toBeInTheDocument();
123
+ // Component shows "Drop files here..." with ellipsis
124
+ expect(screen.getByText(/Drop files here\.\.\./i)).toBeInTheDocument();
123
125
  });
124
126
 
125
127
  it('resets drag state on dragleave', () => {
126
128
  renderWithProviders(<FileUpload {...baseProps} />);
127
- const dropArea = screen.getByText(/Click to upload/i).closest('div') as HTMLElement;
129
+ // The drop area is the CardHeader with role="button", not a div
130
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
128
131
 
129
132
  fireEvent.dragOver(dropArea);
130
- expect(screen.getByText(/Drop files here/i)).toBeInTheDocument();
133
+ // Component shows "Drop files here..." with ellipsis
134
+ expect(screen.getByText(/Drop files here\.\.\./i)).toBeInTheDocument();
131
135
 
132
136
  fireEvent.dragLeave(dropArea);
133
137
  expect(screen.getByText(/Click to upload/i)).toBeInTheDocument();
@@ -135,24 +139,45 @@ describe('[component] FileUpload', () => {
135
139
 
136
140
  it('handles file drop', async () => {
137
141
  const onUploadSuccess = vi.fn();
142
+ const mockUploadFile = vi.fn(async () => createMockUploadResult());
143
+ mockUseFileReference.mockReturnValue({
144
+ uploadFile: mockUploadFile,
145
+ isLoading: false,
146
+ error: null,
147
+ });
148
+
138
149
  renderWithProviders(
139
150
  <FileUpload {...baseProps} onUploadSuccess={onUploadSuccess} showProgress />
140
151
  );
141
152
 
142
- const dropArea = screen.getByText(/Click to upload/i).closest('div') as HTMLElement;
153
+ // Wait for app ID resolution to complete
154
+ await waitFor(() => {
155
+ expect(screen.queryByText(/Resolving app configuration/i)).not.toBeInTheDocument();
156
+ });
157
+
158
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
143
159
  const file = createTestFile('test.png', 'image/png');
144
160
 
161
+ // Use fireEvent.drop with proper dataTransfer mock
145
162
  await act(async () => {
146
163
  fireEvent.drop(dropArea, {
147
164
  dataTransfer: {
148
165
  files: [file],
149
- },
166
+ items: [{
167
+ kind: 'file',
168
+ type: file.type,
169
+ getAsFile: () => file
170
+ }],
171
+ types: ['Files']
172
+ }
150
173
  });
151
174
  });
152
175
 
176
+ // Wait for upload to complete - the uploadFile is called asynchronously
153
177
  await waitFor(() => {
178
+ expect(mockUploadFile).toHaveBeenCalled();
154
179
  expect(onUploadSuccess).toHaveBeenCalled();
155
- });
180
+ }, { timeout: 5000 });
156
181
  });
157
182
 
158
183
  it('does not handle drop when disabled', () => {
@@ -204,7 +229,7 @@ describe('[component] FileUpload', () => {
204
229
 
205
230
  it('triggers file input click when drop area is clicked', () => {
206
231
  renderWithProviders(<FileUpload {...baseProps} />);
207
- const dropArea = screen.getByText(/Click to upload/i).closest('div') as HTMLElement;
232
+ const dropArea = screen.getByRole('button', { name: /File upload area/i });
208
233
  const input = screen.getByTestId('file-input') as HTMLInputElement;
209
234
  const clickSpy = vi.spyOn(input, 'click');
210
235
 
@@ -398,12 +423,17 @@ describe('[component] FileUpload', () => {
398
423
  fireEvent.change(input, { target: { files: [file] } });
399
424
  });
400
425
 
426
+ // Wait for upload to complete and file card to be rendered with File icon
427
+ // The File icon is rendered as an SVG inside CardHeader (header) inside Card (article)
428
+ // Since showPreview is false and it's a PDF, the File icon should be rendered
429
+ // In tests, lucide-react icons are mocked with data-testid="lucide-{name}"
401
430
  await waitFor(() => {
431
+ // First verify the file name is present
402
432
  expect(screen.getByText('test.pdf')).toBeInTheDocument();
403
- // File icon should be shown (emoji or icon)
404
- const fileIcon = document.querySelector('.w-12.h-12');
433
+ // File icon should be shown (lucide-react File icon)
434
+ const fileIcon = screen.getByTestId('lucide-file');
405
435
  expect(fileIcon).toBeInTheDocument();
406
- });
436
+ }, { timeout: 3000 });
407
437
  });
408
438
 
409
439
  it('shows loading spinner when showProgress is false', async () => {
@@ -429,10 +459,11 @@ describe('[component] FileUpload', () => {
429
459
  });
430
460
 
431
461
  // Wait for spinner to appear
462
+ // LoadingSpinner uses role="status" with "Loading..." text, not "Uploading file"
432
463
  await waitFor(() => {
433
- const spinnerContainer = screen.getByRole('status', { name: 'Uploading file' });
434
- const spinner = spinnerContainer.querySelector('.animate-spin');
464
+ const spinner = screen.getByRole('status');
435
465
  expect(spinner).toBeInTheDocument();
466
+ expect(spinner).toHaveClass('animate-spin');
436
467
  });
437
468
 
438
469
  // Resolve upload to clean up
@@ -592,6 +623,4 @@ describe('[component] FileUpload', () => {
592
623
  });
593
624
  });
594
625
  });
595
- });
596
-
597
-
626
+ });
@@ -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.
@@ -90,8 +94,19 @@ export function FileUpload({
90
94
  const [isResolvingAppId, setIsResolvingAppId] = useState(!app_id);
91
95
  const [appIdError, setAppIdError] = useState<string | null>(null);
92
96
  const fileInputRef = useRef<HTMLInputElement>(null);
97
+ const progressIntervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
93
98
  const { uploadFile, isLoading, error } = useFileReference(supabase);
94
99
 
100
+ // Cleanup all progress intervals on unmount
101
+ useEffect(() => {
102
+ return () => {
103
+ progressIntervalsRef.current.forEach((interval) => {
104
+ clearInterval(interval);
105
+ });
106
+ progressIntervalsRef.current.clear();
107
+ };
108
+ }, []);
109
+
95
110
  // Resolve app_id from app name if not provided
96
111
  useEffect(() => {
97
112
  if (app_id) {
@@ -138,7 +153,7 @@ export function FileUpload({
138
153
 
139
154
  // Calculate isUploading and isDisabled early so they can be used in callbacks
140
155
  const isUploading = useMemo(() => {
141
- return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
156
+ return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
142
157
  state.progress.status === 'uploading' || state.progress.status === 'processing'
143
158
  );
144
159
  }, [uploadStates]);
@@ -154,7 +169,7 @@ export function FileUpload({
154
169
  resolve(null);
155
170
  return;
156
171
  }
157
-
172
+
158
173
  const reader = new FileReader();
159
174
  reader.onload = (e) => {
160
175
  resolve(e.target?.result as string || null);
@@ -203,7 +218,7 @@ export function FileUpload({
203
218
  if (!files || files.length === 0) return;
204
219
 
205
220
  const fileArray = Array.from(files);
206
-
221
+
207
222
  // Validate all files first
208
223
  const validationErrors: string[] = [];
209
224
  const validFiles: File[] = [];
@@ -224,11 +239,11 @@ export function FileUpload({
224
239
 
225
240
  // Initialize upload states
226
241
  const newUploadStates = new Map<string, FileUploadState>();
227
-
242
+
228
243
  for (const file of validFiles) {
229
244
  const fileId = `${file.name}-${file.size}-${Date.now()}`;
230
245
  const preview = showPreview ? (await generatePreview(file)) || undefined : undefined;
231
-
246
+
232
247
  const progress: UploadProgress = {
233
248
  loaded: 0,
234
249
  total: file.size,
@@ -291,16 +306,25 @@ export function FileUpload({
291
306
  return updated;
292
307
  });
293
308
  }, 200);
309
+
310
+ // Store interval in ref for cleanup
311
+ progressIntervalsRef.current.set(fileId, progressInterval);
294
312
 
295
313
 
296
314
  // Use resolved app_id
297
315
  if (!resolvedAppId) {
316
+ // Clear interval before throwing error
317
+ clearInterval(progressInterval);
318
+ progressIntervalsRef.current.delete(fileId);
298
319
  const errorMsg = appIdError || 'App ID not available. Please provide app_id prop or set app name.';
299
320
  throw new Error(errorMsg);
300
321
  }
301
322
 
302
323
  // Validate pageContext before upload
303
324
  if (!pageContext) {
325
+ // Clear interval before throwing error
326
+ clearInterval(progressInterval);
327
+ progressIntervalsRef.current.delete(fileId);
304
328
  const errorMsg = 'pageContext is required for file upload. This is used for permission checks.';
305
329
  throw new Error(errorMsg);
306
330
  }
@@ -318,7 +342,9 @@ export function FileUpload({
318
342
  is_public: isPublic
319
343
  }, file);
320
344
 
345
+ // Clear interval and remove from ref
321
346
  clearInterval(progressInterval);
347
+ progressIntervalsRef.current.delete(fileId);
322
348
 
323
349
  if (result) {
324
350
  // Update status to completed
@@ -382,8 +408,15 @@ export function FileUpload({
382
408
  onUploadError?.('Upload failed', file);
383
409
  }
384
410
  } catch (err) {
385
- const errorMessage = err instanceof Error ? err.message : 'Upload failed';
411
+ // Clear interval on error
412
+ const interval = progressIntervalsRef.current.get(fileId);
413
+ if (interval) {
414
+ clearInterval(interval);
415
+ progressIntervalsRef.current.delete(fileId);
416
+ }
386
417
 
418
+ const errorMessage = err instanceof Error ? err.message : 'Upload failed';
419
+
387
420
  setUploadStates(prev => {
388
421
  const updated = new Map(prev);
389
422
  const state = updated.get(fileId);
@@ -432,9 +465,9 @@ export function FileUpload({
432
465
  e.preventDefault();
433
466
  e.stopPropagation();
434
467
  setIsDragging(false);
435
-
468
+
436
469
  if (isDisabled) return;
437
-
470
+
438
471
  const files = e.dataTransfer.files;
439
472
  handleFileSelect(files);
440
473
  }, [isDisabled, handleFileSelect]);
@@ -465,8 +498,8 @@ export function FileUpload({
465
498
  const disabledClasses = isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-sec-50';
466
499
 
467
500
  return (
468
- <div className={`space-y-4 ${className}`}>
469
- <div
501
+ <Card className={className}>
502
+ <CardHeader
470
503
  role="button"
471
504
  tabIndex={isDisabled ? -1 : 0}
472
505
  aria-label="File upload area"
@@ -484,7 +517,7 @@ export function FileUpload({
484
517
  } : undefined}
485
518
  >
486
519
  {children || (
487
- <div className="space-y-2">
520
+ <>
488
521
  <input
489
522
  ref={fileInputRef}
490
523
  type="file"
@@ -496,146 +529,124 @@ export function FileUpload({
496
529
  data-testid="file-input"
497
530
  aria-label={accept ? `Upload file${multiple ? 's' : ''} (${accept})` : `Upload file${multiple ? 's' : ''}`}
498
531
  />
499
- <div className="text-sec-600">
532
+ <p className="text-sec-600">
500
533
  {isResolvingAppId ? (
501
534
  'Resolving app configuration...'
502
535
  ) : isDragging ? (
503
536
  'Drop files here...'
504
537
  ) : (
505
538
  <>
506
- <span className="font-medium">Click to upload</span>
539
+ Click to upload
507
540
  {' '}or drag and drop
508
541
  </>
509
542
  )}
510
- </div>
511
- <div className="text-sm text-sec-500">
543
+ </p>
544
+ <p className="text-sm text-sec-500">
512
545
  {!isResolvingAppId && accept !== '*/*' && `Accepted formats: ${accept}`}
513
546
  {!isResolvingAppId && maxSize && ` • Max size: ${Math.round(maxSize / 1024 / 1024)}MB`}
514
547
  {!isResolvingAppId && multiple && ' • Multiple files allowed'}
515
- </div>
516
- </div>
548
+ </p>
549
+ </>
517
550
  )}
518
-
551
+
519
552
  {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>
553
+ <LoadingSpinner size="lg" className="text-main-500" />
528
554
  )}
529
- </div>
555
+ </CardHeader>
530
556
 
531
557
  {/* Upload Progress List */}
532
558
  {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}%` }}
559
+ <CardContent>
560
+ {Array.from(uploadStates.entries()).map(([fileId, uploadState]) => {
561
+ const { file, progress, preview, result } = uploadState;
562
+ const isError = progress.status === 'error';
563
+ const isCompleted = progress.status === 'completed';
564
+ const isUploading = progress.status === 'uploading' || progress.status === 'processing';
565
+
566
+ return (
567
+ <Card
568
+ key={fileId}
569
+ className={`grid grid-cols-[auto_1fr_auto] items-center gap-3 ${isError
570
+ ? 'bg-acc-50 border-acc-200'
571
+ : isCompleted
572
+ ? 'bg-success-50 border-success-200'
573
+ : 'bg-sec-50 border-sec-200'
574
+ }`}
575
+ >
576
+ <CardHeader className="p-0">
577
+ {preview ? (
578
+ <img
579
+ src={preview}
580
+ alt={file.name}
581
+ className="size-12 object-cover rounded"
582
+ />
583
+ ) : (
584
+ <File className="size-12 text-sec-600" />
585
+ )}
586
+ </CardHeader>
587
+
588
+ <CardContent className="p-0 min-w-0">
589
+ <CardTitle className="text-base truncate">
590
+ {file.name}
591
+ </CardTitle>
592
+ <CardDescription>
593
+ {formatFileSize(file.size)}
594
+ {isCompleted && result && ' • Uploaded'}
595
+ {isError && progress.error && ` • ${progress.error}`}
596
+ </CardDescription>
597
+
598
+ {/* Progress Bar */}
599
+ {showProgress && (isUploading || isError) && (
600
+ <>
601
+ <Progress
602
+ value={progress.percentage}
603
+ max={100}
604
+ style={{
605
+ accentColor: isError ? 'var(--color-acc-500)' : 'var(--color-main-500)'
606
+ }}
586
607
  />
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>
608
+ {isUploading && (
609
+ <p>
610
+ {progress.percentage}% {formatFileSize(progress.loaded)} / {formatFileSize(progress.total)}
611
+ </p>
612
+ )}
613
+ </>
614
+ )}
615
+ </CardContent>
616
+
617
+ <CardFooter className="p-0">
618
+ {isCompleted && (
619
+ <Check className="text-success-500 size-5" />
620
+ )}
621
+ {isError && (
622
+ <X className="text-acc-500 size-5" />
623
+ )}
624
+ {isUploading && (
625
+ <LoadingSpinner size="sm" className="text-main-500" />
626
+ )}
627
+ </CardFooter>
628
+ </Card>
629
+ );
630
+ })}
631
+
632
+ </CardContent>
628
633
  )}
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>
634
+
635
+ {(appIdError || error) && (
636
+ <CardFooter>
637
+ {appIdError && (
638
+ <p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
639
+ {appIdError}
640
+ </p>
641
+ )}
642
+ {error && (
643
+ <p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
644
+ {error}
645
+ </p>
646
+ )}
647
+ </CardFooter>
637
648
  )}
638
- </div>
649
+ </Card>
639
650
  );
640
651
  }
641
652