@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
@@ -37,18 +37,18 @@ const UserProfile = ({ userId }: { userId: string }) => {
37
37
  fetchUser();
38
38
  }, [userId]);
39
39
 
40
- if (loading) return <div>Loading...</div>;
41
- if (error) return <div>Error: {error}</div>;
42
- if (!user) return <div>No user found</div>;
40
+ if (loading) return <p>Loading...</p>;
41
+ if (error) return <p>Error: {error}</p>;
42
+ if (!user) return <p>No user found</p>;
43
43
 
44
44
  return (
45
- <div data-testid="user-profile">
45
+ <section data-testid="user-profile">
46
46
  <h1>{user.name}</h1>
47
47
  <p>{user.email}</p>
48
48
  <button onClick={() => setUser({ ...user, name: 'Updated Name' })}>
49
49
  Update Name
50
50
  </button>
51
- </div>
51
+ </section>
52
52
  );
53
53
  };
54
54
 
@@ -59,17 +59,17 @@ describe('User Profile Integration Tests', () => {
59
59
 
60
60
  // The component loads data synchronously in this mock, so we just check the final state
61
61
  await waitFor(() => {
62
- expect(screen.getByText('John Doe')).toBeInTheDocument();
62
+ expect(screen.getByText('John Doe')).toBeDefined();
63
63
  });
64
64
 
65
- expect(screen.getByText('john@example.com')).toBeInTheDocument();
65
+ expect(screen.getByText('john@example.com')).toBeDefined();
66
66
  });
67
67
 
68
68
  it('handles user not found', async () => {
69
69
  renderWithProviders(<UserProfile userId="999" />);
70
70
 
71
71
  await waitFor(() => {
72
- expect(screen.getByText('Error: User not found')).toBeInTheDocument();
72
+ expect(screen.getByText('Error: User not found')).toBeDefined();
73
73
  });
74
74
  });
75
75
  });
@@ -80,13 +80,13 @@ describe('User Profile Integration Tests', () => {
80
80
  renderWithProviders(<UserProfile userId="1" />);
81
81
 
82
82
  await waitFor(() => {
83
- expect(screen.getByText('John Doe')).toBeInTheDocument();
83
+ expect(screen.getByText('John Doe')).toBeDefined();
84
84
  });
85
85
 
86
86
  await user.click(screen.getByText('Update Name'));
87
87
 
88
- expect(screen.getByText('Updated Name')).toBeInTheDocument();
89
- expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
88
+ expect(screen.getByText('Updated Name')).toBeDefined();
89
+ expect(screen.queryByText('John Doe')).toBeNull();
90
90
  });
91
91
  });
92
92
 
@@ -95,12 +95,12 @@ describe('User Profile Integration Tests', () => {
95
95
  const { rerender } = renderWithProviders(<UserProfile userId="1" />);
96
96
 
97
97
  await waitFor(() => {
98
- expect(screen.getByText('John Doe')).toBeInTheDocument();
98
+ expect(screen.getByText('John Doe')).toBeDefined();
99
99
  });
100
100
 
101
101
  rerender(<UserProfile userId="1" />);
102
102
 
103
- expect(screen.getByText('John Doe')).toBeInTheDocument();
103
+ expect(screen.getByText('John Doe')).toBeDefined();
104
104
  });
105
105
  });
106
106
  });
@@ -117,7 +117,7 @@ describe('User Profile - Different Users', () => {
117
117
  renderWithProviders(<UserProfile userId={userId} />);
118
118
 
119
119
  await waitFor(() => {
120
- expect(screen.getByText(expectedName)).toBeInTheDocument();
120
+ expect(screen.getByText(expectedName)).toBeDefined();
121
121
  });
122
122
  });
123
123
  });
@@ -78,7 +78,7 @@ describe('PagePermissionGuard', () => {
78
78
  operation="read"
79
79
  scope={mockScope}
80
80
  >
81
- <div>Protected Content</div>
81
+ <p>Protected Content</p>
82
82
  </PagePermissionGuard>
83
83
  );
84
84
 
@@ -103,7 +103,7 @@ describe('PagePermissionGuard', () => {
103
103
  operation="read"
104
104
  scope={mockScope}
105
105
  >
106
- <div>Protected Content</div>
106
+ <p>Protected Content</p>
107
107
  </PagePermissionGuard>
108
108
  );
109
109
 
@@ -128,7 +128,7 @@ describe('PagePermissionGuard', () => {
128
128
  operation="read"
129
129
  scope={mockScope}
130
130
  >
131
- <div>Protected Content</div>
131
+ <p>Protected Content</p>
132
132
  </PagePermissionGuard>
133
133
  );
134
134
 
@@ -153,7 +153,7 @@ describe('PagePermissionGuard', () => {
153
153
  operation="read"
154
154
  scope={mockScope}
155
155
  >
156
- <div>Protected Content</div>
156
+ <p>Protected Content</p>
157
157
  </PagePermissionGuard>
158
158
  );
159
159
 
@@ -176,7 +176,7 @@ describe('PagePermissionGuard', () => {
176
176
  operation="read"
177
177
  scope={mockScope}
178
178
  >
179
- <div>Protected Content</div>
179
+ <p>Protected Content</p>
180
180
  </PagePermissionGuard>
181
181
  );
182
182
  };
@@ -207,7 +207,7 @@ describe('PagePermissionGuard', () => {
207
207
  operation="read"
208
208
  scope={mockScope}
209
209
  >
210
- <div>Protected Content</div>
210
+ <p>Protected Content</p>
211
211
  </PagePermissionGuard>
212
212
  );
213
213
 
@@ -32,9 +32,9 @@ describe('ComponentName Accessibility Tests', () => {
32
32
  renderWithProviders(<ComponentName>Content</ComponentName>);
33
33
 
34
34
  // Check for proper semantic elements
35
- expect(screen.getByRole('main')).toBeInTheDocument();
36
- // or: expect(screen.getByRole('button')).toBeInTheDocument();
37
- // or: expect(screen.getByRole('textbox')).toBeInTheDocument();
35
+ expect(screen.getByRole('main')).toBeDefined();
36
+ // or: expect(screen.getByRole('button')).toBeDefined();
37
+ // or: expect(screen.getByRole('textbox')).toBeDefined();
38
38
  });
39
39
 
40
40
  it('has proper heading hierarchy', () => {
@@ -53,13 +53,13 @@ describe('ComponentName Accessibility Tests', () => {
53
53
  renderWithProviders(<ComponentName aria-label="Component description" />);
54
54
 
55
55
  const component = screen.getByLabelText('Component description');
56
- expect(component).toBeInTheDocument();
56
+ expect(component).toBeDefined();
57
57
  });
58
58
 
59
59
  it('has proper ARIA roles', () => {
60
60
  renderWithProviders(<ComponentName role="dialog" />);
61
61
 
62
- expect(screen.getByRole('dialog')).toBeInTheDocument();
62
+ expect(screen.getByRole('dialog')).toBeDefined();
63
63
  });
64
64
 
65
65
  it('uses ARIA describedby for additional information', () => {
@@ -92,10 +92,10 @@ describe('ComponentName Accessibility Tests', () => {
92
92
  describe('Keyboard Navigation', () => {
93
93
  it('supports tab navigation', async () => {
94
94
  renderWithProviders(
95
- <div>
95
+ <section>
96
96
  <ComponentName />
97
97
  <button>Next focusable element</button>
98
- </div>
98
+ </section>
99
99
  );
100
100
 
101
101
  const firstElement = screen.getByRole('button', { name: /component/i });
@@ -179,7 +179,7 @@ describe('ComponentName Accessibility Tests', () => {
179
179
  renderWithProviders(<ComponentName type="form" />);
180
180
 
181
181
  const input = screen.getByLabelText('Input Label');
182
- expect(input).toBeInTheDocument();
182
+ expect(input).toBeDefined();
183
183
  });
184
184
 
185
185
  it('announces loading states', () => {
@@ -204,7 +204,7 @@ describe('ComponentName Accessibility Tests', () => {
204
204
  renderWithProviders(<ComponentName status="error" />);
205
205
 
206
206
  // Should have text indicator, not just color
207
- expect(screen.getByText(/error/i)).toBeInTheDocument();
207
+ expect(screen.getByText(/error/i)).toBeDefined();
208
208
  });
209
209
 
210
210
  it('maintains focus indicators', async () => {
@@ -8,6 +8,7 @@ import React from 'react';
8
8
  import { screen } from '@testing-library/react';
9
9
  import userEvent from '@testing-library/user-event';
10
10
  import { describe, it, expect, vi, beforeEach } from 'vitest';
11
+ // @ts-ignore - Template file: Replace with your actual component import
11
12
  import { ComponentName } from '../../components/ComponentName/ComponentName';
12
13
  import { renderWithProviders } from '../helpers/test-utils';
13
14
 
@@ -20,21 +21,21 @@ describe('ComponentName Component', () => {
20
21
  describe('Rendering', () => {
21
22
  it('renders with default props', () => {
22
23
  renderWithProviders(<ComponentName />);
23
- expect(screen.getByRole('generic')).toBeInTheDocument();
24
+ expect(screen.getByRole('generic')).toBeDefined();
24
25
  });
25
26
 
26
27
  it('renders with custom props', () => {
27
28
  renderWithProviders(<ComponentName customProp="value" />);
28
- expect(screen.getByRole('generic')).toBeInTheDocument();
29
+ expect(screen.getByRole('generic')).toBeDefined();
29
30
  });
30
31
 
31
32
  it('renders with children', () => {
32
33
  renderWithProviders(
33
34
  <ComponentName>
34
- <span>Child content</span>
35
+ <p>Child content</p>
35
36
  </ComponentName>
36
37
  );
37
- expect(screen.getByText('Child content')).toBeInTheDocument();
38
+ expect(screen.getByText('Child content')).toBeDefined();
38
39
  });
39
40
  });
40
41
 
@@ -69,15 +70,15 @@ describe('ComponentName Component', () => {
69
70
  <ComponentName value="initial" />
70
71
  );
71
72
 
72
- expect(screen.getByDisplayValue('initial')).toBeInTheDocument();
73
+ expect(screen.getByDisplayValue('initial')).toBeDefined();
73
74
 
74
75
  rerender(<ComponentName value="updated" />);
75
- expect(screen.getByDisplayValue('updated')).toBeInTheDocument();
76
+ expect(screen.getByDisplayValue('updated')).toBeDefined();
76
77
  });
77
78
 
78
79
  it('handles uncontrolled state', () => {
79
80
  renderWithProviders(<ComponentName defaultValue="default" />);
80
- expect(screen.getByDisplayValue('default')).toBeInTheDocument();
81
+ expect(screen.getByDisplayValue('default')).toBeDefined();
81
82
  });
82
83
  });
83
84
 
@@ -85,12 +86,14 @@ describe('ComponentName Component', () => {
85
86
  describe('Accessibility', () => {
86
87
  it('has proper ARIA attributes', () => {
87
88
  renderWithProviders(<ComponentName aria-label="Test component" />);
88
- expect(screen.getByRole('generic')).toHaveAttribute('aria-label', 'Test component');
89
+ const element = screen.getByRole('generic');
90
+ expect(element.getAttribute('aria-label')).toBe('Test component');
89
91
  });
90
92
 
91
93
  it('is keyboard accessible', () => {
92
94
  renderWithProviders(<ComponentName />);
93
- expect(screen.getByRole('generic')).not.toHaveAttribute('tabindex', '-1');
95
+ const element = screen.getByRole('generic');
96
+ expect(element.getAttribute('tabindex')).not.toBe('-1');
94
97
  });
95
98
 
96
99
  it('supports screen readers', () => {
@@ -102,25 +105,25 @@ describe('ComponentName Component', () => {
102
105
  // Error handling tests
103
106
  describe('Error Handling', () => {
104
107
  it('handles invalid props gracefully', () => {
105
- // @ts-expect-error Testing invalid prop
108
+ // @ts-ignore Testing invalid prop
106
109
  renderWithProviders(<ComponentName invalidProp="test" />);
107
- expect(screen.getByRole('generic')).toBeInTheDocument();
110
+ expect(screen.getByRole('generic')).toBeDefined();
108
111
  });
109
112
 
110
113
  it('displays error states', () => {
111
114
  renderWithProviders(<ComponentName error="Something went wrong" />);
112
- expect(screen.getByText('Something went wrong')).toBeInTheDocument();
115
+ expect(screen.getByText('Something went wrong')).toBeDefined();
113
116
  });
114
117
  });
115
118
 
116
119
  // Integration tests
117
120
  describe('Integration', () => {
118
121
  it('works with other components', () => {
119
- renderWithProviders(
120
- <div>
122
+ renderWithProviders(
123
+ <p>
121
124
  <ComponentName />
122
125
  <ComponentName />
123
- </div>
126
+ </p>
124
127
  );
125
128
 
126
129
  expect(screen.getAllByRole('generic')).toHaveLength(2);
@@ -65,7 +65,9 @@ import {
65
65
  type DateRange,
66
66
  } from 'react-day-picker';
67
67
  import { enAU } from 'date-fns/locale';
68
+ import { format } from 'date-fns';
68
69
  import { cn } from '../../utils/core/cn';
70
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../Select';
69
71
 
70
72
  // Define custom types for components that don't have exported types
71
73
  type MonthGridProps = React.TableHTMLAttributes<HTMLTableElement>;
@@ -77,6 +79,9 @@ type MonthProps = {
77
79
  displayIndex: number;
78
80
  className?: string;
79
81
  children?: React.ReactNode;
82
+ captionLayout?: DayPickerProps['captionLayout'];
83
+ startMonth?: Date;
84
+ endMonth?: Date;
80
85
  };
81
86
  type RootProps = {
82
87
  children?: React.ReactNode;
@@ -190,7 +195,7 @@ const assignToRef = <T,>(ref: React.Ref<T | null> | undefined, value: T | null)
190
195
  * ```
191
196
  */
192
197
  const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
193
- ({ className, classNames, mode, components, locale, month: controlledMonth, onMonthChange: controlledOnMonthChange, onSelect, ...props }, ref) => {
198
+ ({ className, classNames, mode, components, locale, month: controlledMonth, onMonthChange: controlledOnMonthChange, onSelect, captionLayout, startMonth, endMonth, ...props }, ref) => {
194
199
  const tableRef = React.useRef<HTMLTableElement | null>(null);
195
200
  const setForwardedRef = React.useCallback(
196
201
  (node: HTMLTableElement | null) => {
@@ -267,9 +272,42 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
267
272
  });
268
273
  CustomRoot.displayName = 'CustomRoot';
269
274
 
270
- // Custom Months: Remove wrapper div, return children directly
275
+ // Custom Months: Remove wrapper div, filter out MonthCaption and Dropdown when dropdown layout is used
271
276
  const CustomMonths = React.memo(({ children }: MonthsProps) => {
272
- return <>{children}</>;
277
+ // When captionLayout="dropdown", react-day-picker may render MonthCaption or Dropdown components
278
+ // Filter them out since we render our own dropdowns inside the table's <caption> element
279
+ const childrenArray = React.Children.toArray(children);
280
+ const filteredChildren = childrenArray.filter((child: any) => {
281
+ if (!React.isValidElement(child)) return true;
282
+ const childType = child.type as any;
283
+ const displayName = childType?.displayName || childType?.name;
284
+ // Filter out MonthCaption and any Dropdown-related components
285
+ if (displayName === 'MonthCaption' ||
286
+ displayName === 'Dropdown' ||
287
+ displayName === 'DropdownMonth' ||
288
+ displayName === 'DropdownYear' ||
289
+ (typeof childType === 'string' && childType.includes('dropdown'))) {
290
+ return false;
291
+ }
292
+ // Also check for the div wrapper that react-day-picker might render
293
+ if (childType === 'div') {
294
+ const childProps = child.props as { children?: React.ReactNode; [key: string]: unknown };
295
+ if (childProps?.children) {
296
+ const childChildren = React.Children.toArray(childProps.children);
297
+ // If it contains a span with role="status" and aria-live="polite", it's likely the default caption
298
+ const hasCaptionSpan = childChildren.some((cc: any) => {
299
+ if (!React.isValidElement(cc) || cc.type !== 'span') return false;
300
+ const spanProps = cc.props as { role?: string; 'aria-live'?: string; [key: string]: unknown };
301
+ return spanProps?.role === 'status' && spanProps?.['aria-live'] === 'polite';
302
+ });
303
+ if (hasCaptionSpan) {
304
+ return false;
305
+ }
306
+ }
307
+ }
308
+ return true;
309
+ });
310
+ return <>{filteredChildren}</>;
273
311
  });
274
312
  CustomMonths.displayName = 'CustomMonths';
275
313
 
@@ -277,9 +315,93 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
277
315
  return <table ref={forwardedRef} {...props} />;
278
316
  });
279
317
  CustomMonthGrid.displayName = 'CustomMonthGrid';
280
-
318
+
319
+ // Custom MonthCaption: renders dropdowns for month/year selection
320
+ type MonthCaptionProps = {
321
+ displayMonth: Date;
322
+ startMonth?: Date;
323
+ endMonth?: Date;
324
+ locale?: DayPickerProps['locale'];
325
+ };
326
+ const CustomMonthCaption = React.memo(({ displayMonth, startMonth: captionStartMonth, endMonth: captionEndMonth, locale: captionLocale }: MonthCaptionProps) => {
327
+ const { goToMonth } = useDayPicker();
328
+ // Get locale from props (defaults to enAU)
329
+ const calendarLocale = (captionLocale || enAU) as typeof enAU;
330
+
331
+ // Get start and end months from props (passed via Calendar)
332
+ const fromDate = captionStartMonth || new Date(1900, 0);
333
+ const toDate = captionEndMonth || new Date(2100, 11);
334
+
335
+ // Generate month options using date-fns format
336
+ const monthOptions = React.useMemo(() => {
337
+ const months: { value: string; label: string }[] = [];
338
+ for (let i = 0; i < 12; i++) {
339
+ const monthDate = new Date(displayMonth.getFullYear(), i, 1);
340
+ const label = format(monthDate, 'MMMM', { locale: calendarLocale });
341
+ months.push({ value: i.toString(), label });
342
+ }
343
+ return months;
344
+ }, [calendarLocale, displayMonth]);
345
+
346
+ // Generate year options based on startMonth and endMonth
347
+ const yearOptions = React.useMemo(() => {
348
+ const years: { value: string; label: string }[] = [];
349
+ const startYear = fromDate.getFullYear();
350
+ const endYear = toDate.getFullYear();
351
+ for (let year = startYear; year <= endYear; year++) {
352
+ years.push({ value: year.toString(), label: year.toString() });
353
+ }
354
+ return years;
355
+ }, [fromDate, toDate]);
356
+
357
+ const currentMonth = displayMonth.getMonth();
358
+ const currentYear = displayMonth.getFullYear();
359
+
360
+ const handleMonthChange = React.useCallback((value: string) => {
361
+ const newMonth = parseInt(value, 10);
362
+ const newDate = new Date(currentYear, newMonth, 1);
363
+ goToMonth(newDate);
364
+ }, [currentYear, goToMonth]);
365
+
366
+ const handleYearChange = React.useCallback((value: string) => {
367
+ const newYear = parseInt(value, 10);
368
+ const newDate = new Date(newYear, currentMonth, 1);
369
+ goToMonth(newDate);
370
+ }, [currentMonth, goToMonth]);
371
+
372
+ return (
373
+ <nav className="relative flex items-center justify-center gap-2">
374
+ <Select value={currentMonth.toString()} onValueChange={handleMonthChange}>
375
+ <SelectTrigger className="w-auto min-w-[120px]">
376
+ <SelectValue />
377
+ </SelectTrigger>
378
+ <SelectContent>
379
+ {monthOptions.map((option) => (
380
+ <SelectItem key={option.value} value={option.value}>
381
+ {option.label}
382
+ </SelectItem>
383
+ ))}
384
+ </SelectContent>
385
+ </Select>
386
+ <Select value={currentYear.toString()} onValueChange={handleYearChange}>
387
+ <SelectTrigger className="w-auto min-w-[100px]">
388
+ <SelectValue />
389
+ </SelectTrigger>
390
+ <SelectContent>
391
+ {yearOptions.map((option) => (
392
+ <SelectItem key={option.value} value={option.value}>
393
+ {option.label}
394
+ </SelectItem>
395
+ ))}
396
+ </SelectContent>
397
+ </Select>
398
+ </nav>
399
+ );
400
+ });
401
+ CustomMonthCaption.displayName = 'CustomMonthCaption';
402
+
281
403
  // Custom Month: inject caption + navigation directly inside the <table>
282
- const CustomMonth = React.memo(({ calendarMonth, displayIndex, className, children }: MonthProps) => {
404
+ const CustomMonth = React.memo(({ calendarMonth, displayIndex, className, children, captionLayout: monthCaptionLayout, startMonth: monthStartMonth, endMonth: monthEndMonth }: MonthProps) => {
283
405
  const { formatters, components, labels, classNames, previousMonth, nextMonth, goToMonth } = useDayPicker();
284
406
  const caption = formatters.formatCaption(calendarMonth.date, {});
285
407
  const Chevron = components?.Chevron;
@@ -367,6 +489,9 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
367
489
  }
368
490
  : undefined;
369
491
 
492
+ // Determine if we should render dropdowns or buttons
493
+ const isDropdownLayout = monthCaptionLayout === 'dropdown';
494
+
370
495
  return React.cloneElement(
371
496
  monthGridElement,
372
497
  {
@@ -376,45 +501,54 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
376
501
  },
377
502
  <>
378
503
  <caption className="relative">
379
- <nav className="relative flex items-center justify-center gap-1">
380
- <button
381
- type="button"
382
- className={cn(
383
- 'h-7 w-7 bg-transparent p-0',
384
- 'inline-flex items-center justify-center rounded-md',
385
- 'hover:bg-acc-100',
386
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
387
- 'disabled:opacity-50 disabled:pointer-events-none',
388
- classNames?.button_previous
389
- )}
390
- tabIndex={previousMonth ? undefined : -1}
391
- aria-disabled={previousMonth ? undefined : true}
392
- aria-label={previousMonth ? labels.labelPrevious(previousMonth) : undefined}
393
- onClick={handlePreviousClick}
394
- disabled={!previousMonth}
395
- >
396
- {Chevron ? <Chevron orientation="left" className="size-4" disabled={!previousMonth} /> : <span>‹</span>}
397
- </button>
398
- <span className="text-sm font-medium">{caption}</span>
399
- <button
400
- type="button"
401
- className={cn(
402
- 'h-7 w-7 bg-transparent p-0',
403
- 'inline-flex items-center justify-center rounded-md',
404
- 'hover:bg-acc-100',
405
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
406
- 'disabled:opacity-50 disabled:pointer-events-none',
407
- classNames?.button_next
408
- )}
409
- tabIndex={nextMonth ? undefined : -1}
410
- aria-disabled={nextMonth ? undefined : true}
411
- aria-label={nextMonth ? labels.labelNext(nextMonth) : undefined}
412
- onClick={handleNextClick}
413
- disabled={!nextMonth}
414
- >
415
- {Chevron ? <Chevron orientation="right" className="size-4" disabled={!nextMonth} /> : <span>›</span>}
416
- </button>
417
- </nav>
504
+ {isDropdownLayout ? (
505
+ <CustomMonthCaption
506
+ displayMonth={calendarMonth.date}
507
+ startMonth={monthStartMonth}
508
+ endMonth={monthEndMonth}
509
+ locale={locale}
510
+ />
511
+ ) : (
512
+ <nav className="relative flex items-center justify-center gap-1">
513
+ <button
514
+ type="button"
515
+ className={cn(
516
+ 'h-7 w-7 bg-transparent p-0',
517
+ 'inline-flex items-center justify-center rounded-md',
518
+ 'hover:bg-acc-100',
519
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
520
+ 'disabled:opacity-50 disabled:pointer-events-none',
521
+ classNames?.button_previous
522
+ )}
523
+ tabIndex={previousMonth ? undefined : -1}
524
+ aria-disabled={previousMonth ? undefined : true}
525
+ aria-label={previousMonth ? labels.labelPrevious(previousMonth) : undefined}
526
+ onClick={handlePreviousClick}
527
+ disabled={!previousMonth}
528
+ >
529
+ {Chevron ? <Chevron orientation="left" className="size-4" disabled={!previousMonth} /> : <span>‹</span>}
530
+ </button>
531
+ <span className="text-sm font-medium">{caption}</span>
532
+ <button
533
+ type="button"
534
+ className={cn(
535
+ 'h-7 w-7 bg-transparent p-0',
536
+ 'inline-flex items-center justify-center rounded-md',
537
+ 'hover:bg-acc-100',
538
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
539
+ 'disabled:opacity-50 disabled:pointer-events-none',
540
+ classNames?.button_next
541
+ )}
542
+ tabIndex={nextMonth ? undefined : -1}
543
+ aria-disabled={nextMonth ? undefined : true}
544
+ aria-label={nextMonth ? labels.labelNext(nextMonth) : undefined}
545
+ onClick={handleNextClick}
546
+ disabled={!nextMonth}
547
+ >
548
+ {Chevron ? <Chevron orientation="right" className="size-4" disabled={!nextMonth} /> : <span>›</span>}
549
+ </button>
550
+ </nav>
551
+ )}
418
552
  </caption>
419
553
  {monthGridChildren}
420
554
  </>
@@ -442,17 +576,37 @@ const Calendar = React.forwardRef<HTMLTableElement, CalendarProps>(
442
576
  });
443
577
  CustomWeekdays.displayName = 'CustomWeekdays';
444
578
 
579
+ // Create a wrapper for CustomMonth that passes the required props
580
+ const CustomMonthWithProps = React.useCallback((props: MonthProps) => {
581
+ return (
582
+ <CustomMonth
583
+ {...props}
584
+ captionLayout={captionLayout}
585
+ startMonth={startMonth}
586
+ endMonth={endMonth}
587
+ />
588
+ );
589
+ }, [captionLayout, startMonth, endMonth]);
590
+
591
+ // Custom MonthCaption wrapper: returns null to prevent default rendering
592
+ // The actual caption is rendered inside CustomMonth within the table's <caption> element
593
+ const CustomMonthCaptionWrapper = React.memo((_props: any) => {
594
+ return null;
595
+ });
596
+ CustomMonthCaptionWrapper.displayName = 'CustomMonthCaptionWrapper';
597
+
445
598
  // Memoize components to ensure stable references
446
599
  const defaultComponents = React.useMemo(() => ({
447
600
  Root: CustomRoot,
448
601
  Months: CustomMonths,
449
- Month: CustomMonth,
602
+ Month: CustomMonthWithProps,
450
603
  MonthGrid: CustomMonthGrid,
451
- // MonthCaption is now handled inside CustomMonth (injected into table)
604
+ // MonthCaption returns null - actual caption is rendered in CustomMonth inside <caption>
605
+ MonthCaption: CustomMonthCaptionWrapper,
452
606
  Weekdays: CustomWeekdays,
453
607
  // Spread user components AFTER ours so ours take precedence
454
608
  ...(components || {}),
455
- }), [components, CustomRoot, CustomMonths, CustomMonth, CustomWeekdays]);
609
+ }), [components, CustomRoot, CustomMonths, CustomMonthWithProps, CustomMonthCaptionWrapper, CustomWeekdays]);
456
610
 
457
611
  return (
458
612
  <DayPicker