@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
@@ -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 are multiple alert elements (outer Alert and inner Alert for no message case)
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> (level 5), 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
+ // Component 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
+ // Alert component uses p-4, not p-8
434
+ expect(container).toHaveClass('p-4');
432
435
  });
433
436
  });
434
437
  });