@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
@@ -11,7 +11,7 @@ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
11
11
 
12
12
  describe('Alert Component', () => {
13
13
  describe('Rendering', () => {
14
- it('renders as semantic aside element', () => {
14
+ it('renders as semantic p element with role="alert"', () => {
15
15
  renderWithProviders(
16
16
  <Alert>
17
17
  <AlertTitle>Test Title</AlertTitle>
@@ -20,8 +20,9 @@ describe('Alert Component', () => {
20
20
  );
21
21
 
22
22
  const alert = screen.getByRole('alert');
23
- expect(alert.tagName).toBe('ASIDE');
23
+ expect(alert.tagName).toBe('P');
24
24
  expect(alert).toBeInTheDocument();
25
+ expect(alert).toHaveAttribute('role', 'alert');
25
26
  });
26
27
 
27
28
  it('renders with default variant', () => {
@@ -34,7 +35,8 @@ describe('Alert Component', () => {
34
35
 
35
36
  const alert = screen.getByRole('alert');
36
37
  expect(alert).toBeInTheDocument();
37
- expect(alert.tagName).toBe('ASIDE');
38
+ expect(alert.tagName).toBe('P');
39
+ expect(alert).toHaveAttribute('role', 'alert');
38
40
  expect(alert).toHaveClass('relative', 'w-full', 'rounded-lg', 'border', 'p-4');
39
41
  });
40
42
 
@@ -48,7 +50,8 @@ describe('Alert Component', () => {
48
50
 
49
51
  const alert = screen.getByRole('alert');
50
52
  expect(alert).toBeInTheDocument();
51
- expect(alert.tagName).toBe('ASIDE');
53
+ expect(alert.tagName).toBe('P');
54
+ expect(alert).toHaveAttribute('role', 'alert');
52
55
  expect(alert).toHaveClass('border-destructive', 'text-destructive');
53
56
  });
54
57
 
@@ -73,7 +76,8 @@ describe('Alert Component', () => {
73
76
  );
74
77
 
75
78
  const alert = screen.getByRole('alert');
76
- expect(alert.tagName).toBe('ASIDE');
79
+ expect(alert.tagName).toBe('P');
80
+ expect(alert).toHaveAttribute('role', 'alert');
77
81
  expect(alert).toHaveClass('custom-alert-class');
78
82
  });
79
83
 
@@ -88,7 +92,7 @@ describe('Alert Component', () => {
88
92
  });
89
93
 
90
94
  it('forwards ref correctly', () => {
91
- const ref = React.createRef<HTMLElement>();
95
+ const ref = React.createRef<HTMLParagraphElement>();
92
96
 
93
97
  renderWithProviders(
94
98
  <Alert ref={ref}>
@@ -96,8 +100,8 @@ describe('Alert Component', () => {
96
100
  </Alert>
97
101
  );
98
102
 
99
- expect(ref.current).toBeInstanceOf(HTMLElement);
100
- expect(ref.current?.tagName).toBe('ASIDE');
103
+ expect(ref.current).toBeInstanceOf(HTMLParagraphElement);
104
+ expect(ref.current?.tagName).toBe('P');
101
105
  expect(ref.current).toHaveAttribute('role', 'alert');
102
106
  });
103
107
  });
@@ -275,7 +279,54 @@ describe('Alert Component', () => {
275
279
 
276
280
  const alert = screen.getByRole('alert');
277
281
  expect(alert).toBeInTheDocument();
278
- expect(alert.tagName).toBe('ASIDE');
282
+ expect(alert.tagName).toBe('P');
283
+ expect(alert).toHaveAttribute('role', 'alert');
284
+ });
285
+
286
+ it('supports role="status" prop for informational messages', () => {
287
+ renderWithProviders(
288
+ <Alert role="status" aria-live="polite">
289
+ <AlertTitle>Status Message</AlertTitle>
290
+ <AlertDescription>This is a status message</AlertDescription>
291
+ </Alert>
292
+ );
293
+
294
+ const status = screen.getByRole('status');
295
+ expect(status).toBeInTheDocument();
296
+ expect(status.tagName).toBe('P');
297
+ expect(status).toHaveAttribute('role', 'status');
298
+ expect(status).toHaveAttribute('aria-live', 'polite');
299
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
300
+ });
301
+
302
+ it('defaults to role="alert" when role prop not provided', () => {
303
+ renderWithProviders(
304
+ <Alert>
305
+ <AlertTitle>Default Alert</AlertTitle>
306
+ <AlertDescription>This should default to alert role</AlertDescription>
307
+ </Alert>
308
+ );
309
+
310
+ const alert = screen.getByRole('alert');
311
+ expect(alert).toBeInTheDocument();
312
+ expect(alert).toHaveAttribute('role', 'alert');
313
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
314
+ });
315
+
316
+ it('allows custom role values', () => {
317
+ renderWithProviders(
318
+ <Alert role="region" aria-label="Custom region">
319
+ <AlertTitle>Custom Role</AlertTitle>
320
+ <AlertDescription>This uses a custom role</AlertDescription>
321
+ </Alert>
322
+ );
323
+
324
+ const region = screen.getByRole('region', { name: 'Custom region' });
325
+ expect(region).toBeInTheDocument();
326
+ expect(region.tagName).toBe('P');
327
+ expect(region).toHaveAttribute('role', 'region');
328
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
329
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
279
330
  });
280
331
 
281
332
  it('does not have role="alert" for inline variant', () => {
@@ -301,7 +352,8 @@ describe('Alert Component', () => {
301
352
 
302
353
  const alert = screen.getByRole('alert');
303
354
  expect(alert).toBeInTheDocument();
304
- expect(alert.tagName).toBe('ASIDE');
355
+ expect(alert.tagName).toBe('P');
356
+ expect(alert).toHaveAttribute('role', 'alert');
305
357
 
306
358
  // Screen readers will announce the content within the alert
307
359
  expect(screen.getByText('Important Notice')).toBeInTheDocument();
@@ -320,7 +372,8 @@ describe('Alert Component', () => {
320
372
  const title = screen.getByRole('heading', { level: 5 });
321
373
  const description = screen.getByText('Semantic description with proper heading structure');
322
374
 
323
- expect(alert.tagName).toBe('ASIDE');
375
+ expect(alert.tagName).toBe('P');
376
+ expect(alert).toHaveAttribute('role', 'alert');
324
377
  expect(title).toBeInTheDocument();
325
378
  expect(description.tagName).toBe('P');
326
379
  });
@@ -339,7 +392,8 @@ describe('Alert Component', () => {
339
392
 
340
393
  const alert = screen.getByRole('alert');
341
394
  expect(alert).toBeInTheDocument();
342
- expect(alert.tagName).toBe('ASIDE');
395
+ expect(alert.tagName).toBe('P');
396
+ expect(alert).toHaveAttribute('role', 'alert');
343
397
  expect(screen.getByText('⚠️')).toBeInTheDocument();
344
398
  expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
345
399
  });
@@ -355,7 +409,8 @@ describe('Alert Component', () => {
355
409
 
356
410
  const alert = screen.getByRole('alert');
357
411
  expect(alert).toBeInTheDocument();
358
- expect(alert.tagName).toBe('ASIDE');
412
+ expect(alert.tagName).toBe('P');
413
+ expect(alert).toHaveAttribute('role', 'alert');
359
414
  expect(screen.getByText('First description')).toBeInTheDocument();
360
415
  expect(screen.getByText('Second description')).toBeInTheDocument();
361
416
  });
@@ -369,7 +424,8 @@ describe('Alert Component', () => {
369
424
 
370
425
  const alert = screen.getByRole('alert');
371
426
  expect(alert).toBeInTheDocument();
372
- expect(alert.tagName).toBe('ASIDE');
427
+ expect(alert.tagName).toBe('P');
428
+ expect(alert).toHaveAttribute('role', 'alert');
373
429
  expect(screen.getByText('Description without title')).toBeInTheDocument();
374
430
  });
375
431
 
@@ -382,7 +438,8 @@ describe('Alert Component', () => {
382
438
 
383
439
  const alert = screen.getByRole('alert');
384
440
  expect(alert).toBeInTheDocument();
385
- expect(alert.tagName).toBe('ASIDE');
441
+ expect(alert.tagName).toBe('P');
442
+ expect(alert).toHaveAttribute('role', 'alert');
386
443
  expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent('Title without description');
387
444
  });
388
445
  });
@@ -393,7 +450,8 @@ describe('Alert Component', () => {
393
450
 
394
451
  const alert = screen.getByRole('alert');
395
452
  expect(alert).toBeInTheDocument();
396
- expect(alert.tagName).toBe('ASIDE');
453
+ expect(alert.tagName).toBe('P');
454
+ expect(alert).toHaveAttribute('role', 'alert');
397
455
  expect(alert).toBeEmptyDOMElement();
398
456
  });
399
457
 
@@ -423,7 +481,8 @@ describe('Alert Component', () => {
423
481
  // Should fallback to default behavior
424
482
  const alert = screen.getByRole('alert');
425
483
  expect(alert).toBeInTheDocument();
426
- expect(alert.tagName).toBe('ASIDE');
484
+ expect(alert.tagName).toBe('P');
485
+ expect(alert).toHaveAttribute('role', 'alert');
427
486
  });
428
487
 
429
488
  it('handles rapid variant changes', () => {
@@ -454,7 +513,8 @@ describe('Alert Component', () => {
454
513
 
455
514
  const alert = screen.getByRole('alert');
456
515
  expect(alert).toBeInTheDocument();
457
- expect(alert.tagName).toBe('ASIDE');
516
+ expect(alert.tagName).toBe('P');
517
+ expect(alert).toHaveAttribute('role', 'alert');
458
518
  });
459
519
  });
460
520
 
@@ -473,7 +533,8 @@ describe('Alert Component', () => {
473
533
 
474
534
  const alert = screen.getByRole('alert');
475
535
  expect(alert).toBeInTheDocument();
476
- expect(alert.tagName).toBe('ASIDE');
536
+ expect(alert.tagName).toBe('P');
537
+ expect(alert).toHaveAttribute('role', 'alert');
477
538
  expect(screen.getByRole('textbox')).toBeInTheDocument();
478
539
  expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
479
540
  });
@@ -494,8 +555,10 @@ describe('Alert Component', () => {
494
555
 
495
556
  const alerts = screen.getAllByRole('alert');
496
557
  expect(alerts).toHaveLength(2);
497
- expect(alerts[0].tagName).toBe('ASIDE');
498
- expect(alerts[1].tagName).toBe('ASIDE');
558
+ expect(alerts[0].tagName).toBe('P');
559
+ expect(alerts[1].tagName).toBe('P');
560
+ expect(alerts[0]).toHaveAttribute('role', 'alert');
561
+ expect(alerts[1]).toHaveAttribute('role', 'alert');
499
562
  expect(alerts[0]).toHaveClass('bg-background', 'text-foreground');
500
563
  expect(alerts[1]).toHaveClass('border-destructive', 'text-destructive');
501
564
  });
@@ -516,7 +579,8 @@ describe('Alert Component', () => {
516
579
 
517
580
  const alert = screen.getByRole('alert');
518
581
  expect(alert).toBeInTheDocument();
519
- expect(alert.tagName).toBe('ASIDE');
582
+ expect(alert.tagName).toBe('P');
583
+ expect(alert).toHaveAttribute('role', 'alert');
520
584
  expect(screen.getByText('Complex Alert')).toBeInTheDocument();
521
585
  expect(screen.getByText('This is a complex description with')).toBeInTheDocument();
522
586
  expect(screen.getByText('Multiple elements')).toBeInTheDocument();
@@ -10,27 +10,32 @@
10
10
  * Features:
11
11
  * - Multiple visual variants (default, destructive, inline)
12
12
  * - Title and description support
13
- * - Semantic HTML: renders as `<aside>` element
14
- * - ARIA role="alert" for accessibility
13
+ * - Semantic HTML: renders as `<p>` element with `role="alert"` (default) or custom role
15
14
  * - Keyboard and screen reader accessible
16
15
  * - Composable with icons and actions
17
16
  * - Inline variant for lightweight text formatting
18
17
  *
19
18
  * @example
20
19
  * ```tsx
21
- * // Basic alert (renders as <aside> with <h5> title and <p> description)
20
+ * // Basic alert (renders as <p role="alert"> with <h5> title and <p> description)
22
21
  * <Alert>
23
22
  * <AlertTitle>Success</AlertTitle>
24
23
  * <AlertDescription>Your changes have been saved.</AlertDescription>
25
24
  * </Alert>
26
25
  *
27
- * // Destructive alert with icon (renders as <aside> with <h5> title and <p> description)
26
+ * // Destructive alert with icon (renders as <p role="alert"> with <h5> title and <p> description)
28
27
  * <Alert variant="destructive">
29
28
  * <ErrorIcon />
30
29
  * <AlertTitle>Error</AlertTitle>
31
30
  * <AlertDescription>Something went wrong.</AlertDescription>
32
31
  * </Alert>
33
32
  *
33
+ * // Status message (renders as <p role="status"> for informational messages)
34
+ * <Alert role="status" aria-live="polite">
35
+ * <AlertTitle>No data available</AlertTitle>
36
+ * <AlertDescription>Get started by adding your first entry.</AlertDescription>
37
+ * </Alert>
38
+ *
34
39
  * // Inline alert (renders as React.Fragment with <strong> title and <span> description)
35
40
  * <Alert variant="inline">
36
41
  * <AlertTitle>Note:</AlertTitle>
@@ -39,8 +44,8 @@
39
44
  * ```
40
45
  *
41
46
  * @accessibility
42
- * - Uses semantic HTML: `<aside>` element for better semantic meaning
43
- * - Uses role="alert" for screen reader announcement
47
+ * - Uses semantic HTML: `<p>` element with `role="alert"` (default) for screen reader announcements
48
+ * - Can be customized with `role="status"` for informational messages
44
49
  * - Title and description are semantically structured
45
50
  * - Supports keyboard navigation and focus
46
51
  */
@@ -65,9 +70,12 @@ const getAlertClasses = (variant: "default" | "destructive" | "inline" = "defaul
65
70
  };
66
71
 
67
72
  const Alert = React.forwardRef<
68
- HTMLElement,
69
- React.HTMLAttributes<HTMLElement> & { variant?: "default" | "destructive" | "inline" }
70
- >(({ className, variant = "default", ...props }, ref) => {
73
+ HTMLParagraphElement,
74
+ React.HTMLAttributes<HTMLParagraphElement> & {
75
+ variant?: "default" | "destructive" | "inline";
76
+ role?: string;
77
+ }
78
+ >(({ className, variant = "default", role = "alert", ...props }, ref) => {
71
79
  const contextValue = React.useMemo(() => ({ variant }), [variant])
72
80
 
73
81
  if (variant === "inline") {
@@ -80,10 +88,10 @@ const Alert = React.forwardRef<
80
88
 
81
89
  return (
82
90
  <AlertContext.Provider value={contextValue}>
83
- <aside
91
+ <p
84
92
  ref={ref}
85
93
  className={cn(getAlertClasses(variant), className)}
86
- role="alert"
94
+ role={role}
87
95
  {...props}
88
96
  />
89
97
  </AlertContext.Provider>
@@ -163,7 +163,7 @@ function buildVariantClasses(style: Style, color: Color, shade: Shade): string {
163
163
  * Classes used: shadow-badge-soft shadow-main-200 shadow-main-500 shadow-main-700
164
164
  * shadow-sec-200 shadow-sec-500 shadow-sec-700 shadow-acc-200 shadow-acc-500 shadow-acc-700
165
165
  */
166
- const tailwindClassScan = [
166
+ const _tailwindClassScan = [
167
167
  // Solid background classes
168
168
  'bg-main-100', 'bg-main-600', 'bg-main-900',
169
169
  'bg-sec-100', 'bg-sec-600', 'bg-sec-900',
@@ -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
@@ -474,8 +474,9 @@ describe('Checkbox Component', () => {
474
474
  const endTime = performance.now();
475
475
 
476
476
  // Performance test: verify rendering completes in reasonable time
477
+ // Note: Performance can vary based on system load, so we use a more lenient threshold
477
478
  expect(screen.getAllByRole('checkbox')).toHaveLength(100);
478
- expect(endTime - startTime).toBeLessThan(1000);
479
+ expect(endTime - startTime).toBeLessThan(3000);
479
480
  });
480
481
  });
481
482
  });