@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
@@ -36,10 +36,12 @@
36
36
  * - Customizable text content
37
37
  */
38
38
  import React, { useState, useRef, useEffect } from 'react';
39
- import { Dialog, DialogContent, DialogHeader } from '../../Dialog';
39
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '../../Dialog';
40
40
  import { Button } from '../../Button/Button';
41
41
  import { Input } from '../../Input/Input';
42
42
  import { Progress } from '../../Progress';
43
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../Table/Table';
44
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../../Card/Card';
43
45
  import { Upload, FileText, AlertCircle } from 'lucide-react';
44
46
  import { createLogger } from '../../../utils/core/logger';
45
47
 
@@ -372,150 +374,146 @@ export function ImportModal({ isOpen, onClose, onImport, config = {} }: ImportMo
372
374
 
373
375
  return (
374
376
  <Dialog open={isOpen} onOpenChange={handleClose}>
375
- <DialogContent className="sm:max-w-2xl bg-main-50" title={title} description={description}>
377
+ <DialogContent
378
+ maxWidthPercent={95}
379
+ className="w-auto"
380
+ title={title}
381
+ description={description}
382
+ >
376
383
  <DialogHeader>
377
- <h2>{title}</h2>
378
- <p>{description}</p>
384
+ <DialogTitle>{title}</DialogTitle>
385
+ <DialogDescription>{description}</DialogDescription>
379
386
  </DialogHeader>
380
387
 
381
- <div className="space-y-4">
382
- <div className="border-2 border-dashed border-sec-200 rounded-lg p-6 text-center">
383
- <FileText className="size-8 mx-auto text-sec-400 mb-2" />
384
- <p className="text-sec-600 mb-2">
388
+ <Card className="text-center">
389
+ <CardHeader>
390
+ <FileText className="size-8 mx-auto text-sec-400 mb-2" />
391
+ </CardHeader>
392
+ <CardDescription>
385
393
  {file ? `Selected: ${file.name}` : uploadText}
386
- </p>
387
- {file && (
388
- <p className="text-xs text-sec-500">
389
- File selected, processing preview...
390
- </p>
391
- )}
392
- <Button
393
- variant="outline"
394
- size="sm"
395
- onClick={() => fileInputRef.current?.click()}
396
- >
397
- <Upload className="size-4 mr-2" />
398
- {selectFileButtonText}
399
- </Button>
400
- <Input
401
- ref={fileInputRef}
402
- type="file"
403
- accept=".csv"
404
- onChange={handleFileSelect}
405
- className="hidden"
406
- />
407
- </div>
408
-
409
- {error && (
410
- <div className="flex items-center gap-2 p-3 bg-acc-50 border border-acc-200 rounded text-acc-700">
411
- <AlertCircle className="size-4" />
412
- <span className="text-sm">{error}</span>
413
- </div>
414
- )}
415
-
416
- {validationErrors.length > 0 && (
417
- <div className="space-y-2">
418
- <div className="flex items-center gap-2 p-3 bg-acc-50 border border-acc-200 rounded text-acc-700">
419
- <AlertCircle className="size-4" />
420
- <span className="text-sm font-medium">
421
- {validationErrors.length} validation error{validationErrors.length !== 1 ? 's' : ''} found
394
+ {file && (
395
+ <span className="block text-xs text-sec-500 mt-1">
396
+ File selected, processing preview...
422
397
  </span>
423
- </div>
424
- <div className="max-h-32 overflow-y-auto space-y-1">
425
- {validationErrors.slice(0, 10).map((err, idx) => (
426
- <div key={idx} className="text-xs text-acc-600 p-2 bg-acc-50 border border-acc-200 rounded">
427
- <span className="font-medium">Row {err.row}</span> • {err.field}: {err.message}
428
- </div>
429
- ))}
430
- {validationErrors.length > 10 && (
431
- <div className="text-xs text-sec-500 italic">
432
- ... and {validationErrors.length - 10} more errors
433
- </div>
434
- )}
435
- </div>
436
- </div>
437
- )}
438
-
398
+ )}
399
+ </CardDescription>
400
+ <CardContent className="text-center">
401
+ {error && (
402
+ <>
403
+ <AlertCircle className="inline-block size-4 mr-2" />
404
+ <span className="text-sm">{error}</span>
405
+ </>
406
+ )}
407
+
408
+ {validationErrors.length > 0 && (
409
+ <>
410
+ <AlertCircle className="size-4" />
411
+ <span className="text-sm font-medium">
412
+ {validationErrors.length} validation error{validationErrors.length !== 1 ? 's' : ''} found
413
+ </span>
414
+ {validationErrors.slice(0, 10).map((err, idx) => (
415
+ <p key={idx} className="text-xs text-acc-600 p-2 bg-acc-50 border border-acc-200 rounded">
416
+ <span className="font-medium">Row {err.row}</span> • {err.field}: {err.message}
417
+ </p>
418
+ ))}
419
+ {validationErrors.length > 10 && (
420
+ <p className="text-xs text-sec-500 italic">
421
+ ... and {validationErrors.length - 10} more errors
422
+ </p>
423
+ )}
424
+ </>
425
+ )}
439
426
 
440
- {importProgress && isProcessing && (
441
- <div className="space-y-2 p-4 bg-sec-50 rounded-lg border border-sec-200">
442
- <div className="flex items-center justify-between">
443
- <span className="text-sm font-medium text-sec-900">
444
- {importProgress.stage === 'parsing' ? 'Parsing CSV file...' : 'Importing data...'}
445
- </span>
446
- <span className="text-sm text-sec-600">
447
- {importProgress.current.toLocaleString()} / {importProgress.total.toLocaleString()} rows
448
- </span>
449
- </div>
450
- <Progress
451
- value={importProgress.total > 0 ? (importProgress.current / importProgress.total) * 100 : 0}
452
- className="h-2 bg-sec-200"
427
+ {importProgress && isProcessing && (
428
+ <>
429
+ <span className="text-sm font-medium text-sec-900">
430
+ {importProgress.stage === 'parsing' ? 'Parsing CSV file...' : 'Importing data...'}
431
+ </span>
432
+ <span className="text-sm text-sec-600">
433
+ {importProgress.current.toLocaleString()} / {importProgress.total.toLocaleString()} rows
434
+ </span>
435
+ <Progress
436
+ value={importProgress.total > 0 ? (importProgress.current / importProgress.total) * 100 : 0}
437
+ className="h-2 bg-sec-200"
438
+ />
439
+ <p className="text-xs text-sec-500">
440
+ {importProgress.total > 0 && importProgress.current < importProgress.total
441
+ ? `${Math.round((importProgress.current / importProgress.total) * 100)}% complete`
442
+ : importProgress.stage === 'importing' && importProgress.current === 0
443
+ ? 'Processing your data...'
444
+ : importProgress.current === importProgress.total
445
+ ? 'Complete!'
446
+ : 'Processing...'}
447
+ </p>
448
+ </>
449
+ )}
450
+ </CardContent>
451
+ <CardFooter>
452
+ <Button
453
+ variant="outline"
454
+ size="sm"
455
+ onClick={() => fileInputRef.current?.click()}
456
+ >
457
+ <Upload className="size-4 mr-2" />
458
+ {selectFileButtonText}
459
+ </Button>
460
+ <Input
461
+ ref={fileInputRef}
462
+ type="file"
463
+ accept=".csv"
464
+ onChange={handleFileSelect}
465
+ className="hidden"
453
466
  />
454
- <p className="text-xs text-sec-500">
455
- {importProgress.total > 0 && importProgress.current < importProgress.total
456
- ? `${Math.round((importProgress.current / importProgress.total) * 100)}% complete`
457
- : importProgress.stage === 'importing' && importProgress.current === 0
458
- ? 'Processing your data...'
459
- : importProgress.current === importProgress.total
460
- ? 'Complete!'
461
- : 'Processing...'}
462
- </p>
463
- </div>
464
- )}
467
+ </CardFooter>
468
+ </Card>
465
469
 
466
470
  {file && previewData && previewData.length > 0 && !isProcessing ? (
467
- <div className="space-y-3">
468
- <h4 className="text-sec-900">{previewHeaderText}</h4>
469
- <div className="border rounded-lg overflow-hidden">
470
- <div className="overflow-x-auto max-h-48">
471
- <table className="min-w-full text-xs">
472
- <thead className="bg-sec-50">
473
- <tr>
471
+ <Card>
472
+ <CardHeader>
473
+ <CardTitle>{previewHeaderText}</CardTitle>
474
+ </CardHeader>
475
+ <CardContent className="overflow-x-auto">
476
+ <Table className="text-xs min-w-full">
477
+ <TableHeader>
478
+ <TableRow>
474
479
  {Object.keys(previewData[0]).map((header) => (
475
- <th key={header} className="px-2 py-1 text-left font-medium text-sec-900 border-b">
480
+ <TableHead key={header} >
476
481
  {header}
477
- </th>
482
+ </TableHead>
478
483
  ))}
479
- </tr>
480
- </thead>
481
- <tbody>
484
+ </TableRow>
485
+ </TableHeader>
486
+ <TableBody>
482
487
  {previewData.map((row, index) => (
483
- <tr key={index} className={index % 2 === 0 ? 'bg-app-main-50' : 'bg-sec-50'}>
488
+ <TableRow key={index} >
484
489
  {Object.values(row).map((value, cellIndex) => (
485
- <td key={cellIndex} className="px-2 py-1 text-sec-700 border-b">
490
+ <TableCell key={cellIndex} >
486
491
  {String(value)}
487
- </td>
492
+ </TableCell>
488
493
  ))}
489
- </tr>
494
+ </TableRow>
490
495
  ))}
491
- </tbody>
492
- </table>
493
- </div>
494
- </div>
495
- <p className="text-sec-500">
496
- {totalRowsText.replace('{count}', totalCount.toString())}
497
- </p>
498
- </div>
499
- ) : !file ? (
500
- <div className="border rounded-lg p-6 text-center bg-sec-50">
501
- <p className="text-sec-500">
502
- Select a CSV file to preview
503
- </p>
504
- </div>
496
+ </TableBody>
497
+ </Table>
498
+ </CardContent>
499
+ <CardFooter>
500
+ {totalRowsText.replace('{count}', totalCount.toString())}
501
+ </CardFooter>
502
+ </Card>
505
503
  ) : null}
506
504
 
507
- <div className="flex justify-end gap-2">
508
- <Button variant="outline" onClick={handleClose}>
509
- {cancelButtonText}
510
- </Button>
511
- <Button
512
- onClick={handleImport}
513
- disabled={!file || isProcessing}
514
- >
515
- {isProcessing ? importButtonProcessingText : importButtonText}
516
- </Button>
517
- </div>
518
- </div>
505
+
506
+ <DialogFooter>
507
+ <Button variant="outline" onClick={handleClose}>
508
+ {cancelButtonText}
509
+ </Button>
510
+ <Button
511
+ onClick={handleImport}
512
+ disabled={!file || isProcessing}
513
+ >
514
+ {isProcessing ? importButtonProcessingText : importButtonText}
515
+ </Button>
516
+ </DialogFooter>
519
517
  </DialogContent>
520
518
  </Dialog>
521
519
  );
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { LoadingSpinner } from '../../LoadingSpinner/LoadingSpinner';
2
3
 
3
4
  /**
4
5
  * Loading state component for DataTable.
@@ -8,11 +9,9 @@ import React from 'react';
8
9
  */
9
10
  export function LoadingState() {
10
11
  return (
11
- <div className="p-8 text-center">
12
- <div className="flex items-center justify-center space-x-2">
13
- <div className="animate-spin rounded-full size-6 border-b-2 border-primary"></div>
14
- <span aria-live="polite" className="text-muted-foreground">Loading...</span>
15
- </div>
16
- </div>
12
+ <p className="grid place-items-center text-center p-8">
13
+ <LoadingSpinner />
14
+ <strong>Loading...</strong>
15
+ </p>
17
16
  );
18
17
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @file Sort Indicator Component
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components
5
+ * @since 0.4.0
6
+ *
7
+ * Shared component for displaying column sort indicators.
8
+ * Provides consistent sorting chevron icons across all DataTable components.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';
13
+ import { cn } from '../../../utils/core/cn';
14
+
15
+ /**
16
+ * Props for the SortIndicator component
17
+ */
18
+ export interface SortIndicatorProps {
19
+ /** Current sort state: 'asc' for ascending, 'desc' for descending, false for unsorted */
20
+ sortState: 'asc' | 'desc' | false;
21
+ /** Optional className for styling */
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * Sort indicator component that displays the appropriate chevron icon
27
+ * based on the current sort state.
28
+ *
29
+ * @param props - Sort indicator configuration
30
+ * @returns The rendered sort indicator icon
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <SortIndicator sortState={column.getIsSorted()} />
35
+ * ```
36
+ */
37
+ export function SortIndicator({ sortState, className }: SortIndicatorProps) {
38
+ return (
39
+ <>
40
+ {sortState === 'asc' ? (
41
+ <ChevronUp className={cn('size-4', className)} />
42
+ ) : sortState === 'desc' ? (
43
+ <ChevronDown className={cn('size-4', className)} />
44
+ ) : (
45
+ <ChevronsUpDown className={cn('size-4', className)} />
46
+ )}
47
+ </>
48
+ );
49
+ }
50
+
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Summary
4
4
 
5
- The DataTable subcomponents (FilterRow, EditableRow, ColumnFilter, GroupHeader, ViewRowModal, etc.) are **intentionally not tested in isolation** because:
5
+ The DataTable subcomponents (FilterRow, EditableRow, ColumnFilter, etc.) are **intentionally not tested in isolation** because:
6
6
 
7
7
  1. They are tightly integrated with TanStack React Table
8
8
  2. They are extensively tested through DataTable integration tests
@@ -35,9 +35,9 @@ The following DataTable integration tests provide comprehensive coverage:
35
35
  | **ColumnFilter** | ✅ | Integration tests validate filter input behavior |
36
36
  | **FilterRow** | ✅ | Integration tests validate filtering across columns |
37
37
  | **EditableRow** | ✅ | Integration tests validate editing workflows |
38
- | **GroupHeader** | ✅ | Integration tests validate grouping functionality |
39
- | **ViewRowModal** | ✅ | Integration tests validate modal display |
40
- | **DraggableColumnHeader** | ✅ | Integration tests validate column reordering |
38
+ | **Grouped rows** | ✅ | Integration tests validate grouping functionality (handled inline by RowComponent) |
39
+ | **Modal display** | ✅ | Integration tests validate modal functionality |
40
+ | **Column reordering** | ✅ | Integration tests validate column order persistence |
41
41
  | **ActionButtons** | ✅ | Integration tests validate action buttons |
42
42
 
43
43
  ## Why This Approach Works
@@ -57,8 +57,8 @@ vi.mock('../../Select/Select', async () => {
57
57
  {children}
58
58
  </button>
59
59
  ),
60
- SelectValue: ({ placeholder }: any) => (
61
- <span data-testid="select-value">{placeholder}</span>
60
+ SelectValue: ({ children }: any) => (
61
+ <span data-testid="select-value">{children}</span>
62
62
  ),
63
63
  SelectContent: ({ children }: any) => (
64
64
  <div data-testid="select-content">{children}</div>
@@ -449,79 +449,6 @@ describe('[component] ColumnFilter', () => {
449
449
  });
450
450
  });
451
451
 
452
- describe('Clear Filter Button', () => {
453
- it('shows clear button when filter has value', () => {
454
- const column = createMockColumn({
455
- getFilterValue: vi.fn(() => 'test'),
456
- });
457
-
458
- render(<ColumnFilter column={column} />);
459
-
460
- const clearButton = screen.getByRole('button');
461
- expect(clearButton).toBeInTheDocument();
462
- expect(screen.getByTestId('x-icon')).toBeInTheDocument();
463
- });
464
-
465
- it('hides clear button when filter has no value', () => {
466
- const column = createMockColumn({
467
- getFilterValue: vi.fn(() => undefined),
468
- });
469
-
470
- render(<ColumnFilter column={column} />);
471
-
472
- expect(screen.queryByTestId('x-icon')).not.toBeInTheDocument();
473
- });
474
-
475
- it('hides clear button when filter value is empty string', () => {
476
- const column = createMockColumn({
477
- getFilterValue: vi.fn(() => ''),
478
- });
479
-
480
- render(<ColumnFilter column={column} />);
481
-
482
- expect(screen.queryByTestId('x-icon')).not.toBeInTheDocument();
483
- });
484
-
485
- it('clears filter when clear button is clicked', async () => {
486
- const user = userEvent.setup();
487
- const setFilterValue = vi.fn();
488
- const column = createMockColumn({
489
- getFilterValue: vi.fn(() => 'test'),
490
- setFilterValue,
491
- });
492
-
493
- render(<ColumnFilter column={column} />);
494
-
495
- const clearButton = screen.getByRole('button');
496
- await user.click(clearButton);
497
-
498
- expect(setFilterValue).toHaveBeenCalledWith(undefined);
499
- });
500
- });
501
-
502
- describe('Filter Indicator', () => {
503
- it('shows filter indicator dot when filter has value', () => {
504
- const column = createMockColumn({
505
- getFilterValue: vi.fn(() => 'test'),
506
- });
507
-
508
- render(<ColumnFilter column={column} />);
509
-
510
- const indicator = document.querySelector('.bg-main-500.rounded-full');
511
- expect(indicator).toBeInTheDocument();
512
- });
513
-
514
- it('hides filter indicator dot when filter has no value', () => {
515
- const column = createMockColumn({
516
- getFilterValue: vi.fn(() => undefined),
517
- });
518
-
519
- render(<ColumnFilter column={column} />);
520
-
521
- const indicator = document.querySelector('.bg-main-500.rounded-full');
522
- expect(indicator).not.toBeInTheDocument();
523
- });
524
- });
525
452
 
526
453
  describe('Edge Cases', () => {
527
454
  it('handles undefined filter value gracefully', () => {
@@ -618,15 +545,29 @@ describe('[component] ColumnFilter', () => {
618
545
  expect(input).toBeInTheDocument();
619
546
  });
620
547
 
621
- it('clear button is accessible', () => {
622
- const column = createMockColumn({
623
- getFilterValue: vi.fn(() => 'test'),
624
- });
548
+ it('renders Filter icon in select filter', () => {
549
+ render(
550
+ <ColumnFilter
551
+ column={mockColumn}
552
+ filterType="select"
553
+ options={[{ value: 'option1', label: 'Option 1' }]}
554
+ />
555
+ );
625
556
 
626
- render(<ColumnFilter column={column} />);
557
+ expect(screen.getByTestId('filter-icon')).toBeInTheDocument();
558
+ });
559
+
560
+ it('renders column name in select filter value', () => {
561
+ render(
562
+ <ColumnFilter
563
+ column={mockColumn}
564
+ filterType="select"
565
+ options={[{ value: 'option1', label: 'Option 1' }]}
566
+ />
567
+ );
627
568
 
628
- const clearButton = screen.getByRole('button');
629
- expect(clearButton).toBeInTheDocument();
569
+ const selectValue = screen.getByTestId('select-value');
570
+ expect(selectValue).toHaveTextContent('test-column...');
630
571
  });
631
572
  });
632
573
 
@@ -89,8 +89,9 @@ describe('[component] DataTableErrorBoundary', () => {
89
89
  </DataTableErrorBoundary>
90
90
  );
91
91
 
92
- // Alert component uses role="alert", not data-testid
93
- expect(screen.getByRole('alert')).toBeInTheDocument();
92
+ // There may be multiple alerts (outer and inner), use getAllByRole
93
+ const alerts = screen.getAllByRole('alert');
94
+ expect(alerts.length).toBeGreaterThan(0);
94
95
  expect(screen.getByText('DataTable Error')).toBeInTheDocument();
95
96
  expect(screen.getByText('Something went wrong')).toBeInTheDocument();
96
97
  });
@@ -127,8 +128,16 @@ describe('[component] DataTableErrorBoundary', () => {
127
128
  </DataTableErrorBoundary>
128
129
  );
129
130
 
131
+ // Check for error details summary
130
132
  const details = screen.getByText('Error Details');
131
133
  expect(details).toBeInTheDocument();
134
+ // Check that error message is displayed in the details/pre element
135
+ // The error message is inside a <pre> tag within <details>
136
+ const preElement = screen.getByText((content, element) => {
137
+ return element?.tagName.toLowerCase() === 'pre' && content.includes('Test error');
138
+ }, { selector: 'pre' });
139
+ expect(preElement).toBeInTheDocument();
140
+ expect(preElement).toHaveTextContent('Test error');
132
141
  });
133
142
 
134
143
  it('hides error details when showErrorDetails is false', () => {
@@ -140,10 +149,11 @@ describe('[component] DataTableErrorBoundary', () => {
140
149
 
141
150
  // Error details section is shown when error.message exists
142
151
  // showErrorDetails only controls stack trace visibility, not the details section
143
- // So we check that the details section exists but stack trace is not shown
144
152
  expect(screen.getByText('Error Details')).toBeInTheDocument();
145
153
  // Stack trace should not be visible when showErrorDetails is false
146
154
  expect(screen.queryByText(/Stack Trace/i)).not.toBeInTheDocument();
155
+ // Error message should still be visible
156
+ expect(screen.getByText('Test error')).toBeInTheDocument();
147
157
  });
148
158
 
149
159
  it('displays stack trace when showErrorDetails is true', () => {
@@ -155,6 +165,8 @@ describe('[component] DataTableErrorBoundary', () => {
155
165
 
156
166
  // Stack trace should be in the details section
157
167
  expect(screen.getByText('Error Details')).toBeInTheDocument();
168
+ // Stack trace text should be present when showErrorDetails is true
169
+ expect(screen.getByText(/Stack Trace/i)).toBeInTheDocument();
158
170
  });
159
171
  });
160
172
 
@@ -237,7 +249,8 @@ describe('[component] DataTableErrorBoundary', () => {
237
249
  </DataTableErrorBoundary>
238
250
  );
239
251
 
240
- expect(screen.getByRole('alert')).toBeInTheDocument();
252
+ const alerts = screen.getAllByRole('alert');
253
+ expect(alerts.length).toBeGreaterThan(0);
241
254
 
242
255
  const retryButton = screen.getByRole('button', { name: /retry/i });
243
256
  await user.click(retryButton);
@@ -317,7 +330,8 @@ describe('[component] DataTableErrorBoundary', () => {
317
330
  </DataTableErrorBoundary>
318
331
  );
319
332
 
320
- expect(screen.getByRole('alert')).toBeInTheDocument();
333
+ const alerts = screen.getAllByRole('alert');
334
+ expect(alerts.length).toBeGreaterThan(0);
321
335
 
322
336
  const resetButton = screen.getByRole('button', { name: /reset/i });
323
337
 
@@ -352,7 +366,9 @@ describe('[component] DataTableErrorBoundary', () => {
352
366
  </DataTableErrorBoundary>
353
367
  );
354
368
 
355
- expect(screen.getByRole('alert')).toBeInTheDocument();
369
+ // There may be multiple alerts when error has no message
370
+ const alerts = screen.getAllByRole('alert');
371
+ expect(alerts.length).toBeGreaterThan(0);
356
372
  expect(screen.getByText('An unexpected error occurred')).toBeInTheDocument();
357
373
  });
358
374
 
@@ -365,19 +381,31 @@ describe('[component] DataTableErrorBoundary', () => {
365
381
  </DataTableErrorBoundary>
366
382
  );
367
383
 
368
- expect(screen.getByRole('alert')).toBeInTheDocument();
384
+ const alerts = screen.getAllByRole('alert');
385
+ expect(alerts.length).toBeGreaterThan(0);
369
386
 
370
387
  const retryButton = screen.getByRole('button', { name: /retry/i });
371
388
  await user.click(retryButton);
372
389
 
373
- // Trigger another error
390
+ // Wait for retry to complete and error state to reset
391
+ await waitFor(() => {
392
+ // Wait for the timeout to complete
393
+ }, { timeout: 200 });
394
+
395
+ // Trigger another error - rerender with new error
374
396
  rerender(
375
397
  <DataTableErrorBoundary showRetryButton={true}>
376
398
  <ThrowError shouldThrow={true} message="Second error" />
377
399
  </DataTableErrorBoundary>
378
400
  );
379
401
 
380
- expect(screen.getByRole('alert')).toBeInTheDocument();
402
+ // Wait for the new error to be caught and displayed
403
+ await waitFor(() => {
404
+ const newAlerts = screen.getAllByRole('alert');
405
+ expect(newAlerts.length).toBeGreaterThan(0);
406
+ const preElement = document.querySelector('pre');
407
+ expect(preElement).toHaveTextContent('Second error');
408
+ }, { timeout: 300 });
381
409
  });
382
410
 
383
411
  it('handles cleanup on unmount', () => {
@@ -306,7 +306,8 @@ describe('[component] EmptyState', () => {
306
306
  it('uses semantic heading for title', () => {
307
307
  render(<EmptyState title="Custom Title" />);
308
308
 
309
- const heading = screen.getByRole('heading', { level: 3 });
309
+ // AlertTitle renders as h5, not h3
310
+ const heading = screen.getByRole('heading', { level: 5 });
310
311
  expect(heading).toHaveTextContent('Custom Title');
311
312
  });
312
313
 
@@ -410,11 +411,12 @@ describe('[component] EmptyState', () => {
410
411
  });
411
412
 
412
413
  describe('Layout and Styling', () => {
413
- it('renders with centered flex layout', () => {
414
+ it('renders with centered grid layout', () => {
414
415
  render(<EmptyState />);
415
416
 
416
417
  const container = screen.getByRole('status');
417
- expect(container).toHaveClass('flex', 'flex-col', 'items-center', 'justify-center');
418
+ // EmptyState uses grid place-items-center, not flex
419
+ expect(container).toHaveClass('grid', 'place-items-center');
418
420
  });
419
421
 
420
422
  it('applies text-center class', () => {
@@ -428,7 +430,8 @@ describe('[component] EmptyState', () => {
428
430
  render(<EmptyState />);
429
431
 
430
432
  const container = screen.getByRole('status');
431
- expect(container).toHaveClass('p-8');
433
+ // EmptyState uses p-4, not p-8
434
+ expect(container).toHaveClass('p-4');
432
435
  });
433
436
  });
434
437
  });