@acronis-platform/ui-react 2.0.0 → 4.0.0

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 (243) hide show
  1. package/dist/components/ui/accordion-container/accordion-container.js +144 -0
  2. package/dist/components/ui/accordion-container/accordion-container.js.map +1 -0
  3. package/dist/components/ui/app-shell-chat/app-shell-chat.js.map +1 -1
  4. package/dist/components/ui/area-chart/area-chart.js +141 -139
  5. package/dist/components/ui/area-chart/area-chart.js.map +1 -1
  6. package/dist/components/ui/avatar/avatar.js +33 -22
  7. package/dist/components/ui/avatar/avatar.js.map +1 -1
  8. package/dist/components/ui/bar-chart/bar-chart.js +295 -293
  9. package/dist/components/ui/bar-chart/bar-chart.js.map +1 -1
  10. package/dist/components/ui/card/card.js +150 -74
  11. package/dist/components/ui/card/card.js.map +1 -1
  12. package/dist/components/ui/card-section/card-section.js +111 -0
  13. package/dist/components/ui/card-section/card-section.js.map +1 -0
  14. package/dist/components/ui/category-bar/category-bar.js +109 -93
  15. package/dist/components/ui/category-bar/category-bar.js.map +1 -1
  16. package/dist/components/ui/chart/chart-palette.js +113 -0
  17. package/dist/components/ui/chart/chart-palette.js.map +1 -0
  18. package/dist/components/ui/chart/chart.js +176 -129
  19. package/dist/components/ui/chart/chart.js.map +1 -1
  20. package/dist/components/ui/chart-state/chart-state-illustrations.js +310 -0
  21. package/dist/components/ui/chart-state/chart-state-illustrations.js.map +1 -0
  22. package/dist/components/ui/chart-state/chart-state.js +48 -34
  23. package/dist/components/ui/chart-state/chart-state.js.map +1 -1
  24. package/dist/components/ui/chart-widget/chart-widget.js +87 -0
  25. package/dist/components/ui/chart-widget/chart-widget.js.map +1 -0
  26. package/dist/components/ui/composed-chart/composed-chart.js +117 -115
  27. package/dist/components/ui/composed-chart/composed-chart.js.map +1 -1
  28. package/dist/components/ui/confidence-cone/confidence-cone.js +166 -158
  29. package/dist/components/ui/confidence-cone/confidence-cone.js.map +1 -1
  30. package/dist/components/ui/data-table/data-table.js +4 -2
  31. package/dist/components/ui/data-table/data-table.js.map +1 -1
  32. package/dist/components/ui/empty-overlay/empty-overlay.js +49 -0
  33. package/dist/components/ui/empty-overlay/empty-overlay.js.map +1 -0
  34. package/dist/components/ui/funnel-chart/funnel-chart.js +162 -161
  35. package/dist/components/ui/funnel-chart/funnel-chart.js.map +1 -1
  36. package/dist/components/ui/histogram/histogram.js +94 -92
  37. package/dist/components/ui/histogram/histogram.js.map +1 -1
  38. package/dist/components/ui/input/input.js.map +1 -1
  39. package/dist/components/ui/label/label.js +2 -9
  40. package/dist/components/ui/label/label.js.map +1 -1
  41. package/dist/components/ui/line-chart/line-chart.js +143 -141
  42. package/dist/components/ui/line-chart/line-chart.js.map +1 -1
  43. package/dist/components/ui/metric/metric.js +74 -185
  44. package/dist/components/ui/metric/metric.js.map +1 -1
  45. package/dist/components/ui/page-content/page-content.js.map +1 -1
  46. package/dist/components/ui/pie-chart/pie-chart.js +201 -195
  47. package/dist/components/ui/pie-chart/pie-chart.js.map +1 -1
  48. package/dist/components/ui/radar-chart/radar-chart.js +133 -131
  49. package/dist/components/ui/radar-chart/radar-chart.js.map +1 -1
  50. package/dist/components/ui/radial-bar-chart/radial-bar-chart.js +297 -265
  51. package/dist/components/ui/radial-bar-chart/radial-bar-chart.js.map +1 -1
  52. package/dist/components/ui/sankey-chart/sankey-chart.js +123 -101
  53. package/dist/components/ui/sankey-chart/sankey-chart.js.map +1 -1
  54. package/dist/components/ui/scatter-chart/scatter-chart.js +76 -74
  55. package/dist/components/ui/scatter-chart/scatter-chart.js.map +1 -1
  56. package/dist/components/ui/section/section.js +157 -31
  57. package/dist/components/ui/section/section.js.map +1 -1
  58. package/dist/components/ui/stepper/stepper.js +24 -24
  59. package/dist/components/ui/stepper/stepper.js.map +1 -1
  60. package/dist/components/ui/stepper-item/stepper-item.js +43 -24
  61. package/dist/components/ui/stepper-item/stepper-item.js.map +1 -1
  62. package/dist/components/ui/table/table.js +27 -27
  63. package/dist/components/ui/table/table.js.map +1 -1
  64. package/dist/components/ui/treemap/treemap.js +133 -124
  65. package/dist/components/ui/treemap/treemap.js.map +1 -1
  66. package/dist/components/ui/trend-indicator/trend-indicator.js +34 -69
  67. package/dist/components/ui/trend-indicator/trend-indicator.js.map +1 -1
  68. package/dist/components/ui/wizard/wizard.js +59 -0
  69. package/dist/components/ui/wizard/wizard.js.map +1 -0
  70. package/dist/hooks/use-wizard.js +75 -0
  71. package/dist/hooks/use-wizard.js.map +1 -0
  72. package/dist/index.js +540 -525
  73. package/dist/index.js.map +1 -1
  74. package/dist/llms/accordion-container.md +181 -0
  75. package/dist/llms/accordion.md +124 -0
  76. package/dist/llms/alert.md +225 -0
  77. package/dist/llms/app-shell-chat.md +415 -0
  78. package/dist/llms/area-chart.md +355 -0
  79. package/dist/llms/avatar.md +208 -0
  80. package/dist/llms/bar-chart.md +406 -0
  81. package/dist/llms/breadcrumb.md +326 -0
  82. package/dist/llms/button-group.md +298 -0
  83. package/dist/llms/button-icon-input.md +142 -0
  84. package/dist/llms/button-icon-menu.md +209 -0
  85. package/dist/llms/button-icon.md +128 -0
  86. package/dist/llms/button-menu.md +172 -0
  87. package/dist/llms/button.md +185 -0
  88. package/dist/llms/calendar-panel.md +733 -0
  89. package/dist/llms/calendar.md +188 -0
  90. package/dist/llms/card-filter.md +214 -0
  91. package/dist/llms/card-section.md +293 -0
  92. package/dist/llms/card.md +447 -0
  93. package/dist/llms/carousel-dialog.md +156 -0
  94. package/dist/llms/category-bar.md +193 -0
  95. package/dist/llms/chart-state.md +190 -0
  96. package/dist/llms/chart-widget.md +209 -0
  97. package/dist/llms/chart.md +182 -0
  98. package/dist/llms/checkbox.md +257 -0
  99. package/dist/llms/chip.md +195 -0
  100. package/dist/llms/combobox.md +164 -0
  101. package/dist/llms/composed-chart.md +508 -0
  102. package/dist/llms/confidence-cone.md +277 -0
  103. package/dist/llms/data-table.md +474 -0
  104. package/dist/llms/date-range-picker.md +272 -0
  105. package/dist/llms/description-list.md +131 -0
  106. package/dist/llms/dialog-footer-carousel.md +147 -0
  107. package/dist/llms/dialog-footer-default.md +142 -0
  108. package/dist/llms/dialog-root.md +246 -0
  109. package/dist/llms/dialog-welcome.md +223 -0
  110. package/dist/llms/dialog.md +403 -0
  111. package/dist/llms/dropdown-menu.md +193 -0
  112. package/dist/llms/empty-overlay.md +146 -0
  113. package/dist/llms/empty.md +156 -0
  114. package/dist/llms/field.md +185 -0
  115. package/dist/llms/filter-cards.md +157 -0
  116. package/dist/llms/filter-chips.md +323 -0
  117. package/dist/llms/filter-search.md +512 -0
  118. package/dist/llms/form.md +154 -0
  119. package/dist/llms/funnel-chart.md +333 -0
  120. package/dist/llms/grid.md +80 -0
  121. package/dist/llms/histogram.md +190 -0
  122. package/dist/llms/input-date-picker.md +203 -0
  123. package/dist/llms/input-otp.md +173 -0
  124. package/dist/llms/input-password.md +197 -0
  125. package/dist/llms/input-search.md +170 -0
  126. package/dist/llms/input-select.md +278 -0
  127. package/dist/llms/input-text-area.md +271 -0
  128. package/dist/llms/input-text.md +192 -0
  129. package/dist/llms/line-chart.md +369 -0
  130. package/dist/llms/link.md +209 -0
  131. package/dist/llms/loading.md +130 -0
  132. package/dist/llms/meter.md +177 -0
  133. package/dist/llms/metric.md +204 -0
  134. package/dist/llms/number-field.md +122 -0
  135. package/dist/llms/page-content.md +69 -0
  136. package/dist/llms/page-header.md +182 -0
  137. package/dist/llms/pagination.md +133 -0
  138. package/dist/llms/pie-chart.md +307 -0
  139. package/dist/llms/popover.md +252 -0
  140. package/dist/llms/progress-circle.md +126 -0
  141. package/dist/llms/progress.md +111 -0
  142. package/dist/llms/radar-chart.md +316 -0
  143. package/dist/llms/radial-bar-chart.md +319 -0
  144. package/dist/llms/radio.md +255 -0
  145. package/dist/llms/resizable.md +195 -0
  146. package/dist/llms/sankey-chart.md +208 -0
  147. package/dist/llms/scatter-chart.md +239 -0
  148. package/dist/llms/scroll-area.md +183 -0
  149. package/dist/llms/section.md +401 -0
  150. package/dist/llms/select.md +210 -0
  151. package/dist/llms/separator.md +87 -0
  152. package/dist/llms/sheet.md +202 -0
  153. package/dist/llms/sidebar-primary.md +360 -0
  154. package/dist/llms/sidebar-secondary.md +417 -0
  155. package/dist/llms/skeleton.md +81 -0
  156. package/dist/llms/slider.md +97 -0
  157. package/dist/llms/spinner.md +100 -0
  158. package/dist/llms/stack.md +80 -0
  159. package/dist/llms/stepper-item.md +309 -0
  160. package/dist/llms/stepper.md +251 -0
  161. package/dist/llms/switch.md +140 -0
  162. package/dist/llms/table.md +436 -0
  163. package/dist/llms/tabs.md +197 -0
  164. package/dist/llms/tag.md +139 -0
  165. package/dist/llms/timeline.md +361 -0
  166. package/dist/llms/timer.md +259 -0
  167. package/dist/llms/toast.md +241 -0
  168. package/dist/llms/toggle-group.md +121 -0
  169. package/dist/llms/toolbar.md +453 -0
  170. package/dist/llms/tooltip.md +164 -0
  171. package/dist/llms/treemap.md +254 -0
  172. package/dist/llms/trend-indicator.md +132 -0
  173. package/dist/llms/widget-placeholder.md +169 -0
  174. package/dist/llms/wizard.md +412 -0
  175. package/dist/llms.txt +142 -0
  176. package/dist/react.js +540 -525
  177. package/dist/react.js.map +1 -1
  178. package/dist/src/components/ui/accordion-container/accordion-container.d.ts +44 -0
  179. package/dist/src/components/ui/accordion-container/index.d.ts +1 -0
  180. package/dist/src/components/ui/area-chart/area-chart.d.ts +9 -4
  181. package/dist/src/components/ui/avatar/avatar.d.ts +27 -8
  182. package/dist/src/components/ui/bar-chart/bar-chart.d.ts +9 -4
  183. package/dist/src/components/ui/button/button.d.ts +1 -1
  184. package/dist/src/components/ui/card/card.d.ts +69 -8
  185. package/dist/src/components/ui/card/index.d.ts +1 -1
  186. package/dist/src/components/ui/card-section/card-section.d.ts +50 -0
  187. package/dist/src/components/ui/card-section/index.d.ts +1 -0
  188. package/dist/src/components/ui/category-bar/category-bar.d.ts +10 -4
  189. package/dist/src/components/ui/chart/__stories__/palette-control.d.ts +48 -0
  190. package/dist/src/components/ui/chart/__tests__/chart-palette.test.d.ts +1 -0
  191. package/dist/src/components/ui/chart/chart-palette.d.ts +128 -0
  192. package/dist/src/components/ui/chart/chart.d.ts +66 -15
  193. package/dist/src/components/ui/chart/index.d.ts +4 -2
  194. package/dist/src/components/ui/chart-state/chart-state-illustrations.d.ts +6 -0
  195. package/dist/src/components/ui/chart-state/chart-state.d.ts +13 -2
  196. package/dist/src/components/ui/chart-state/index.d.ts +1 -0
  197. package/dist/src/components/ui/chart-widget/chart-widget.d.ts +64 -0
  198. package/dist/src/components/ui/chart-widget/index.d.ts +1 -0
  199. package/dist/src/components/ui/composed-chart/composed-chart.d.ts +12 -7
  200. package/dist/src/components/ui/confidence-cone/confidence-cone.d.ts +10 -5
  201. package/dist/src/components/ui/dialog/dialog.d.ts +1 -1
  202. package/dist/src/components/ui/empty-overlay/empty-overlay.d.ts +20 -0
  203. package/dist/src/components/ui/empty-overlay/index.d.ts +2 -0
  204. package/dist/src/components/ui/funnel-chart/funnel-chart.d.ts +11 -5
  205. package/dist/src/components/ui/grid/grid.d.ts +1 -1
  206. package/dist/src/components/ui/histogram/histogram.d.ts +10 -4
  207. package/dist/src/components/ui/label/index.d.ts +1 -1
  208. package/dist/src/components/ui/label/label.d.ts +1 -5
  209. package/dist/src/components/ui/line-chart/line-chart.d.ts +9 -4
  210. package/dist/src/components/ui/metric/metric.d.ts +13 -14
  211. package/dist/src/components/ui/pie-chart/pie-chart.d.ts +17 -8
  212. package/dist/src/components/ui/radar-chart/radar-chart.d.ts +10 -5
  213. package/dist/src/components/ui/radial-bar-chart/radial-bar-chart.d.ts +17 -6
  214. package/dist/src/components/ui/sankey-chart/sankey-chart.d.ts +10 -5
  215. package/dist/src/components/ui/scatter-chart/scatter-chart.d.ts +10 -5
  216. package/dist/src/components/ui/section/index.d.ts +1 -1
  217. package/dist/src/components/ui/section/section.d.ts +75 -6
  218. package/dist/src/components/ui/sheet/sheet.d.ts +1 -1
  219. package/dist/src/components/ui/stack/stack.d.ts +1 -1
  220. package/dist/src/components/ui/stepper-item/stepper-item.d.ts +6 -5
  221. package/dist/src/components/ui/tag/tag.d.ts +1 -1
  222. package/dist/src/components/ui/treemap/treemap.d.ts +11 -5
  223. package/dist/src/components/ui/trend-indicator/trend-indicator.d.ts +3 -7
  224. package/dist/src/components/ui/wizard/index.d.ts +1 -0
  225. package/dist/src/components/ui/wizard/wizard.d.ts +6 -0
  226. package/dist/src/hooks/index.d.ts +1 -0
  227. package/dist/src/hooks/use-wizard.d.ts +80 -0
  228. package/dist/src/index.d.ts +5 -3
  229. package/dist/ui-react.css +1 -1
  230. package/package.json +12 -6
  231. package/dist/components/ui/app-shell/app-shell.js +0 -75
  232. package/dist/components/ui/app-shell/app-shell.js.map +0 -1
  233. package/dist/components/ui/auth-layout/auth-layout.js +0 -56
  234. package/dist/components/ui/auth-layout/auth-layout.js.map +0 -1
  235. package/dist/components/ui/collapsible/collapsible.js +0 -22
  236. package/dist/components/ui/collapsible/collapsible.js.map +0 -1
  237. package/dist/src/components/ui/app-shell/app-shell.d.ts +0 -8
  238. package/dist/src/components/ui/app-shell/index.d.ts +0 -1
  239. package/dist/src/components/ui/auth-layout/auth-layout.d.ts +0 -6
  240. package/dist/src/components/ui/auth-layout/index.d.ts +0 -1
  241. package/dist/src/components/ui/collapsible/collapsible.d.ts +0 -5
  242. package/dist/src/components/ui/collapsible/index.d.ts +0 -1
  243. package/dist/src/components/ui/label/label.docs.d.ts +0 -10
@@ -0,0 +1,733 @@
1
+ # CalendarPanel
2
+
3
+ > The Figma "Calendar" panel: a bordered, elevated day-picker panel built on react-day-picker and themed by the dedicated `--ui-calendar-*` token tier. A single `variant` prop selects one of the three selection modes, and each one changes the panel's structure — `single` is one month column with no footer, `multiple` adds a Cancel/Apply footer, and `range` shows two month columns side by side (divided by a vertical rule) sharing one footer. Months are navigated through two `InputSelect` dropdowns in each month's header; there are no prev/next chevrons. `variant` is a plain string union, not a `cva` axis.
4
+
5
+ **Status:** draft · **Category:** forms · **Since:** v0.62.0
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import { CalendarPanel } from '@acronis-platform/ui-react'
11
+ ```
12
+
13
+ ## Quick example
14
+
15
+ ```tsx
16
+ {/* single — commits on click, no footer */}
17
+ <CalendarPanel
18
+ variant="single"
19
+ selected={date}
20
+ onSelect={setDate}
21
+ />
22
+
23
+ {/* multiple — a bounded set of dates, committed via Apply */}
24
+ <CalendarPanel
25
+ variant="multiple"
26
+ max={5}
27
+ selected={dates}
28
+ onSelect={setDates}
29
+ onCancel={close}
30
+ onApply={() => { commit(dates); close(); }}
31
+ />
32
+
33
+ {/* range — two months, one footer */}
34
+ <CalendarPanel
35
+ variant="range"
36
+ selected={range}
37
+ onSelect={setRange}
38
+ disabled={{ before: new Date() }}
39
+ onApply={close}
40
+ />
41
+
42
+ {/* localized labels + a narrower year window */}
43
+ <CalendarPanel
44
+ variant="single"
45
+ monthLabel="Mois"
46
+ yearLabel="Année"
47
+ formatMonthLabel={(d) => d.toLocaleString('fr', { month: 'long' })}
48
+ fromYear={2020}
49
+ toYear={2030}
50
+ />
51
+ ```
52
+
53
+ A self-contained date-picker panel: a bordered, elevated day-picker built on
54
+ `react-day-picker` and themed by the dedicated `--ui-calendar-*` token tier. One
55
+ `variant` prop selects the selection mode _and_ the panel's structure — the
56
+ number of month columns and whether a Cancel/Apply footer exists.
57
+
58
+ Months are navigated through two `InputSelect` dropdowns in each month's header.
59
+ There are no prev/next chevrons: that is the design, not an omission.
60
+
61
+ ## When to Use
62
+
63
+ - A date, a set of dates, or a date range needs to be picked from a **panel with
64
+ its own chrome** — border, rounded corners, shadow.
65
+ - The panel is dropped inside a `Popover` or `Dialog` opened from a trigger (a
66
+ date field, a filter button, a "custom range" menu item).
67
+ - The picker needs a **commit step** — `multiple` / `range` build a selection
68
+ over several clicks, and the footer gives the user an explicit Apply/Cancel.
69
+ - Long-jump navigation matters (e.g. a birthdate 40 years back): the year
70
+ dropdown reaches any year in the `fromYear`…`toYear` window in one interaction.
71
+
72
+ ## When NOT to Use
73
+
74
+ - **An always-visible embedded calendar** with no panel chrome — a month grid
75
+ sitting flush in a page or card. This component always draws the panel border,
76
+ radius and shadow; use a plain calendar grid for that (see `Calendar`).
77
+ - **A text-entry date field.** Use `InputDatePicker`, which owns the field, the
78
+ trigger and its popover.
79
+ - **A range date field.** Use `DateRangePicker`, which owns the trigger, the
80
+ popover and the draft/commit/revert cycle around this panel. (A preset list —
81
+ "Last 7 days", "This month", … — is not currently implemented by any
82
+ component in this repo; neither this panel nor `DateRangePicker` renders
83
+ presets.)
84
+ - **Modal semantics** (focus trap, Escape to close, `role="dialog"`). This is a
85
+ plain panel — wrap it in `Dialog`/`Popover` and let the wrapper own them.
86
+
87
+ ## Variants
88
+
89
+ | Variant | Months | Footer | Selection |
90
+ | ---------- | ------ | ------ | ------------------------------------------------------------------ |
91
+ | `single` | 1 | no | One `Date`; commits on the click itself |
92
+ | `multiple` | 1 | yes | A `Date[]`; clicking toggles a day, bounded by `min` / `max` |
93
+ | `range` | 2 | yes | A `{ from, to }` range; first click sets the start, second the end |
94
+
95
+ The two `range` month columns are divided by a vertical rule drawn as the first
96
+ column's **inline-end** border, so it mirrors under `dir="rtl"`.
97
+
98
+ Every cell — idle, hover, in-range, endpoint — is a flat square. This design has
99
+ no corner-radius differentiation, so range endpoints deliberately get no "pill"
100
+ rounding.
101
+
102
+ ## Parts
103
+
104
+ | Part | Element | Role |
105
+ | --------------- | ------------------------------------ | --------------------------------------------------------------------------- |
106
+ | `root` | `<div data-slot="calendar-panel">` | The bordered, elevated panel; carries `data-variant` |
107
+ | `month-column` | `<div>` | One 248px month (caption + grid); twice for `range`, and owns the divider |
108
+ | `month-caption` | `<header>` | Header bar of one column, holding both dropdowns |
109
+ | `month-select` | `InputSelect` | Month dropdown, named by `monthLabel` |
110
+ | `year-select` | `InputSelect` | Year dropdown over `fromYear`…`toYear`, named by `yearLabel` |
111
+ | `weekdays` | `<tr>` | Weekday abbreviation row, starting at `weekStartsOn` |
112
+ | `day-grid` | `<table role="grid">` | The month's day table |
113
+ | `day` | `<button>` | One date cell (Figma's "CalendarItem"); `data-active` / `data-range-middle` |
114
+ | `footer` | `DialogFooterDefault` + two `Button` | Cancel / Apply; **only** for `multiple` / `range` |
115
+
116
+ ## Quick Examples
117
+
118
+ ### Single date
119
+
120
+ ```tsx
121
+ import { CalendarPanel } from '@acronis-platform/ui-react';
122
+
123
+ function DueDate() {
124
+ const [date, setDate] = React.useState<Date | undefined>();
125
+
126
+ return (
127
+ <CalendarPanel
128
+ variant="single"
129
+ selected={date}
130
+ onSelect={(next) => setDate(next)}
131
+ />
132
+ );
133
+ }
134
+ ```
135
+
136
+ ### Multiple dates, committed via Apply
137
+
138
+ ```tsx
139
+ import { CalendarPanel } from '@acronis-platform/ui-react';
140
+
141
+ function BackupDays({ onClose }: { onClose: () => void }) {
142
+ const [dates, setDates] = React.useState<Date[] | undefined>();
143
+
144
+ return (
145
+ <CalendarPanel
146
+ variant="multiple"
147
+ max={5}
148
+ selected={dates}
149
+ onSelect={(next) => setDates(next)}
150
+ onCancel={onClose}
151
+ onApply={onClose}
152
+ />
153
+ );
154
+ }
155
+ ```
156
+
157
+ ### Range, inside a popover
158
+
159
+ ```tsx
160
+ import {
161
+ CalendarPanel,
162
+ Popover,
163
+ PopoverTrigger,
164
+ PopoverContent,
165
+ Button,
166
+ } from '@acronis-platform/ui-react';
167
+ import type { DateRange } from 'react-day-picker';
168
+
169
+ function ReportRange() {
170
+ const [open, setOpen] = React.useState(false);
171
+ const [range, setRange] = React.useState<DateRange | undefined>();
172
+
173
+ return (
174
+ <Popover open={open} onOpenChange={setOpen}>
175
+ <PopoverTrigger
176
+ render={<Button variant="secondary">Pick a range</Button>}
177
+ />
178
+ <PopoverContent>
179
+ <CalendarPanel
180
+ variant="range"
181
+ selected={range}
182
+ onSelect={(next) => setRange(next)}
183
+ disabled={{ after: new Date() }}
184
+ onCancel={() => setOpen(false)}
185
+ onApply={() => setOpen(false)}
186
+ />
187
+ </PopoverContent>
188
+ </Popover>
189
+ );
190
+ }
191
+ ```
192
+
193
+ ### Localized labels
194
+
195
+ ```tsx
196
+ <CalendarPanel
197
+ variant="single"
198
+ monthLabel="Mois"
199
+ yearLabel="Année"
200
+ cancelLabel="Annuler"
201
+ applyLabel="Appliquer"
202
+ formatMonthLabel={(d) => d.toLocaleString('fr', { month: 'long' })}
203
+ />
204
+ ```
205
+
206
+ ## Notes
207
+
208
+ - **Controlled vs uncontrolled.** Passing `selected` alone seeds the panel, which
209
+ then manages the selection itself; adding `onSelect` makes `selected` the
210
+ single source of truth (react-day-picker's own contract). The same split
211
+ applies to `defaultMonth` vs `month` + `onMonthChange`.
212
+ - **The footer never closes anything.** `onCancel` / `onApply` just report the
213
+ press — the caller reverts, commits and dismisses the surrounding surface.
214
+ - **Every rendered string is a prop.** `monthLabel`, `yearLabel`, `cancelLabel`,
215
+ `applyLabel` and `formatMonthLabel` carry their literals only as defaults, so
216
+ consumers localize at their layer.
217
+
218
+ ## Spec Files
219
+
220
+ | File | Contents |
221
+ | ------------------ | ----------------------------------------------------------------- |
222
+ | `index.yaml` | Identity, status, category, dependencies, Figma link |
223
+ | `anatomy.yaml` | Root, parts, layout, internal state, transitions, states |
224
+ | `api.yaml` | Framework-agnostic contract + framework adapters |
225
+ | `tokens.yaml` | `--ui-calendar-*` token references |
226
+ | `behavior.md` | Given/When/Then behavior scenarios |
227
+ | `accessibility.md` | ARIA roles, keyboard map, screen-reader and contrast requirements |
228
+
229
+ ## Props
230
+
231
+ | Prop | Type | Default | Required | Description |
232
+ |------|------|---------|----------|-------------|
233
+ | `variant` | `'single' | 'multiple' | 'range'` | `'single'` | no | Selection mode *and* panel structure. `single` selects one date and renders one month with no footer. `multiple` selects a set of dates and adds the Cancel/Apply footer. `range` selects a start/end pair, renders two months side by side (divided by a vertical rule) and shares one footer. Also reflected on the root as `data-variant`. |
234
+ | `selected` | `Date | Date[] | DateRange | undefined` | `undefined` | no | The selected value, shaped by `variant` — a `Date` for `single`, a `Date[]` for `multiple`, a `{ from, to }` range for `range`. Uncontrolled (a starting value the panel then manages itself) when no select handler is given; supplying the handler makes this the single source of truth, per react-day-picker's own controlled/uncontrolled contract. |
235
+ | `onSelect` | `(selected: Date | Date[] | DateRange | undefined, triggerDate: Date) => void` | `undefined` | no | Called when the selection changes, with the next value and the date that was clicked. Providing it makes `selected` controlled. Neutral event name: `select`. |
236
+ | `defaultMonth` | `Date` | `the current month` | no | The month shown initially, when the displayed month is uncontrolled. Ignored once `month` is supplied. |
237
+ | `month` | `Date` | `undefined` | no | Controlled displayed month — the first month for `range`. Pair with the month-change event. |
238
+ | `onMonthChange` | `(month: Date) => void` | `undefined` | no | Called when the displayed month changes (i.e. from the header's month or year dropdown). Neutral event name: `month-change`. |
239
+ | `disabled` | `Matcher | Matcher[]` | `undefined` | no | Days that cannot be selected, as a react-day-picker matcher (a date, a date range, a `{ before }`/`{ after }` bound, a day-of-week set, or a predicate) or an array of them. Matched days render in the disabled value color and take no pointer events. |
240
+ | `min` | `number` | `undefined` | no | Minimum count, semantics depending on `variant`: for `multiple`, the number of selected dates; for `range`, the number of nights between the range's start and end (`differenceInCalendarDays(to, from)`). Applies to `multiple` and `range` only; ignored for `single`. |
241
+ | `max` | `number` | `undefined` | no | Maximum count, semantics depending on `variant`: for `multiple`, the number of selected dates; for `range`, the number of nights between the range's start and end (`differenceInCalendarDays(to, from)`). Applies to `multiple` and `range` only; ignored for `single`. |
242
+ | `showOutsideDays` | `boolean` | `true` | no | Render the leading/trailing days of the adjacent months to fill the first and last week rows. Outside days use the disabled value color. |
243
+ | `weekStartsOn` | `0 | 1 | 2 | 3 | 4 | 5 | 6` | `the `locale`'s week start, else 1` | no | First day of the week (0 = Sunday). Defaults to `locale`'s own week start when `locale` is set; falls back to Monday, per the design, when neither is given. |
244
+ | `fromYear` | `number` | `the current year − 50` | no | First year offered by the year dropdown (inclusive) and the earliest month the grid can navigate to — via the dropdown, a controlled `month`, or Shift+PageUp/PageDown. |
245
+ | `toYear` | `number` | `the current year + 50` | no | Last year offered by the year dropdown (inclusive) and the latest month the grid can navigate to — via the dropdown, a controlled `month`, or Shift+PageUp/PageDown. |
246
+ | `locale` | `Locale` | `date-fns `enUS`` | no | Localizes weekday names and the day grid's default accessible labels (e.g. `import { es } from 'react-day-picker/locale'`). Also supplies the fallback week start when `weekStartsOn` is unset. Does not translate `monthLabel`/`yearLabel`/`cancelLabel`/`applyLabel`/ `formatMonthLabel` — those are separate props the consumer must localize themselves. |
247
+ | `dir` | `'ltr' | 'rtl'` | `undefined` | no | Text direction, forwarded to the day grid explicitly. An ancestor's `dir` already mirrors the panel's own layout via logical CSS properties, but the day grid only flips its Arrow Left/Right keyboard roaming when `dir` reaches it directly, so it must also be set here. |
248
+ | `monthLabel` | `string` | `'Month'` | no | Accessible name of the month dropdown. The dropdown has no visible label in the design, so the component supplies one — override it to localize. |
249
+ | `yearLabel` | `string` | `'Year'` | no | Accessible name of the year dropdown. The dropdown has no visible label in the design, so the component supplies one — override it to localize. |
250
+ | `formatMonthLabel` | `(date: Date) => string` | `date-fns `format(date, 'MMMM')`` | no | Formats a month's name for the month dropdown's items and value — the hook for localized or abbreviated month names. |
251
+ | `cancelLabel` | `string` | `'Cancel'` | no | Footer secondary button label. Only rendered for `multiple` / `range`. |
252
+ | `applyLabel` | `string` | `'Apply'` | no | Footer primary button label. Only rendered for `multiple` / `range`. |
253
+ | `onCancel` | `() => void` | `undefined` | no | Called when the footer's Cancel button is pressed. The panel neither reverts the selection nor closes itself — the caller decides. Neutral event name: `cancel`. |
254
+ | `onApply` | `() => void` | `undefined` | no | Called when the footer's Apply button is pressed. The panel does not close itself — the caller commits the selection and dismisses whatever surface holds the panel. Neutral event name: `apply`. Apply is never disabled by the panel, so this can fire with an empty or partial selection (e.g. a `range` with only `from` set) — gate on selection completeness in the handler if that matters to the consumer. |
255
+ | `className` | `string` | `undefined` | no | Extra classes merged onto the root panel — e.g. to override the shadow or the width when the panel is embedded in a popover. |
256
+
257
+ ## Events
258
+
259
+ | Event | Payload | Description |
260
+ |-------|---------|-------------|
261
+ | `select` | `(Date | Date[] | DateRange | undefined, Date)` | Fired when the selection changes (React: `onSelect`). Payload is the next selection plus the date that was clicked. |
262
+ | `month-change` | `Date` | Fired when the displayed month changes via the header dropdowns (React: `onMonthChange`). |
263
+ | `cancel` | `void` | Fired when the footer's Cancel button is pressed (React: `onCancel`). Never fired for `single`, which has no footer. |
264
+ | `apply` | `void` | Fired when the footer's Apply button is pressed (React: `onApply`). Never fired for `single`, which has no footer. Not gated on selection completeness — can fire with an empty or partial selection. |
265
+
266
+ ## Behavior
267
+
268
+ Selection, month navigation and day-grid keyboard roaming are
269
+ `react-day-picker`'s; this component supplies the panel chrome, the
270
+ `InputSelect`-backed month/year header, and the Cancel/Apply footer.
271
+
272
+ ### Structure
273
+
274
+ #### Renders the bordered panel
275
+
276
+ **Given** a CalendarPanel with no props
277
+ **When** it renders
278
+ **Then** the root is a `<div data-slot="calendar-panel">` with
279
+ `data-variant="single"`
280
+ **And** it draws the `--ui-calendar-container-*` border, radius and surface plus a
281
+ shadow
282
+
283
+ #### `single` renders one month and no footer
284
+
285
+ **Given** `variant="single"`
286
+ **When** the panel renders
287
+ **Then** exactly one month column is shown
288
+ **And** no footer is rendered — there is no Cancel or Apply button anywhere in
289
+ the panel
290
+
291
+ #### `multiple` renders one month and the footer
292
+
293
+ **Given** `variant="multiple"`
294
+ **When** the panel renders
295
+ **Then** exactly one month column is shown
296
+ **And** a footer with a secondary Cancel button and a primary Apply button is
297
+ rendered below the grid
298
+
299
+ #### `range` renders two months sharing one footer
300
+
301
+ **Given** `variant="range"`
302
+ **When** the panel renders
303
+ **Then** two month columns are shown side by side, the second showing the month
304
+ after the first
305
+ **And** each column has its own month/year header
306
+ **And** the first column draws the vertical divider as its inline-end border, so
307
+ the rule sits between the columns and mirrors under `dir="rtl"`
308
+ **And** a single footer spans the whole panel below both columns
309
+
310
+ ---
311
+
312
+ ### Single-date selection
313
+
314
+ #### Selects a date
315
+
316
+ **Given** `variant="single"`
317
+ **When** a day cell is clicked
318
+ **Then** that day gets `data-active="true"` and takes the active cell
319
+ background + active value color
320
+ **And** the select event fires with the clicked `Date` and the trigger date
321
+
322
+ #### Replaces the previous date
323
+
324
+ **Given** `variant="single"` with a day already selected
325
+ **When** a different day is clicked
326
+ **Then** only the newly clicked day carries `data-active="true"`
327
+ **And** the select event fires with the new `Date`
328
+
329
+ #### Commits immediately
330
+
331
+ **Given** `variant="single"`
332
+ **When** a day is clicked
333
+ **Then** the selection is final — there is no footer to confirm it with
334
+
335
+ ---
336
+
337
+ ### Multi-date selection
338
+
339
+ #### Adds and removes dates
340
+
341
+ **Given** `variant="multiple"`
342
+ **When** several day cells are clicked in turn
343
+ **Then** each clicked day carries `data-active="true"`
344
+ **And** the select event fires each time with the full `Date[]`
345
+ **And** clicking an already-selected day removes it from the array
346
+
347
+ #### Honors `max`
348
+
349
+ **Given** `variant="multiple"` with `max={3}` and three dates already selected
350
+ **When** a fourth day is clicked
351
+ **Then** the selection does not grow past three dates
352
+ **And** removing one of the selected days frees a slot again
353
+
354
+ #### Honors `min`
355
+
356
+ **Given** `variant="multiple"` with `min={2}` and exactly two dates selected
357
+ **When** one of the selected days is clicked to deselect it
358
+ **Then** the selection is not reduced below two dates
359
+
360
+ ---
361
+
362
+ ### Range selection
363
+
364
+ #### Picks a start, then an end
365
+
366
+ **Given** `variant="range"` with nothing selected
367
+ **When** a day is clicked
368
+ **Then** it becomes the range start and carries `data-active="true"`
369
+ **And** the select event fires with `{ from }` only
370
+ **When** a later day is then clicked
371
+ **Then** it becomes the range end and also carries `data-active="true"`
372
+ **And** the select event fires with `{ from, to }`
373
+
374
+ #### Highlights the days between the endpoints
375
+
376
+ **Given** a completed range
377
+ **When** the grid renders
378
+ **Then** every day strictly between the endpoints carries
379
+ `data-range-middle="true"` and takes the hover-tier fill with the primary value
380
+ color
381
+ **And** those days do **not** carry `data-active` — the two attributes are
382
+ mutually exclusive, so the endpoint and in-range backgrounds never compete
383
+ **And** no cell is rounded: endpoints get no "pill" treatment in this design
384
+
385
+ #### Spans both months
386
+
387
+ **Given** `variant="range"`
388
+ **When** the start is picked in the first month column and the end in the second
389
+ **Then** the in-range highlighting continues across both grids
390
+
391
+ #### Honors `min` / `max`
392
+
393
+ **Given** `variant="range"` with `max={7}`
394
+ **When** an end date more than seven days after the start is clicked
395
+ **Then** the range is not accepted beyond the allowed length
396
+
397
+ ---
398
+
399
+ ### Month / year navigation
400
+
401
+ #### Navigates by month
402
+
403
+ **Given** `variant="single"` or `variant="multiple"`, or the **first** column
404
+ of `variant="range"`
405
+ **When** a month is chosen in that column's month dropdown
406
+ **Then** the grid re-renders on that month of the same year
407
+ **And** the month-change event fires with the new month
408
+
409
+ **Given** the **second** column of `variant="range"`
410
+ **When** a month is chosen in that column's month dropdown
411
+ **Then** both columns re-render, the second showing the chosen month
412
+ **And** the month-change event fires with the month **one before** the chosen
413
+ one — `month` is always the first displayed month, so choosing month M in the
414
+ second column reports M − 1
415
+
416
+ #### Navigates by year
417
+
418
+ **Given** `variant="single"` or `variant="multiple"`, or the **first** column
419
+ of `variant="range"`
420
+ **When** a year is chosen in that column's year dropdown
421
+ **Then** the grid re-renders on the same month of that year
422
+ **And** the month-change event fires with the new month
423
+
424
+ **Given** the **second** column of `variant="range"`
425
+ **When** a year is chosen in that column's year dropdown
426
+ **Then** both columns re-render, the second showing the chosen year
427
+ **And** the month-change event fires with the month one before the chosen
428
+ column's month, in the chosen year (same asymmetry as month navigation above)
429
+
430
+ #### There are no prev/next chevrons
431
+
432
+ **Given** any variant
433
+ **When** the panel renders
434
+ **Then** no previous/next month buttons exist — the two dropdowns are the only
435
+ navigation affordance, per the design
436
+
437
+ #### Year window is configurable
438
+
439
+ **Given** `fromYear={2020}` and `toYear={2030}`
440
+ **When** the year dropdown is opened
441
+ **Then** it lists exactly 2020 through 2030
442
+ **And** with neither prop it spans the current year ±50
443
+
444
+ #### Year window also clamps grid navigation
445
+
446
+ **Given** `fromYear={2020}` and `toYear={2030}`
447
+ **When** the grid is navigated past that window — a controlled `month`
448
+ outside the range, or Shift+PageUp/PageDown repeated past an edge
449
+ **Then** the displayed month does not move past January 2020 or December 2030
450
+ **And** this clamp applies even though the year dropdown's option list is a
451
+ separate concern from grid navigation
452
+
453
+ #### Month names are formattable
454
+
455
+ **Given** a `formatMonthLabel` returning abbreviated or localized names
456
+ **When** the month dropdown renders
457
+ **Then** its items and its displayed value use those strings
458
+
459
+ #### Uncontrolled vs controlled month
460
+
461
+ **Given** `defaultMonth` set and no `month`
462
+ **When** the panel renders and a dropdown is used
463
+ **Then** the panel starts on `defaultMonth` and then manages the displayed month
464
+ itself
465
+
466
+ **Given** `month` supplied
467
+ **When** a dropdown is used
468
+ **Then** the month-change event fires but the grid only moves once the consumer
469
+ passes a new `month` back
470
+
471
+ ---
472
+
473
+ ### Footer actions
474
+
475
+ #### Cancel
476
+
477
+ **Given** `variant="multiple"` or `"range"`
478
+ **When** the Cancel button is pressed
479
+ **Then** the cancel event fires
480
+ **And** the panel neither reverts the selection nor closes itself — the caller
481
+ decides what "cancel" means
482
+
483
+ #### Apply
484
+
485
+ **Given** `variant="multiple"` or `"range"`
486
+ **When** the Apply button is pressed
487
+ **Then** the apply event fires
488
+ **And** the panel stays mounted and keeps its selection — the caller commits and
489
+ dismisses the surrounding surface
490
+
491
+ #### Apply is never disabled
492
+
493
+ **Given** `variant="multiple"` or `"range"` with an empty or partial selection
494
+ (e.g. `range` with only `from` set, or nothing selected at all)
495
+ **When** the Apply button is pressed
496
+ **Then** the apply event still fires — the panel does not gate Apply on
497
+ selection completeness or on the selection having changed
498
+ **And** it is the caller's responsibility to disable/hide Apply or otherwise
499
+ guard `onApply` if committing an empty or half-open selection is undesirable
500
+
501
+ #### No footer events for `single`
502
+
503
+ **Given** `variant="single"` with cancel and apply handlers supplied
504
+ **When** the user interacts with the panel
505
+ **Then** neither handler can ever fire — no footer is rendered
506
+
507
+ #### Footer labels are overridable
508
+
509
+ **Given** `cancelLabel` and `applyLabel`
510
+ **When** the footer renders
511
+ **Then** the buttons use those strings as their visible (and accessible) names
512
+
513
+ ---
514
+
515
+ ### Disabled and outside days
516
+
517
+ #### Disabled days cannot be selected
518
+
519
+ **Given** `disabled={{ before: today }}`
520
+ **When** the grid renders
521
+ **Then** every day before today renders in the disabled value color and takes no
522
+ pointer events
523
+ **And** clicking one changes nothing and fires no select event
524
+
525
+ #### Outside days
526
+
527
+ **Given** `showOutsideDays` left at its default
528
+ **When** the grid renders
529
+ **Then** the leading/trailing days of the adjacent months fill the first and last
530
+ week rows, in the disabled value color
531
+ **And** they stay clickable and selectable — unlike a `disabled` day, they are
532
+ only visually de-emphasized, not blocked from pointer interaction; they are
533
+ excluded only from keyboard focus (Tab / arrow-key roaming can never land on
534
+ one)
535
+
536
+ **Given** `showOutsideDays={false}`
537
+ **When** the grid renders
538
+ **Then** those cells are empty instead
539
+
540
+ ---
541
+
542
+ ### Controlled vs uncontrolled selection
543
+
544
+ #### Uncontrolled
545
+
546
+ **Given** `selected` supplied without a select handler
547
+ **When** days are clicked
548
+ **Then** the panel starts from `selected` and then manages the selection itself
549
+
550
+ #### Controlled
551
+
552
+ **Given** both `selected` and the select handler supplied
553
+ **When** a day is clicked
554
+ **Then** the event fires with the next value
555
+ **And** the highlighting only changes once the consumer passes the new `selected`
556
+ back — a handler that ignores the value leaves the grid unchanged
557
+
558
+ ---
559
+
560
+ ### Week start
561
+
562
+ #### Starts on Monday by default
563
+
564
+ **Given** neither `weekStartsOn` nor `locale`
565
+ **When** the grid renders
566
+ **Then** the weekday row begins with Monday, per the design
567
+
568
+ **Given** `weekStartsOn={0}`
569
+ **When** the grid renders
570
+ **Then** the weekday row and every week begin with Sunday
571
+
572
+ #### Follows the locale when `weekStartsOn` is unset
573
+
574
+ **Given** a `locale` with its own week start (e.g. `enUS` → Sunday) and no
575
+ `weekStartsOn`
576
+ **When** the grid renders
577
+ **Then** the weekday row begins on that locale's first day, not Monday
578
+
579
+ **Given** both `locale` and `weekStartsOn`
580
+ **When** the grid renders
581
+ **Then** the explicit `weekStartsOn` wins over the locale
582
+
583
+ ---
584
+
585
+ ### Text direction
586
+
587
+ #### `dir` mirrors keyboard roaming, not just layout
588
+
589
+ **Given** `dir="rtl"` passed to the panel
590
+ **When** the day grid renders
591
+ **Then** the `range` divider and the panel's own layout already mirror via
592
+ logical CSS properties, regardless of `dir`
593
+ **And**, additionally, Arrow Left/Right roaming direction flips — because
594
+ `dir` is forwarded to the underlying day grid explicitly, which only mirrors
595
+ its own keyboard handling when `dir` reaches it directly
596
+
597
+ **Given** an ancestor sets `dir="rtl"` but the panel's own `dir` prop is left
598
+ unset
599
+ **When** the day grid renders
600
+ **Then** the panel's layout still mirrors (inherited via logical properties)
601
+ **But** Arrow Left/Right roaming direction does **not** flip
602
+
603
+ ## Accessibility
604
+
605
+ The day grid's roles, keyboard model and selection semantics are **inherited from
606
+ `react-day-picker`** — none of it is custom. This component replaces two of the
607
+ library's rendered parts (`MonthCaption` → the `InputSelect` header,
608
+ `DayButton` → the styled day cell) and adds the footer; everything else, the
609
+ grid semantics included, is the library's.
610
+
611
+ ## Roles & semantics (inherited from react-day-picker)
612
+
613
+ - Each month's grid is a `<table role="grid">`, with a weekday header row of
614
+ column headers and one row per week.
615
+ - Each day cell is a `gridcell` containing a focusable `<button>`; the selected
616
+ state is exposed via `aria-selected` on the cell.
617
+ - `multiple` and `range` grids additionally carry `aria-multiselectable` on the
618
+ grid — the only announced difference between the variants' grids.
619
+ - Days matched by `disabled` render as disabled buttons — out of the tab order
620
+ and not activatable.
621
+ - Outside days (the adjacent months' filler days) are rendered but only
622
+ visually de-emphasized — react-day-picker exposes them via a `data-outside`
623
+ hook on the cell with no additional ARIA state (no `aria-hidden`, no
624
+ `aria-disabled`). They stay mouse-clickable and selectable, but
625
+ react-day-picker excludes them from the roving-tabindex focus target, so
626
+ they can never be reached via Tab or arrow-key roaming — pointer-only, not
627
+ "ordinary" day buttons.
628
+ - The day button additionally carries `data-day`, `data-active` (a selected day
629
+ or either range endpoint) and `data-range-middle` (a day between the
630
+ endpoints). These are styling hooks, not a11y state — `aria-selected` remains
631
+ the announced source of truth.
632
+
633
+ ## The month / year dropdowns
634
+
635
+ - The header is **not** react-day-picker's `captionLayout="dropdown"` native
636
+ `<select>` pair — it is two real `InputSelect` instances, so they inherit
637
+ `InputSelect`'s listbox semantics, typeahead and keyboard model.
638
+ - Neither dropdown has a visible label in the design, so the component supplies
639
+ the accessible name itself: `monthLabel` (default `'Month'`) and `yearLabel`
640
+ (default `'Year'`) are applied as the trigger's `aria-label`. Both are props —
641
+ override them to localize.
642
+ - The month dropdown's option text comes from `formatMonthLabel`, so the
643
+ announced month names localize with it.
644
+
645
+ ## Keyboard
646
+
647
+ | Key | Element | Action |
648
+ | ------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
649
+ | Tab / Shift+Tab | panel | Moves between the month dropdown, the year dropdown, the grid, and the footer buttons |
650
+ | Arrow Left / Right | day grid | Moves focus one day back / forward; mirrors under `dir="rtl"` (inherited, but only when `dir` is passed to the panel — see the `dir` prop) |
651
+ | Arrow Up / Down | day grid | Moves focus one week back / forward (inherited) |
652
+ | Home / End | day grid | Moves focus to the first / last day of the week (inherited) |
653
+ | Page Up / Page Down | day grid | Moves focus to the previous / next month (inherited) |
654
+ | Shift+Arrow Left / Right | day grid | Moves focus to the same weekday in the previous / next month (inherited) |
655
+ | Shift+Arrow Up / Down | day grid | Moves focus to the same date in the previous / next year (inherited) |
656
+ | Shift+Page Up / Page Down | day grid | Moves focus to the previous / next year (inherited) |
657
+ | Enter / Space | focused day | Selects the focused day (inherited) |
658
+ | Enter / Space, Arrow Up / Down | month / year dropdown | Opens the dropdown and moves through options (InputSelect) |
659
+ | Escape | open dropdown | Closes the dropdown without changing the month (InputSelect) |
660
+ | Enter / Space | Cancel / Apply | Activates the footer action |
661
+
662
+ The grid is a single tab stop with arrow-key roaming (the roving-tabindex
663
+ day-grid model), so Tab does not walk 28–42 individual day buttons. When
664
+ react-day-picker marks a day as focused, the day button pulls DOM focus to
665
+ itself so the roaming stays in sync.
666
+
667
+ `CalendarPanel` renders a **panel**, not a modal dialog — it has no
668
+ `role="dialog"`, no focus trap and no Escape-to-close. Wrap it in a `Popover`
669
+ or `Dialog` when those semantics are wanted; that wrapper owns them.
670
+
671
+ ## Focus indication
672
+
673
+ - The focused day cell shows a 3px focus ring in `--ui-focus-primary`, raised
674
+ above its neighbours (`z-index`) so the ring is not clipped by the adjacent
675
+ flat cells.
676
+ - The dropdowns and footer buttons use their own components' focus rings, which
677
+ resolve to the same `--ui-focus-primary` token.
678
+
679
+ ## Screen reader
680
+
681
+ 1. Moving into the grid announces the focused date and, when selected, its
682
+ selected state.
683
+ 2. Disabled days are announced as unavailable.
684
+ 3. Choosing a month or year re-renders the grid; the newly focused date is
685
+ announced on the next arrow-key move.
686
+ 4. The footer buttons announce their visible labels (`cancelLabel` /
687
+ `applyLabel`).
688
+
689
+ ## Color and contrast
690
+
691
+ | Element | Minimum ratio | Standard |
692
+ | --------------------------------------- | ------------- | ---------------------- |
693
+ | Day number vs cell background | 4.5:1 | WCAG 1.4.3 (AA) |
694
+ | Selected day number vs active cell fill | 4.5:1 | WCAG 1.4.3 (AA) |
695
+ | Weekday abbreviation vs panel surface | 4.5:1 | WCAG 1.4.3 (AA) |
696
+ | Panel / divider borders vs surface | 3:1 | WCAG 1.4.11 (non-text) |
697
+ | Focus indicator | 3:1 | WCAG 1.4.11 |
698
+
699
+ Disabled days use `--ui-calendar-value-color-disabled` and are exempt from the
700
+ text-contrast minimum (WCAG 1.4.3 excludes inactive controls).
701
+
702
+ Outside days (the adjacent months' filler days — see above) share that same
703
+ `--ui-calendar-value-color-disabled` token even though they remain
704
+ mouse-clickable and selectable, not inactive controls in the ARIA sense —
705
+ they are simply unreachable by keyboard (see "Roles & semantics" above).
706
+ Measured against `--ui-calendar-container-color`, this renders at 2.13:1
707
+ (light) / 2.31:1 (dark) — below the 4.5:1 minimum this table requires
708
+ elsewhere. This is a known, accepted design trade-off (visual de-emphasis of
709
+ adjacent-month days takes priority over AA contrast for that one element),
710
+ not a WCAG exemption — unlike disabled days, no exclusion in the standard
711
+ actually applies to them.
712
+
713
+ Regardless of contrast, selection is never conveyed by color alone —
714
+ `aria-selected` carries it for assistive tech.
715
+
716
+ ## Testing checklist
717
+
718
+ - [ ] Each month renders a `role="grid"` table with a weekday header row
719
+ - [ ] Day cells are `gridcell`s and expose `aria-selected`
720
+ - [ ] Arrow keys roam the grid; Tab does not enter every day button
721
+ - [ ] Enter/Space on the focused day selects it
722
+ - [ ] The month and year dropdowns have accessible names from `monthLabel` /
723
+ `yearLabel`, and both are overridable
724
+ - [ ] The focused day shows the `--ui-focus-primary` ring, unclipped
725
+ - [ ] Disabled days are not focusable or selectable
726
+ - [ ] Outside days are not focusable via Tab or arrow-key roaming, but remain
727
+ mouse-clickable and selectable
728
+ - [ ] Cancel / Apply announce `cancelLabel` / `applyLabel`, and are absent for
729
+ `variant="single"`
730
+ - [ ] The panel renders correctly under `dir="rtl"`, including the `range`
731
+ divider (an inline-end border, so it mirrors)
732
+ - [ ] With `dir="rtl"` passed to the panel, Arrow Left/Right roaming direction
733
+ also mirrors (it only flips when `dir` reaches the day grid directly)